Отладка нестабильных CSS-анимаций с помощью DevTools
Нестабильные CSS-анимации возникают из-за превышения бюджета кадра: при 60fps браузер располагает примерно 16,7 мс на формирование каждого кадра, и любой кадр, обработка которого затянулась — из-за пересчёта макета, отрисовки или перегруженного основного потока — снижает частоту кадров и проявляется в виде заметных подёргиваний. Решение редко сводится к тому, чтобы «добавить побольше will-change». Это диагностика: нужно выяснить, какой этап конвейера рендеринга выполнялся слишком долго и в каком потоке. В этой статье описывается систематический рабочий процесс с использованием четырёх панелей Chrome DevTools — Rendering, Performance, Animations и Layers — для выявления причины нестабильной анимации, включая разбор конкретного примера «до и после», который можно воспроизвести самостоятельно.
Предполагается, что читатель знает, что transform и opacity — недорогие операции, но не имеет чёткой методики диагностики. Типичный случай: анимация отлично работает на ноутбуке и подёргивается на среднебюджетном Android-устройстве. Здесь нужна методика триажа, а не очередной совет по свойствам.
Ключевые выводы
- При 60fps у браузера есть около 16,7 мс на кадр для выполнения этапов style, layout, paint и composite; пропуск этого бюджета хотя бы раз приводит к заметному подёргиванию (документация Chrome DevTools по производительности).
- Диагностируйте последовательно по панелям: Rendering — для быстрой визуальной проверки, Performance — для выявления причины медленного кадра, Animations — для изоляции проблемного ключевого кадра, Layers — для оценки затрат памяти на уровне компоновщика.
- Фиолетовые полосы Recalculate Style или Layout непосредственно под жёлтой полосой JavaScript в треке Main панели Performance сигнализируют о принудительном синхронном макете; красный треугольник ведёт к конкретной строке JS.
- Компоновщик может анимировать
transformиopacityбез layout и paint; анимацияleft/top/width/heightпринудительно запускает layout основного потока на каждом кадре (CSS Triggers). - Анимации, управляемые прокруткой и использующие
animation-timeline, выполняются на компоновщике дляtransform/opacity, поэтому их нестабильность отображается в треке Frames, а не как долгая задача основного потока.
Что такое нестабильность анимации?
Нестабильность (jank) — это пропущенный или задержанный кадр, который пользователь воспринимает визуально. Для поддержания 60fps браузер должен завершать обработку каждого кадра примерно за 16,7 мс (1000 мс ÷ 60). В этот промежуток входят пересчёт стилей, макет, отрисовка и компоновка для данного кадра. Когда один кадр обрабатывается слишком долго, браузер не укладывается в дедлайн, эффективная частота кадров падает — до 30fps или ниже — и движение начинает «прыгать». Руководство Google по производительности рендеринга описывает тот же бюджет: плавные визуальные изменения должны укладываться в окно одного кадра, а анимации — наиболее заметное место, где этот бюджет нарушается, поскольку взгляд отслеживает непрерывное движение.
Почему один медленный кадр заметен, а один медленный сетевой запрос — нет: анимация — это последовательность кадров, которую мозг интегрирует в движение. Один запоздавший кадр нарушает эту интеграцию, и остановка в середине движения воспринимается как скачок. Именно поэтому «в целом плавно» недостаточно — воспринимаемое качество определяется худшим кадром, а не средним.
Конвейер рендеринга: краткий обзор
Каждое визуальное обновление проходит через фиксированный конвейер: parse → style → layout → paint → composite. Браузер разбирает HTML и CSS в DOM и CSSOM, вычисляет применяемые стили (style), рассчитывает геометрию и положение каждого блока (layout), растеризует пиксели в слои (paint) и, наконец, объединяет слои в отображаемое изображение (composite). Статья web.dev о конвейере рендеринга — это основной подробный справочник; для целей данной статьи достаточно краткого изложения.
Ключевой момент, на котором строится вся дальнейшая статья: каждый этап конвейера выполняется в определённом потоке и отображается в определённой панели DevTools. Layout и paint выполняются в основном потоке, наряду с вашим JavaScript. Компоновка выполняется отдельно. Анимация, которая только компонует — перемещает существующий слой, изменяет его прозрачность — практически полностью обходит основной поток. Анимация, вызывающая layout, возвращает работу в основной поток на каждом кадре, где она конкурирует со всем остальным. Именно это различие и позволяют увидеть описанные ниже панели.
Какие панели DevTools помогают диагностировать нестабильность анимации?
Discover how at OpenReplay.com.
Систематическая диагностика нестабильности анимации включает четыре панели в следующем порядке: Rendering — для быстрой визуальной проверки (мигает ли отрисовка там, где не должна?), Performance — для записи и выявления причины медленного кадра, Animations — для изоляции конкретного ключевого кадра или свойства, вызывающего проблему, и Layers — для оценки того, помогает ли продвижение слоя компоновщика или создаёт давление на память. Используйте их именно в таком порядке: Rendering за секунды исключает или подтверждает целые классы проблем, Performance даёт трассировку, Animations сужает область до конкретного свойства, а Layers проверяет стоимость вашего исправления.
Панель Rendering: быстрая проверка
Панель Rendering — это первая остановка, поскольку она визуально и немедленно отвечает на вопрос: перерисовывает ли ваша анимация то, что не должна. Откройте её через Command Menu (Cmd/Ctrl+Shift+P, введите «Show Rendering») или через More Tools → Rendering (справочник по рендерингу Chrome DevTools). Три переключателя имеют значение:
- Frame Rendering Stats отображает живой счётчик FPS и оверлей с данными GPU-памяти во время работы анимации. Значение, которое значительно падает ниже 60 во время анимации, подтверждает наличие нестабильности.
- Paint flashing подсвечивает зелёным цветом области, которые браузер перерисовывает. Элемент, анимирующий только
transform, не должен давать зелёной вспышки при движении; зелёная вспышка, сопровождающая анимацию, означает, что происходит paint. - Layer borders обводит слои компоновщика оранжевым цветом. Используйте его, чтобы убедиться, что элемент, который должен быть аппаратно ускорен, действительно получил собственный слой, — и чтобы обнаружить непреднамеренно созданные слои.
Порядок действий:
- Откройте панель Rendering.
- Включите Paint flashing и запустите анимацию.
- Если анимируемый элемент мигает зелёным при движении, анимация перерисовывается на каждом кадре — анимируется свойство, затрагивающее layout или paint. Это указывает на проблему уровня свойств и означает, что следующим шагом нужно открыть панель Performance.
- Если вспышек нет, но движение всё равно подёргивается, узкое место, скорее всего, находится в JavaScript основного потока, а не в paint — это тоже вопрос для панели Performance.
Панель Performance: выявление причины медленного кадра
Панель Performance позволяет записать анимацию и точно определить, какой этап конвейера превысил бюджет кадра. Она отображает диаграмму FPS, тайминг каждого кадра в треке Frames, активность основного потока Main и — в актуальных версиях Chrome — боковую панель Insights, которая автоматически выявляет проблемы, такие как принудительный reflow (справочник по производительности Chrome DevTools).
Перед записью ограничьте CPU, чтобы приблизить условия к устройству, на котором реально наблюдается нестабильность. Chrome DevTools документирует пресеты ограничения CPU в разделе Capture settings; панель предлагает пресет «4x slowdown», при этом рекомендуется тестировать с замедлением, приближённым к менее мощному оборудованию (справочник по производительности, ограничение CPU). Ограничение важно, потому что наиболее распространённая причина, по которой CSS-анимация проходит локальное профилирование, но подёргивается в продакшне, — это контекст устройства: среднебюджетный Android с несколькими открытыми вкладками располагает лишь долей CPU-бюджета ноутбука разработчика, что ограничение приближённо воспроизводит, но не может полностью воспроизвести без симуляции давления памяти и параллельной нагрузки на GPU.
Порядок действий:
- Откройте панель Performance и включите Screenshots.
- В Capture settings (значок шестерёнки) установите ограничение CPU.
- Нажмите Record, запустите анимацию на несколько секунд, затем нажмите Stop.
- Сначала просмотрите трек FPS/Frames. Красные метки над кадрами указывают на кадры, превысившие бюджет.
- Приблизьте проблемный кадр и изучите трек Main.
Вот наиболее полезная эвристика при отладке анимаций:
Фиолетовые полосы под жёлтыми в треке Main = принудительный синхронный layout. Красный треугольник — это ссылка на исправление.
В треке основного потока фиолетовые полосы Recalculate Style или Layout, появляющиеся непосредственно под жёлтой полосой JavaScript, сигнализируют о принудительном синхронном layout — браузер был вынужден разрешить геометрию в середине выполнения скрипта, потому что JavaScript читал свойство макета сразу после записи в DOM. Чтение offsetWidth, offsetTop или вызов getBoundingClientRect() после изменения стиля принудительно запускает синхронный layout; в каноническом списке Пола Айриша перечислены все такие триггеры. Красный треугольник на фиолетовой полосе открывает запись Summary с предупреждением «Layout Forced» и ссылкой на конкретную строку JS. Руководство web.dev по layout thrashing подробно рассматривает паттерн «чтение после записи».
Когда фиолетового под жёлтым нет, JavaScript завершил свою работу и позволил браузеру выполнять рендеринг по собственному расписанию. Именно к такой трассировке нужно стремиться.
Панель Animations: изоляция ключевого кадра
Панель Animations позволяет инспектировать, перематывать и замедлять активные анимации, чтобы привязать нестабильность к конкретному ключевому кадру или свойству, а не к анимации в целом. Откройте её через More Tools → Animations (документация Chrome DevTools по анимациям). Chrome отслеживает анимации и перечисляет их по мере запуска, позволяя инспектировать захваченную анимацию, перематывать её временну́ю шкалу и изучать ключевые кадры.
Диагностическая ценность этой панели раскрывается в сочетании с Paint flashing. Замедление анимации до 10% скорости воспроизведения при одновременном наблюдении за Paint flashing в панели Rendering — самый быстрый способ определить, какой именно ключевой кадр вызывает перерисовку: зелёная вспышка появляется в точный момент, когда вступает в силу значение проблемного свойства.
Порядок действий:
- Откройте панель Animations и запустите анимацию, чтобы она появилась в списке.
- Установите скорость воспроизведения 10% (элементы управления находятся в верхней части панели).
- С включённым Paint flashing перемотайте временну́ю шкалу и следите за зелёной вспышкой.
- Если зелёная вспышка появляется в определённой точке временно́й шкалы, сосредоточьте расследование на ключевом кадре, активном в этот момент.
Firefox и другие браузеры имеют собственные инспекторы анимаций; здесь предполагается работа в Chrome.
Панель Layers: оценка затрат компоновщика
Панель Layers показывает, какие элементы были продвинуты в собственный слой компоновщика, по какой причине и с какими затратами памяти — именно это позволяет не разбрасывать will-change повсюду. Откройте её через More Tools → Layers (документация Chrome DevTools по слоям). При выборе слоя в панели деталей отображается его потребление памяти и причина компоновки.
Продвижение — это компромисс. Перенос элемента на собственный слой позволяет компоновщику анимировать его без перерисовки соседних элементов, но каждый слой выделяет GPU-память под свою текстуру. Документация MDN по will-change прямо указывает, что это свойство является крайней мерой: применение его к слишком многим элементам расходует ресурсы впустую, поскольку браузер уже самостоятельно оптимизирует недорогие свойства, а избыточное продвижение может ухудшить производительность. Используйте панель Layers для подсчёта продвинутых слоёв и проверки того, что каждый из них оправдывает затраты памяти.
Разбор примера «до и после»: анимация left против transform
Анимация left вызывает layout на каждом кадре; анимация transform: translateX() не вызывает ни layout, ни paint. Одно и то же движение выполняется в разных потоках. Вот неправильная версия с анимацией left:
/* Неправильно: анимируется свойство, затрагивающее layout */
.box {
position: absolute;
left: 0;
width: 100px;
height: 100px;
background: tomato;
animation: slide 1s ease-in-out infinite alternate;
}
@keyframes slide {
to {
left: 200px;
}
}
Что показывает каждая панель для этой версии: Rendering мигает зелёным на блоке на протяжении всей анимации, поскольку изменение left вызывает layout, а за layout всегда следует paint. Трек Main в Performance заполнен фиолетовыми полосами Recalculate Style и Layout на каждом кадре, а трек Frames показывает кадры, превышающие бюджет, при включённом ограничении CPU. Свойства left, top, width и height — все вызывают layout (см. CSS Triggers для разбивки по свойствам), а layout выполняется в основном потоке, конкурируя со всем остальным за бюджет в 16,7 мс.
Переработанная версия выражает то же движение только через transform:
/* Правильно: анимируется свойство, работающее только на уровне компоновщика */
.box {
position: absolute;
left: 0;
width: 100px;
height: 100px;
background: tomato;
animation: slide 1s ease-in-out infinite alternate;
}
@keyframes slide {
to {
transform: translateX(200px);
}
}
translateX воспроизводит позиционное изменение через transform. После переработки: Paint flashing не показывает зелёного при движении, трек Main в Performance больше не заполняется фиолетовым на каждом кадре, и анимация выполняется на компоновщике. Компоновщик может анимировать transform и opacity без вызова layout или paint, поэтому браузер перемещает текстуру существующего слоя вместо пересчёта геометрии на каждом кадре.
Список исправлений
Исправление — это замена свойств: замените всё, что вызывает layout или paint, на transform или opacity. Таблица сопоставляет каждое анимационное намерение с его аналогом, работающим только на уровне компоновщика.
| Намерение | Избегать (вызывает layout/paint) | Использовать (только компоновщик) |
|---|---|---|
| Перемещение | left, top, margin | transform: translate() |
| Изменение размера | width, height | transform: scale() |
| Поворот | хаки, затрагивающие layout | transform: rotate() |
| Затухание | переключение visibility, изменение фона | opacity |
Наиболее безопасные и широко поддерживаемые CSS-свойства для анимации — transform (перемещение, масштабирование, поворот, наклон) и opacity, поскольку браузеры, как правило, могут выполнять их на компоновщике без вызова layout или paint. filter также может быть аппаратно ускорен для таких функций, как blur(), однако поддержка и поведение варьируются, поэтому перед тем как считать его «бесплатным», проверьте его в панели Rendering с включённым Paint flashing — документация MDN по filter описывает свойство, а CSS Triggers фиксирует его влияние на рендеринг для каждого движка. Многие другие анимируемые свойства вызывают paint, а свойства, изменяющие размер или положение, как правило, запускают пересчёт layout в основном потоке.
Для анимаций, управляемых JavaScript, группируйте все операции чтения DOM перед всеми операциями записи. Принудительный синхронный layout в рассмотренном примере возникает из-за чтения свойства layout после записи; группировка чтений в начале позволяет браузеру обслуживать их из layout предыдущего кадра вместо принудительного вычисления нового. Руководство по layout thrashing подробно описывает этот паттерн.
Используйте will-change стратегически, а не по умолчанию. Применяйте его к элементу непосредственно перед анимацией и удаляйте по её завершении; согласно MDN, широкое применение этого свойства расходует GPU-память впустую, поскольку браузер уже самостоятельно оптимизирует недорогие свойства. Подтвердите эффект в панели Layers.
Анимации, управляемые прокруткой: другая сигнатура нестабильности
Анимации, управляемые прокруткой и объявленные с помощью animation-timeline: scroll() или animation-timeline: view(), меняют место поиска в DevTools. Когда они анимируют только transform или opacity, они выполняются на компоновщике, поэтому их нестабильность не проявляется как долгая задача в треке Main — вместо этого ищите пропущенные кадры в треке Frames. Документация MDN по animation-timeline и руководство Chrome по анимациям, управляемым прокруткой охватывают эту функцию и базовый уровень поддержки браузерами. Если вы применяете эвристику трека Main и ничего не находите, но трек Frames всё равно показывает кадры, превышающие бюджет, вероятно, в ключевые кадры анимации, управляемой прокруткой, проникло свойство, не поддерживающее компоновку.
Почему анимация подёргивается только в продакшне?
DevTools профилирует в контролируемых условиях; переменная, которую он не может полностью воспроизвести, — это реальный пользовательский контекст: класс CPU устройства, давление памяти, параллельная активность. Когда сообщение о нестабильности не воспроизводится локально, причиной обычно является именно этот недостающий контекст. Запись сессий (session replay) захватывает его, позволяя знать, какие условия нужно симулировать перед записью.
Запускайте четыре панели по порядку — Rendering для подтверждения, Performance для выявления причины, Animations для изоляции, Layers для оценки — и следующая нестабильная анимация перестанет быть загадкой.
Часто задаваемые вопросы
Дело в контексте устройства, а не в коде. Среднебюджетный Android с несколькими открытыми вкладками располагает лишь долей CPU-бюджета ноутбука. Включите ограничение CPU в Capture settings панели Performance и используйте запись сессий, чтобы зафиксировать реальные условия, при которых возникла нестабильность в продакшне.
`transform` выполняется в потоке компоновщика без вызова layout или paint — браузер просто перемещает текстуру существующего слоя на каждом кадре. `left` или `top` принудительно запускает пересчёт layout в основном потоке на каждом кадре, за которым следует paint, конкурируя с JavaScript за бюджет в 16,7 мс.
Не гарантированно. Только `transform` и `opacity` гарантированно работают исключительно на уровне компоновщика. `filter` может быть аппаратно ускорен для таких функций, как `blur()`, в некоторых движках, но поддержка варьируется. Проверьте в панели Rendering с включённым Paint flashing: зелёная вспышка означает перерисовку на каждом кадре.
`animation-timeline: scroll()` и `view()` выполняются на компоновщике, когда анимируют только `transform` или `opacity`, не создавая долгих задач в основном потоке. Нестабильность проявляется в треке Frames. Если Main ничего не показывает, но Frames отображает кадры, превышающие бюджет, вероятно, в ключевые кадры проникло свойство, не поддерживающее компоновку.
Understand every bug
Uncover frustrations, understand bugs and fix slowdowns like never before with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data. Check our GitHub repo and join the thousands of developers in our community.