CSS-анимации без дёрганий: почему падает FPS и как чинить
Анимация меняет left и margin — и на скролле начинаются рывки, а на мобильном браузер съедает батарею. Разбираем, почему это происходит на уровне рендер-конвейера и как удержать 60 fps.

Карточка выезжает из-за края экрана, меняя свойство left с −300px до 0. На десктопе выглядит нормально, но на среднем Android панель дёргается, а профайлер в DevTools показывает жёлто-фиолетовые полосы Layout и Paint на каждом кадре. Проблема не в мощности телефона — проблема в том, что каждый шаг анимации заставляет браузер пересчитывать геометрию всей страницы. Ниже — как устроен рендер-конвейер браузера, какие свойства анимировать дёшево, а какие дорого, и как измерить разницу вместо того, чтобы гадать.
Что происходит с кадром внутри браузера
Каждый кадр анимации браузер прогоняет через конвейер. В движках на Blink (Chrome, Edge, Opera) и WebKit (Safari) этапы называются так:
- Style — вычисление итоговых стилей для элементов.
- Layout (Reflow) — расчёт размеров и позиций: где что лежит на странице.
- Paint — заливка пикселей: цвета, тени, границы, текст в отдельные слои-растры.
- Composite — сборка готовых слоёв в итоговую картинку, часто силами GPU.
Ключевая мысль: чем раньше в этой цепочке вклинивается ваша анимация, тем дороже кадр. Меняете свойство, влияющее на Layout, — браузер вынужден пройти все четыре этапа. Меняете только то, что затрагивает Composite, — три этапа пропускаются.
60 кадров в секунду означают бюджет примерно 16,6 мс на кадр. Из них часть уходит на служебные операции браузера, так что на вашу работу остаётся около 10 мс. Один принудительный Layout по всему дереву легко съедает этот бюджет целиком.
Дешёвые и дорогие свойства
Свойства делятся по тому, какой этап конвейера они запускают.
| Свойство | Что запускает | Стоимость |
|---|---|---|
transform | Composite | дёшево |
opacity | Composite | дёшево |
color, background-color | Paint + Composite | средне |
box-shadow, border-radius | Paint + Composite | средне-дорого |
width, height, margin, padding | Layout + Paint + Composite | дорого |
top, left, right, bottom | Layout + Paint + Composite | дорого |
Отсюда практическое правило: анимируйте transform и opacity. Почти любое движение выражается через transform: translate(), масштаб — через scale(), а появление — через opacity. Список свойств, которые какой этап запускают, сверяйте на csstriggers.com — он показывает данные отдельно для Blink и WebKit.
Переписываем дорогую анимацию
Вернёмся к карточке. Исходный вариант дёргается:
/* Дорого: каждый кадр — новый Layout всей строки */
.card {
position: relative;
left: -300px;
transition: left 0.3s ease-out;
}
.card.is-open {
left: 0;
}
Переписываем на transform. Визуально то же смещение, но браузер обрабатывает его на этапе Composite и не трогает геометрию соседей:
/* Дёшево: только Composite */
.card {
transform: translateX(-300px);
transition: transform 0.3s ease-out;
}
.card.is-open {
transform: translateX(0);
}
После этой замены полосы Layout в профайлере исчезают, а на кадре остаётся только строка Composite. На слабом мобильном это разница между заметным подёргиванием и гладким движением.
will-change: подсказка, а не переключатель
Свойство will-change заранее сообщает браузеру, что элемент будет меняться, и тот может вынести его в отдельный слой заблаговременно:
.card {
transform: translateX(-300px);
transition: transform 0.3s ease-out;
will-change: transform;
}
Но это не бесплатная кнопка «сделать быстро». Каждый слой занимает видеопамять. Если навесить will-change на сотню элементов списка, вы получите обратный эффект — браузер задохнётся от количества слоёв. Ставьте will-change точечно и, если движение разовое, снимайте его после окончания анимации через JS. Держать will-change постоянно на элементе, который анимируется раз в минуту, — трата памяти.
Что ещё роняет плавность
- Анимация на большом количестве элементов сразу. Даже дешёвый
transformна пятистах карточках одновременно перегрузит композитор. Анимируйте видимую часть, остальное — по мере появления в вьюпорте. - Тяжёлый Paint под движущимся элементом. Если под анимируемым слоем лежит крупная область со сложными тенями и градиентами, браузер может перерисовывать её. Здесь помогает
contain: paintили вынос анимируемого элемента в собственный слой. - Синхронный доступ к геометрии из JS. Чтение
offsetWidth,getBoundingClientRect()илиscrollTopсразу после записи стилей вынуждает браузер сделать Layout прямо сейчас — это называется forced synchronous layout, или «layout thrashing». Сгруппируйте все чтения, потом все записи. - Анимация через
setIntervalвместоrequestAnimationFrame. Интервал не синхронизирован с частотой обновления экрана и рисует кадры невпопад. Для JS-анимаций используйтеrequestAnimationFrame, а лучше — CSS-переходы, которые браузер оптимизирует сам.
Уважайте prefers-reduced-motion
Часть пользователей отключает анимации на уровне системы — из-за вестибулярных нарушений или просто по желанию. Браузер отдаёт это через медиазапрос. Игнорировать его — не только вопрос этики, но и повод к жалобам на «мельтешение».
@media (prefers-reduced-motion: reduce) {
.card {
transition: none;
}
}
Как измерять, а не угадывать
Плавность — это не «на глаз кажется норм». Её видно в инструментах (актуально для Chrome/Edge на движке Blink версии 2024–2025 годов):
- Панель Performance в DevTools. Запишите анимацию, разверните дорожку Main. Оранжевые блоки Layout и зелёные Paint на каждом кадре — сигнал, что вы анимируете дорогое свойство.
- Rendering → Frame Rendering Stats (в меню More tools). Показывает текущий FPS поверх страницы во время взаимодействия.
- Rendering → Paint flashing. Подсвечивает зелёным области, которые браузер перерисовывает. Если при движении карточки мигает пол-экрана — ищите лишний Paint.
- Layer borders в том же меню Rendering показывает границы композитных слоёв: удобно проверить, что элемент действительно вынесен в свой слой.
Мерьте на реальном мобильном или через троттлинг CPU (в панели Performance есть множитель замедления 4x/6x). Десктоп прощает то, за что телефон наказывает рывками.
Что делать дальше
Начните с ревизии существующих анимаций: откройте Performance, запишите каждую и найдите те, где мелькает Layout. Переведите их на transform/opacity — это закрывает большинство проблем с плавностью. Дальше стоит посмотреть в сторону Web Animations API, если анимациями нужно управлять из JS сложнее, чем позволяет CSS-переход. Отдельная большая тема — анимация свойств, которые раньше не анимировались (например, переходы между display: none и видимым состоянием через @starting-style): её оставим на следующий разбор.
Логотип и фирменный стиль: Три концепции разного характера, палитра с обоснованием, проверка на читаемость в малом размере и в один цвет.
Спросить за 60 ₽Источники: MDN: CSS performance optimization, CSS Triggers — какие свойства запускают Layout, Paint, Composite
Частые вопросы
А если я всё перевёл на transform, но на мобильном всё равно дёргается?
will-change ускоряет всё подряд — почему не поставить на все элементы?
Чем transform: translateX отличается от смены left по результату?
requestAnimationFrame или CSS-transition — что выбрать?
Работают ли эти правила во всех браузерах одинаково?
Не сломает ли prefers-reduced-motion мой интерфейс, если анимация несёт смысл?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.