41 Модуль 9 Качество и производительность 17 мин урок 16 из 25

Почему сайт тормозит: reflow, repaint, composite

Что браузер делает между «получил CSS» и «показал картинку», какие свойства стоят дорого и как это увидеть, а не угадать.

Что ты научишься делать

  • Поймёшь, из каких этапов складывается отрисовка кадра
  • Отличишь дорогие свойства от дешёвых и объяснишь почему
  • Найдёшь причину подтормаживания во вкладке Performance

#Пять этапов одного кадра

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

  1. Style — вычисление стилей

    Для каждого элемента собирается итоговый набор свойств: каскад, наследование, специфичность.

  2. Layout — раскладка

    Считаются размеры и координаты. Самый дорогой этап: изменение одного элемента может сдвинуть половину страницы. Его же называют reflow.

  3. Paint — отрисовка

    Заполняются пиксели: фоны, текст, тени, рамки. Дешевле раскладки, но всё ещё работа процессора.

  4. Composite — сборка слоёв

    Готовые куски складываются в итоговую картинку. Это делает видеокарта, и делает очень быстро.

  5. Показ кадра

    На всё про всё около 16 миллисекунд, если хочешь 60 кадров в секунду.

#Что с какого этапа запускает пересчёт

МеняемНачинается сЦена
width, height, top, left, margin, paddingLayoutДорого: пересчёт раскладки, потом отрисовка и сборка
font-size, display, добавление элементаLayoutДорого
color, background, box-shadow, border-radiusPaintСредне: раскладка не трогается
transform, opacity, filterCompositeДёшево: работа уходит видеокарте

Отсюда правило, которое повторяют в каждой статье про анимацию: анимируй только transform и opacity. Теперь понятно, почему именно их: только они не заставляют браузер пересчитывать раскладку и перерисовывать пиксели.

Интерактивный разбор доступен при включённом JavaScript.

Так не надо

/* Каждый кадр — новая раскладка всей страницы */
@keyframes slide {
  from { left: 0; }
  to   { left: 300px; }
}

.box {
  position: relative;
  animation: slide 1s infinite alternate;
}

Свойство left запускает пересчёт с этапа Layout. Шестьдесят раз в секунду браузер заново считает координаты — и не только этого элемента.

Так надо

/* Раскладка не трогается вовсе */
@keyframes slide {
  from { transform: translateX(0); }
  to   { transform: translateX(300px); }
}

.box {
  animation: slide 1s infinite alternate;
}

Элемент остаётся на месте с точки зрения раскладки — сдвигается только его готовая картинка. Этим занимается видеокарта.

#Layout thrashing

Отдельная беда, которая случается в скриптах. Браузер откладывает пересчёт раскладки до последнего — копит правки и делает всё разом.

Но если после записи сразу прочитать геометрию, браузер обязан посчитать её немедленно. Чередование записи и чтения в цикле заставляет его пересчитывать раскладку на каждой итерации.

Так не надо

<script>
// Читаем и пишем вперемешку — пересчёт на каждой итерации
items.forEach((el) => {
  el.style.width = el.offsetWidth + 10 + 'px';
});
</script>

offsetWidth — чтение, style.width — запись. Сто элементов дают сто принудительных пересчётов раскладки.

Так надо

<script>
// Сначала всё прочитали…
const widths = items.map((el) => el.offsetWidth);

// …потом всё записали
items.forEach((el, i) => {
  el.style.width = widths[i] + 10 + 'px';
});
</script>

Один пересчёт вместо ста. Приём называют «сначала читаем, потом пишем».

#will-change: обещание, а не ускоритель

will-change просит браузер заранее вынести элемент на отдельный слой. Это помогает — но у слоя есть цена: память видеокарты.

Свойство, поставленное «на всякий случай» всем подряд, ухудшает производительность, а не улучшает. На слабых устройствах десяток лишних слоёв заметен сразу.

