Пятьдесят карточек на одном холсте: куда уходил процессор, и карточка «Команда»
Простаивающий холст с тремя карточками агентов съедал целое ядро процессора. Виноват оказался мультяшный персонаж. Что мы намерили, что поменяли, и карточка, которая теперь есть на каждом холсте.
NeuroSquad 0.1.117 — выпуск, который замечаешь в основном по тому, что прекратилось: вентилятор, разгоняющийся, когда ничего не происходит, рывки при перетаскивании рамки, полной карточек, подвисание каждый раз, когда отдаляешь холст за порог, после которого карточки становятся плитками. А ещё в нём появилась карточка «Команда» — одна доска, где видно, чем занят каждый агент воркспейса.
Цель для холста не менялась с первых недель: до пятидесяти живых карточек — терминалов, браузеров, агентов — с плавными паном, зумом, перетаскиванием и изменением размера. Правило, как к ней идти, тоже не менялось: сначала замерить. Каждое число в этой записи получено на отдельном экземпляре приложения со свежим профилем, которым управляли настоящими событиями ввода через протокол DevTools.
Первый раунд: пятьдесят терминалов
Первый проход случился ещё до публичного выпуска — на воркспейсе из пятидесяти карточек, где двенадцать терминалов печатали с максимальной скоростью. Зум тормозил по причине, которую легко пропустить: шапка каждой карточки держит постоянный экранный размер, и делала она это через width и margin. Это свойства раскладки. Каждый кадр зума менял размер каждой шапки, та меняла размер тела карточки, а оно дёргало ResizeObserver каждого терминала — пятьдесят подгонок терминала, пятьдесят сообщений в главный процесс, пятьдесят ресайзов консоли. На каждый кадр.
Исправление: фиксировать значение зума один раз за жест, а не на каждом кадре, менять размер терминала, только когда реально меняется его сетка из колонок и строк, и на время движения зума подменять живые сетки терминалов статичной заглушкой. Затем терминалы перешли с DOM-рендерера xterm на WebGL-рендерер — и именно туда уходила большая часть времени: медиана кадра зума упала с 18,2 до 6,1 мс, p95 — со 133 до 12 мс.
Один замер того раунда определил правило, которое до сих пор живёт в коде. Заглушки вместо терминалов вдвое ускорили зум (медиана 6,1 против 12,0 мс) и замедлили пан (в среднем 19,8 против 16,3 мс): пан просто сдвигает уже нарисованные слои, и спрятать пятьдесят сеток, а потом вернуть их, стоит дороже, чем даёт. Поэтому заглушки появляются, только когда жест действительно меняет зум.
Второй раунд: холст в простое
Второй раунд начался с более странной жалобы: холст тяжёлый, даже когда ничего не происходит. Четыре параллельных ревью кода и одна сессия живого профилирования на воркспейсе «Launch day» нашли не одну причину, а кучу мелких постоянных трат. Самой крупной оказался мультяшный персонаж.
У карточек ИИ-агентов есть маленький маскот, который показывает состояние агента. Его анимации ожидания были бесконечными CSS-анимациями на дочерних элементах SVG — а их браузер перерисовывает в главном потоке, каждый кадр, вечно. С тремя карточками Claude Code на холсте при зуме 60% процесс рендерера занимал 98–104% ядра, а GPU — 32–36%, ничего при этом не делая. Теперь зациклено только состояние «работает», и двигается в нём HTML-обёртка, с которой композитор справляется сам; остальные состояния проигрываются несколько раз и замирают в позе, которая сама говорит, что происходит.
Остальное — список вещей, каждая из которых стоила немного, но все работали постоянно:
- Селекторы с
*и:has()внутри холста. Переключение класса на панели холста каждый раз стоило 25–34 мс пересчёта стилей: правила вроде «каждый потомок жеста» приходилось заново сопоставлять с каждым узлом каждой карточки. Явный список элементов, которые действительно анимируются, свёл это примерно к 1,5 мс. - CSS-переменная на панели. Уровень зума жил в кастомном свойстве на элементе, который содержит все карточки, и его изменение инвалидировало стили всего поддерева, терминалы включительно, — 22–34 мс на запись. Теперь это одно правило в теге
<style>, которое касается только элементов, читающих это значение. - Кнопки со своими слоями. Библиотека компонентов даёт кнопкам собственный GPU-слой. Внутри холста это означало десятки лишних слоёв; одно переопределение вне нажатого состояния снизило их число со 180 до 66.
- Таймеры в карточках. Карточки, которые что-то опрашивают, — порты, бюджет, стендап, наблюдение за страницей, — теперь тикают, только пока их воркспейс открыт и окно видно, и делят одни часы на период вместо собственного интервала у каждой.
- Браузерная карточка, на которую никто не смотрел, продолжала стримить кадры. Теперь кадры идут, только пока карточку показывает хоть одно окно: в обзоре и в скрытых воркспейсах — ноль, при возврате — снова идут.
Долгие задачи во время жестов исчезли из перетаскивания рамки, выделения рамкой, зума через порог обзора и пана в обзоре. Пану и перетаскиванию теперь нужно примерно вдвое меньше процессора рендерера; время стилей и раскладки в трассе пана упало с 311 до 53 мс и с 378 до 28 мс.
Когда карточки слишком мелкие, чтобы их читать
Отдалите холст как следует — и терминал станет серым прямоугольником. Ниже 55% (порог меняется в настройках) каждая карточка показывает вместо содержимого плитку: иконку, название и один факт, важный для её типа, — состояние агента, 3/11 у списка задач, колонки доски, первую строку заметки.
Тело карточки остаётся смонтированным под плиткой, в своём настоящем размере: терминал не убивается и даже не подгоняется заново. Плитка рисуется в экранных пикселях, поэтому читается при любом зуме. Пересечение порога перерисовывает ноль тел карточек и каждую плитку по одному разу; пан не перерисовывает вообще ничего.
Есть одна тонкость. Терминал за плиткой невидим, но не на паузе — собственная проверка видимости у библиотеки терминала не замечает CSS-свойство visibility, — так что он продолжал бы рисовать вывод, который никто не видит. Вместо этого карточка копит вывод и записывает его одним махом, когда вы снова приближаете холст. Кроме одного вида фрагментов: вопрос, который программа задаёт терминалу («кто ты?», «где курсор?»), — часть диалога, и ответить на него нужно сразу. Запоздалый ответ появлялся в приглашении bash буквальным 1;2c — найдено вживую и исправлено.
Карточка «Команда»
Когда на холсте много агентов, вопрос «кто меня ждёт?» не должен требовать экскурсии по холсту. Карточка «Команда» отвечает на него одной доской: все ИИ-агенты воркспейса по группам ждёт вас, работает, закончил и свободен — с CLI, моделью и тем, сколько длится текущий ход. Нажмите на плитку, чтобы отфильтровать список; нажмите на строку — и камера перелетит к этой карточке.
Колонка модели аккуратнее, чем кажется. Она показывает модель последнего запроса агента, как она записана в его собственном журнале сессии; если такой записи нет — модель, заданную на карточке; если нет и её — модель по умолчанию из файла настроек самого CLI. Когда нет ничего из этого, там написано «Модель по умолчанию», а не догадка.
«Команда» добавляется в каждый воркспейс один раз — левее самой левой карточки. Если её удалить, она останется удалённой; вернуть её можно из меню. Чтобы «один раз» работало правильно, понадобилось ещё одно исправление: первая версия ставила карточку раньше, чем загружалась сохранённая раскладка воркспейса, и холст раскладывал все карточки заново. Первое открытие существующего воркспейса могло сложить все его карточки в один угол. Это и есть баг из раздела «Исправлено» этого выпуска.
Изменилось и меню добавления: агенты идут первыми, установленные на этом компьютере отмечены, есть поиск, а пустой холст открывается крупными плитками агентов. Полный список — в журнале изменений 0.1.117.

