awesome-everything EN
↑ Обратно к восхождению

Браузер и фронтенд-рантайм

BeginMainFrame, анимации на потоке compositor и память GPU

Суть Как работает двухпоточное рукопожатие Chromium, почему CSS-анимации на transform/opacity выживают при заблокированном главном потоке, и что происходит при исчерпании памяти GPU.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min

CSS-анимация работает плавно на 60 fps, пока главный поток заблокирован на 400 мс парсингом JSON-блоба. rAF-анимация на том же элементе замерзает на те же 400 мс. Одно железо, одно свойство, противоположные результаты — потому что одна передаёт управление compositor, другая нет.

Рукопожатие BeginMainFrame

В процессе рендерера Chromium поток compositor является драйвером, а главный поток — исполнителем. Поток за каждый vsync:

  1. Compositor посылает BeginMainFrame главному потоку
  2. Главный поток выполняет всю покадровую работу: rAF-колбэки, стили, компоновку, подготовку отрисовки
  3. Главный поток посылает CommitMainFrame обратно, передавая новое дерево слоёв
  4. Compositor просит растровых воркеров заполнить грязные тайлы bitmap-ами
  5. Compositor выполняет GPU-draw и отображает кадр

Если главный поток слишком медленный, compositor не ждёт: он отправляет дерево слоёв предыдущего кадра снова («нарисованный, но устаревший» кадр). Пользователь воспринимает это как пропущенный ввод или заикание.

BeginMainFrame приходит каждые ~16.67 мс, готов главный поток или нет. Это глубинная причина, почему работа главного потока не может превышать бюджет кадра: метроном неустанен.

Поток BeginMainFrame

CompositorBeginMainFrame →
Главный потокrAF + style + layout + paint setup
Главный поток← CommitMainFrame
Compositorрастровые воркеры заполняют грязные тайлы → GPU draw
Если главный поток опаздывает: compositor отправляет предыдущий кадр («устаревший кадр»)

CSS-анимации на потоке compositor

Когда вы пишете CSS-анимацию, изменяющую только transform или opacity на элементе с продвижением compositor, браузер обнаруживает это в начале анимации и передаёт таймлайн анимации напрямую потоку compositor. С этого момента главный поток вообще не участвует — compositor интерполирует значение кадр за кадром и рендерит следующий bitmap напрямую.

Вот почему CSS-анимация может работать плавно, даже когда главный поток полностью заблокирован (alert-диалог, долгий синхронный парсинг, пауза дебаггера): compositor всё равно работает.

Та же анимация, написанная на JS через rAF, не может этого делать, потому что rAF работает на главном потоке. Заблокированный главный поток замораживает rAF-анимацию.

CSS-анимации на потоке compositor — единственный способ гарантировать непрерывность анимации под нагрузкой на главный поток.

Исчерпание памяти GPU и вытеснение слоёв

Слои не бесплатны. Каждый слой — GPU-bitmap, стоящий ширина × высота × 4 байта. На телефоне с 256 МБ GPU-памяти пятьдесят слоёв 1080p исчерпывают бюджет. Когда ОС начинает вытеснять тайлы:

  1. Браузер обнаруживает вытесненные тайлы
  2. Растровые воркеры растеризуют их повторно на лету
  3. Пока растеризация идёт, compositor компонует с неполными тайлами
  4. Страница заикается — часто хуже, чем если бы слои вообще не запрашивались

Анти-паттерн will-change вызывает это: will-change: transform на каждом компоненте постоянно резервирует слой для каждого экземпляра. Список из 100 карточек с 5 элементами с will-change каждая держит 500 слоёв.

Правильный паттерн: устанавливайте will-change непосредственно перед началом анимации (на mouseenter, или в слушателе transitionstart), убирайте на animationend или transitionend. Это держит GPU-память вблизи нуля когда пользователь не взаимодействует активно.

Почему это работает

Почему will-change: transform вызывает продвижение ещё до начала анимации? Браузеру нужно время для растеризации слоя в GPU-память до первого кадра анимации. Если он ждал до начала анимации, первые 1–3 кадра были бы пустыми пока шла растеризация. will-change — подсказка «подготовься сейчас, не при старте анимации». Цена — хранение bitmap на протяжении действия подсказки — вот почему подсказку нужно убирать по окончании анимации.

Выбери лучший вариант

Анимировать карточку с y=0 до y=200 за 300 мс при 60 fps. Выберите реализацию.

Викторина

Главный поток заблокирован на 400 мс синхронным JSON.parse. Какой вид анимации продолжает работать плавно на 60 fps во время блокировки?

Викторина

GPU-память раздувается на мобильном устройстве. Профилирование показывает 500 слоёв compositor. Большинство от карточек в списке, каждая с `will-change: transform`, установленным постоянно при монтировании. Каково точечное лечение?

Вспомните перед уходом
  1. 01
    Что делает compositor, если главный поток пропускает дедлайн BeginMainFrame?
  2. 02
    Почему CSS transform-анимация выживает при заблокированном главном потоке, а rAF-анимация нет?
  3. 03
    Что такое анти-паттерн will-change и как его лечить?
Итог

Compositor Chromium посылает BeginMainFrame каждые ~16.67 мс. Главный поток отвечает CommitMainFrame, если успевает; если нет — compositor отправляет предыдущий кадр как устаревший. CSS-анимации на transform или opacity продвинутых элементов передаются compositor при старте — главный поток вне игры, поэтому они выживают при заблокированном главном потоке на 60 fps. rAF-анимации работают на главном потоке и замерзают при блокировке. Анти-паттерн will-change постоянно резервирует GPU-bitmap; на телефоне с 256 МБ GPU-памяти 500 слоёв от дизайн-системы исчерпывают бюджет, ОС вытесняет тайлы. Ограничивайте will-change длительностью анимации, чтобы держать GPU-память вблизи нуля в покое.

Связанные уроки
встречается в143
Продолжить восхождение ↑Observability в проде: LoAF, INP и полная поверхность атаки
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources4
expand
  1. 01
  2. 02
  3. 03
  4. 04

Trademarks belong to their respective owners. Editorial reference only.