Back

Отладка нестабильных CSS-анимаций с помощью DevTools

Отладка нестабильных 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 помогают диагностировать нестабильность анимации?

Систематическая диагностика нестабильности анимации включает четыре панели в следующем порядке: 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 обводит слои компоновщика оранжевым цветом. Используйте его, чтобы убедиться, что элемент, который должен быть аппаратно ускорен, действительно получил собственный слой, — и чтобы обнаружить непреднамеренно созданные слои.

Порядок действий:

  1. Откройте панель Rendering.
  2. Включите Paint flashing и запустите анимацию.
  3. Если анимируемый элемент мигает зелёным при движении, анимация перерисовывается на каждом кадре — анимируется свойство, затрагивающее layout или paint. Это указывает на проблему уровня свойств и означает, что следующим шагом нужно открыть панель Performance.
  4. Если вспышек нет, но движение всё равно подёргивается, узкое место, скорее всего, находится в 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.

Порядок действий:

  1. Откройте панель Performance и включите Screenshots.
  2. В Capture settings (значок шестерёнки) установите ограничение CPU.
  3. Нажмите Record, запустите анимацию на несколько секунд, затем нажмите Stop.
  4. Сначала просмотрите трек FPS/Frames. Красные метки над кадрами указывают на кадры, превысившие бюджет.
  5. Приблизьте проблемный кадр и изучите трек 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 — самый быстрый способ определить, какой именно ключевой кадр вызывает перерисовку: зелёная вспышка появляется в точный момент, когда вступает в силу значение проблемного свойства.

Порядок действий:

  1. Откройте панель Animations и запустите анимацию, чтобы она появилась в списке.
  2. Установите скорость воспроизведения 10% (элементы управления находятся в верхней части панели).
  3. С включённым Paint flashing перемотайте временну́ю шкалу и следите за зелёной вспышкой.
  4. Если зелёная вспышка появляется в определённой точке временно́й шкалы, сосредоточьте расследование на ключевом кадре, активном в этот момент.

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, margintransform: translate()
Изменение размераwidth, heighttransform: scale()
Поворотхаки, затрагивающие layouttransform: 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.

OpenReplay