CSSКак правильно
/* Плохо: слой существует всегда, даже когда ничего не происходит */.card { will-change: transform; }/* Хорошо: обещание даётся прямо перед анимацией */.card { transition: transform .3s var(--ease); }.card-list:hover .card { will-change: transform; }/* Ещё лучше — вообще не трогать: браузер и так создаёт слой   для transform-анимаций, когда это нужно */

#content-visibility и contain

Два свойства, которые говорят браузеру: «сюда можно не заглядывать».

content-visibility: auto пропускает раскладку и отрисовку содержимого, пока блок за пределами экрана. На длинной странице с сотней карточек это самая дешёвая оптимизация из существующих.

contain обещает, что внутренние изменения не выйдут наружу, — браузер перестаёт пересчитывать соседей.

CSSДлинный список
.post {  /* Содержимое не считается, пока блок далеко от экрана */  content-visibility: auto;  /* Обязательно: иначе полоса прокрутки будет прыгать,     ведь высоту невычисленного блока браузер не знает */  contain-intrinsic-size: auto 420px;}

#Как это увидеть

Гадать не нужно. Вкладка Performance в инструментах разработчика записывает всё, что браузер делал в каждом кадре.

  1. Запиши десять секунд

    Открой вкладку Performance, нажми запись и повтори действие, которое тормозит: прокрутку, наведение, открытие меню.

  2. Найди красные полосы

    Красным помечены кадры, не уложившиеся в бюджет. Это и есть подтормаживание, которое человек чувствует.

  3. Посмотри, чем занят кадр

    Фиолетовый — раскладка и стили, зелёный — отрисовка. Много фиолетового означает, что анимируется свойство из первой строки таблицы.

  4. Ищи предупреждения

    Браузер сам помечает принудительные пересчёты раскладки — это подсвеченный Forced reflow с указанием строки в скрипте.

  5. Проверь на слабом устройстве

    В настройках записи есть замедление процессора в четыре-шесть раз. На быстром ноутбуке проблема может быть незаметна вовсе.

#Как это выглядит в реальном проекте

CSSЛипкая шапка и длинный список
@layer components {  /* Шапка прячется при прокрутке вниз.     Двигаем transform, а не top — иначе пересчёт на каждом кадре */  .header {    position: sticky;    inset-block-start: 0;    transition: transform .25s var(--ease);  }  .header.is-hidden { transform: translateY(-100%); }  /* Длинная лента: браузер не считает то, чего не видно */  .feed__item {    content-visibility: auto;    contain-intrinsic-size: auto 380px;  }  /* Карточка: тень появляется через opacity отдельного слоя,     а не через анимацию box-shadow */  .card { position: relative; }  .card::after {    content: "";    position: absolute;    inset: 0;    border-radius: inherit;    box-shadow: 0 12px 30px -18px rgb(20 24 48 / .5);    opacity: 0;    transition: opacity .25s var(--ease);  }  .card:hover::after { opacity: 1; }}

#Когда НЕ надо

  • Оптимизировать без замера. Половина «оптимизаций» из статей не даёт ничего в конкретном проекте, а часть вредит. Сначала запись, потом правка.
  • Ставить will-change везде. Каждый слой занимает память видеокарты. На телефоне это ощутимо.
  • Гнаться за кадрами в статике. Если на странице нет анимаций и прокрутки, разница между этапами не важна вовсе.
  • Заменять читаемый код на быстрый заранее. Сложный приём ради миллисекунды, которую никто не заметит, — плохая сделка.

#Поддержка браузерами

ВозможностьСостояние
Композитные слои для transform и opacityВезде и давно
will-changeВо всех современных браузерах
containВо всех современных браузерах
content-visibilityПоддерживается везде, Safari подтянулся позже остальных
Вкладка PerformanceЕсть во всех настольных браузерах, устройство немного различается

Где спотыкаются почти все

Анимация через <code>top</code> и <code>left</code>

Почему Каждый кадр запускает пересчёт раскладки. На слабом устройстве анимация начинает дёргаться.

