# Пятьдесят карточек на одном холсте: куда уходил процессор, и карточка «Команда»

> Как NeuroSquad снизил нагрузку простаивающего холста на процессор со ~100% до ~15%: замеренные причины, пул WebGL-контекстов, семантический зум и новая карточка «Команда».

- URL: https://neurosquad.ai/ru/blog/fifty-cards-one-canvas/
- Published: 2026-09-24
- Publisher: NeuroSquad (https://neurosquad.ai/)

NeuroSquad 0.1.117 — выпуск, который замечаешь в основном по тому, что прекратилось: вентилятор, разгоняющийся, когда ничего не происходит, рывки при перетаскивании рамки, полной карточек, подвисание каждый раз, когда отдаляешь холст за порог, после которого карточки становятся плитками. А ещё в нём появилась карточка «Команда» — одна доска, где видно, чем занят каждый агент воркспейса.

Цель для холста не менялась с первых недель: до пятидесяти живых карточек — терминалов, браузеров, агентов — с плавными паном, зумом, перетаскиванием и изменением размера. Правило, как к ней идти, тоже не менялось: сначала замерить. Каждое число в этой записи получено на отдельном экземпляре приложения со свежим профилем, которым управляли настоящими событиями ввода через протокол DevTools.

## Первый раунд: пятьдесят терминалов

Первый проход случился ещё до публичного выпуска — на воркспейсе из пятидесяти карточек, где двенадцать терминалов печатали с максимальной скоростью. Зум тормозил по причине, которую легко пропустить: шапка каждой карточки держит постоянный экранный размер, и делала она это через `width` и `margin`. Это свойства раскладки. Каждый кадр зума менял размер каждой шапки, та меняла размер тела карточки, а оно дёргало `ResizeObserver` каждого терминала — пятьдесят подгонок терминала, пятьдесят сообщений в главный процесс, пятьдесят ресайзов консоли. На каждый кадр.

Исправление: фиксировать значение зума один раз за жест, а не на каждом кадре, менять размер терминала, только когда реально меняется его сетка из колонок и строк, и на время движения зума подменять живые сетки терминалов статичной заглушкой. Затем терминалы перешли с DOM-рендерера xterm на WebGL-рендерер — и именно туда уходила большая часть времени: медиана кадра зума упала с 18,2 до 6,1 мс, p95 — со 133 до 12 мс.

> **Chromium держит ровно 16 WebGL-контекстов на процесс** Мы это измерили, а не предположили. С WebGL на всех пятидесяти терминалах за секунду получалось «live: 16, lost: 34» — причём выживали шестнадцать смонтированных первыми, а не те, на которые вы смотрите. Поэтому терминалы получают контексты из пула на пятнадцать: их выдают тем, что на экране, и забирают у тех, что с него ушли.

Один замер того раунда определил правило, которое до сих пор живёт в коде. Заглушки вместо терминалов вдвое ускорили зум (медиана 6,1 против 12,0 мс) и замедлили пан (в среднем 19,8 против 16,3 мс): пан просто сдвигает уже нарисованные слои, и спрятать пятьдесят сеток, а потом вернуть их, стоит дороже, чем даёт. Поэтому заглушки появляются, только когда жест действительно меняет зум.

## Второй раунд: холст в простое

Второй раунд начался с более странной жалобы: холст тяжёлый, даже когда *ничего* не происходит. Четыре параллельных ревью кода и одна сессия живого профилирования на воркспейсе «Launch day» нашли не одну причину, а кучу мелких постоянных трат. Самой крупной оказался мультяшный персонаж.

У карточек ИИ-агентов есть маленький маскот, который показывает состояние агента. Его анимации ожидания были бесконечными CSS-анимациями на дочерних элементах SVG — а их браузер перерисовывает в главном потоке, каждый кадр, вечно. С тремя карточками Claude Code на холсте при зуме 60% процесс рендерера занимал 98–104% ядра, а GPU — 32–36%, ничего при этом не делая. Теперь зациклено только состояние «работает», и двигается в нём HTML-обёртка, с которой композитор справляется сам; остальные состояния проигрываются несколько раз и замирают в позе, которая сама говорит, что происходит.

- **~100% → 15%** — процессор рендерера, холст в простое с тремя карточками агентов
- **~34% → 3%** — GPU на том же холсте
- **180 → 66** — композитных слоёв после одного CSS-правила

Остальное — список вещей, каждая из которых стоила немного, но все работали постоянно:

- **Селекторы с `*` и `:has()` внутри холста.** Переключение класса на панели холста каждый раз стоило 25–34 мс пересчёта стилей: правила вроде «каждый потомок жеста» приходилось заново сопоставлять с каждым узлом каждой карточки. Явный список элементов, которые действительно анимируются, свёл это примерно к 1,5 мс.
- **CSS-переменная на панели.** Уровень зума жил в кастомном свойстве на элементе, который содержит все карточки, и его изменение инвалидировало стили всего поддерева, терминалы включительно, — 22–34 мс на запись. Теперь это одно правило в теге `<style>`, которое касается только элементов, читающих это значение.
- **Кнопки со своими слоями.** Библиотека компонентов даёт кнопкам собственный GPU-слой. Внутри холста это означало десятки лишних слоёв; одно переопределение вне нажатого состояния снизило их число со 180 до 66.
- **Таймеры в карточках.** Карточки, которые что-то опрашивают, — порты, бюджет, стендап, наблюдение за страницей, — теперь тикают, только пока их воркспейс открыт и окно видно, и делят одни часы на период вместо собственного интервала у каждой.
- **Браузерная карточка, на которую никто не смотрел,** продолжала стримить кадры. Теперь кадры идут, только пока карточку показывает хоть одно окно: в обзоре и в скрытых воркспейсах — ноль, при возврате — снова идут.

Долгие задачи во время жестов исчезли из перетаскивания рамки, выделения рамкой, зума через порог обзора и пана в обзоре. Пану и перетаскиванию теперь нужно примерно вдвое меньше процессора рендерера; время стилей и раскладки в трассе пана упало с 311 до 53 мс и с 378 до 28 мс.

## Когда карточки слишком мелкие, чтобы их читать

Отдалите холст как следует — и терминал станет серым прямоугольником. Ниже 55% (порог меняется в настройках) каждая карточка показывает вместо содержимого плитку: иконку, название и один факт, важный для её типа, — состояние агента, `3/11` у списка задач, колонки доски, первую строку заметки.

Тело карточки остаётся смонтированным под плиткой, в своём настоящем размере: терминал не убивается и даже не подгоняется заново. Плитка рисуется в экранных пикселях, поэтому читается при любом зуме. Пересечение порога перерисовывает ноль тел карточек и каждую плитку по одному разу; пан не перерисовывает вообще ничего.

![Воркспейс Launch day при зуме ниже 55%: каждая карточка — читаемая плитка с состоянием](https://neurosquad.ai/blog/fifty-cards-one-canvas/overview-ru.webp)

*«Launch day» издалека: каждая карточка превращается в плитку с главным фактом, а терминалы под ними продолжают работать.*

Есть одна тонкость. Терминал за плиткой невидим, но не на паузе — собственная проверка видимости у библиотеки терминала не замечает CSS-свойство `visibility`, — так что он продолжал бы рисовать вывод, который никто не видит. Вместо этого карточка копит вывод и записывает его одним махом, когда вы снова приближаете холст. Кроме одного вида фрагментов: вопрос, который программа задаёт терминалу («кто ты?», «где курсор?»), — часть диалога, и ответить на него нужно сразу. Запоздалый ответ появлялся в приглашении bash буквальным `1;2c` — найдено вживую и исправлено.

## Карточка «Команда»

Когда на холсте много агентов, вопрос «кто меня ждёт?» не должен требовать экскурсии по холсту. Карточка «Команда» отвечает на него одной доской: все ИИ-агенты воркспейса по группам *ждёт вас*, *работает*, *закончил* и *свободен* — с CLI, моделью и тем, сколько длится текущий ход. Нажмите на плитку, чтобы отфильтровать список; нажмите на строку — и камера перелетит к этой карточке.

*Копия карточки «Команда» со строками самого приложения. Часы идут; нажмите на плитку, а потом на строку.* (interactive: https://neurosquad.ai/ru/blog/fifty-cards-one-canvas/)

Колонка модели аккуратнее, чем кажется. Она показывает модель последнего запроса агента, как она записана в его собственном журнале сессии; если такой записи нет — модель, заданную на карточке; если нет и её — модель по умолчанию из файла настроек самого CLI. Когда нет ничего из этого, там написано «Модель по умолчанию», а не догадка.

![Карточка «Команда» в воркспейсе Launch day: три агента Claude Code и их состояние](https://neurosquad.ai/blog/fifty-cards-one-canvas/squad-ru.webp)

*Настоящая карточка. Строка один раз вспыхивает янтарным, когда агент останавливается, чтобы что-то у вас спросить.*

«Команда» добавляется в каждый воркспейс один раз — левее самой левой карточки. Если её удалить, она останется удалённой; вернуть её можно из меню. Чтобы «один раз» работало правильно, понадобилось ещё одно исправление: первая версия ставила карточку раньше, чем загружалась сохранённая раскладка воркспейса, и холст раскладывал все карточки заново. Первое открытие существующего воркспейса могло сложить все его карточки в один угол. Это и есть баг из раздела «Исправлено» этого выпуска.

Изменилось и меню добавления: агенты идут первыми, установленные на этом компьютере отмечены, есть поиск, а пустой холст открывается крупными плитками агентов. Полный список — в [журнале изменений 0.1.117](https://neurosquad.ai/ru/changelog/#v0.1.117).