Как надо Двигай через transform: translate() — раскладка не трогается вовсе.

Чтение геометрии в цикле после записи

Почему Браузер вынужден считать раскладку немедленно на каждой итерации вместо одного раза в конце.

Как надо Сначала собери все замеры, потом применяй все правки.

<code>will-change</code> на всех карточках

Почему Десятки слоёв в памяти видеокарты. Получается медленнее, чем без него.

Как надо Ставь перед анимацией и снимай после. Чаще всего он вообще не нужен.

<code>content-visibility</code> без <code>contain-intrinsic-size</code>

Почему Высота невычисленных блоков считается нулевой — прокрутка прыгает, якоря промахиваются.

Как надо Указывай примерную высоту блока.

Пересчёт раскладки reflow, layout
Вычисление размеров и координат. Самый дорогой этап отрисовки.
Перерисовка repaint
Заполнение пикселей без пересчёта координат.
Композиция composite
Сборка готовых слоёв в кадр. Выполняется видеокартой.
Пробуксовка раскладки layout thrashing
Чередование записи и чтения геометрии, вынуждающее пересчитывать раскладку многократно.
Композитный слой compositing layer
Отдельная картинка в памяти видеокарты. Ускоряет анимацию, но занимает память.

Проверь себя

  1. Почему <code>transform</code> дешевле, чем <code>left</code>?

    <code>left</code> начинает пересчёт с этапа Layout. <code>transform</code> двигает уже нарисованный слой.

  2. Что такое layout thrashing?

    Браузер откладывает пересчёт, но чтение геометрии заставляет сделать его немедленно. В цикле это даёт пересчёт на каждой итерации.

  3. Что произойдёт, если поставить <code>will-change: transform</code> сотне карточек?

    Обещание браузеру стоит памяти. Ставят точечно, перед анимацией, и снимают после.

  4. Что обязательно добавить к <code>content-visibility: auto</code>?

    Иначе высота невычисленных блоков считается нулевой, и полоса прокрутки прыгает.

  5. С чего начинать работу над производительностью?

    Оптимизация без замера чаще всего тратит время впустую, а иногда делает хуже.

Задание

Починить три причины подтормаживания

В коде три проблемы: анимация двигает left, тень анимируется напрямую, а will-change висит на всех карточках постоянно.

Переведи движение на transform, тень — на прозрачность отдельного слоя через ::after, а will-change либо убери, либо оставь только на время наведения. Разметку не трогай.

Открыть в песочнице
    Подсказка 1

    Движение: transform: translateX(0); в покое и translateX(8px) при наведении, transition по transform.

    Подсказка 2

    Тень: .card::after { content: ""; position: absolute; inset: 0; border-radius: inherit; box-shadow: …; opacity: 0; transition: opacity .3s ease; } и .card:hover::after { opacity: 1; }

    Подсказка 3

    will-change убери из .card совсем или перенеси в .list:hover .card — тогда слои живут только во время взаимодействия.

    Показать решение
    HTMLindex.html
    <div class="list">  <article class="card">Первая</article>  <article class="card">Вторая</article>  <article class="card">Третья</article></div>
    CSSstyle.css
    .list { display: grid; gap: 12px; }.card {  position: relative;  padding: 16px;  background: #f4f4f8;  border-radius: 12px;  /* Двигаем готовый слой — раскладка не трогается */  transform: translateX(0);  transition: transform .3s ease;}/* Тень живёт отдельным слоем и появляется через прозрачность */.card::after {  content: "";  position: absolute;  inset: 0;  border-radius: inherit;  box-shadow: 0 12px 30px -18px rgb(20 24 48 / .5);  opacity: 0;  transition: opacity .3s ease;  pointer-events: none;}/* Обещание браузеру — только на время взаимодействия */.list:hover .card { will-change: transform; }.card:hover { transform: translateX(8px); }.card:hover::after { opacity: 1; }

    Проверь себя