Суть Читай реальные сниппеты манипуляции DOM, прогнозируй, какие стадии pipeline перевыполняет каждый, и выбирай фикс с максимальным рычагом до обращения к compositor.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Баги рендеринга диагностируются в коде обработчика и в полосе DevTools, а не в абстракции. Прочитай каждый сниппет, спрогнозируй, какие стадии перевыполняются, и выбери фикс, который senior-инженер делает первым.
Цель
Отработай цикл, который ты запускаешь в каждом инциденте джанка: прочитай горячий путь, спрогнозируй, где происходит flush layout, и тянись к структурному фиксу — сгруппировать чтения, использовать transform — прежде чем трогать что-либо ещё.
Сниппет 1 — цикл изменения размера
function applyRowWidths(rows, padding) { for (const row of rows) { const w = row.offsetWidth; // read: forces a layout flush row.style.width = (w + padding) + 'px'; // write: marks layout dirty }}
Викторина
Completed
С 5 000 строк это блокирует main thread на секунды. Что происходит и какой один фикс даёт максимальный рычаг?
Heads-up Аллокация здесь не узкое место. Стоимость — N синхронных flush layout от чередования чтений и записей; группировка чтений перед записями роняет это до одного layout.
Heads-up rAF группирует вызов, а не инвалидацию. Чтение после записи внутри колбэка всё равно форсирует синхронный flush. Нужно разделить чтения и записи.
Heads-up scaleX избежал бы layout при записи, но чтение offsetWidth всё равно форсирует flush на каждой итерации. Структурная проблема — чередование read/write, а не свойство.
Сниппет 2 — два кандидата на анимацию
/* A */ .card.move-a { top: 200px; transition: top 300ms; }/* B */ .card.move-b { transform: translateY(200px); transition: transform 300ms; }
Викторина
Completed
Оба анимируют карточку вниз на 200px за 300ms. Какие стадии перевыполняются покадрово для A против B, и какой ты отправишь в прод?
Heads-up transition лишь описывает интерполяцию. top всё ещё flow-affecting, поэтому A перевыполняет layout каждый кадр; только transform/opacity остаются на compositor.
Heads-up Простота свойства нерелевантна. top инвалидирует layout на main thread; transform на promoted layer полностью пропускает layout и paint.
Heads-up transform двигает уже отрисованный битмап на compositor; содержимое не меняется, поэтому перерисовки нет. Именно поэтому B дёшев.
Сниппет 3 — чередование в двух хелперах
function expand(items) { items.forEach((el) => { const top = el.offsetTop; // read el.style.height = top / 2 + 'px'; // write const h = el.getBoundingClientRect().height; // read again — flush! el.style.marginTop = h / 4 + 'px'; // write again });}
Викторина
Completed
DevTools логирует предупреждение Forced reflow while executing JavaScript, указывающее на эту функцию. Сколько forced layout на элемент и каков фикс?
Heads-up Чтения не коалесцируются через запись. Запись между двумя чтениями снова делает layout dirty, поэтому второе чтение форсирует второй flush — два на элемент.
Heads-up getBoundingClientRect возвращает живую геометрию и форсирует flush отложенных записей. Это одно из канонических чтений, триггерящих layout.
Heads-up Конструкция цикла нерелевантна. Reflow берётся из чтения геометрии после записи стилей; фикс — разделение чтений и записей, а не стиль итерации.
Сниппет 4 — реакция на изменение размера
const box = document.querySelector('.panel');box.addEventListener('transitionend', () => { requestAnimationFrame(() => { const w = box.offsetWidth; // inside rAF, before style/layout step sibling.style.width = w + 'px'; });});
Викторина
Completed
Разработчик использовал rAF, ожидая, что чтение offsetWidth будет бесплатным. Что на самом деле происходит и какой API — правильный инструмент, чтобы прочитать новый размер без forced flush?
Heads-up rAF выполняется до style/layout в кадре; чтение геометрии там всё равно форсирует flush. То, что чтение внутри rAF, не делает его бесплатным.
Heads-up IntersectionObserver срабатывает асинхронно между кадрами и сообщает о пересечении, а не о размере элемента. Это не инструмент для чтения размера в том же кадре; для этого ResizeObserver.
Heads-up MutationObserver срабатывает на мутации DOM, а не после layout. Чтение геометрии в его колбэке само может форсировать flush. ResizeObserver — это хук после layout.
Итог
Каждый инцидент рендеринга читается в коде: чередующиеся чтения геометрии и записи стилей форсируют один flush layout на чтение, так что N строк стоят N layout — сгруппируй все чтения перед всеми записями. top анимируется на main thread, transform на compositor, поэтому для движения предпочитай transform. rAF выполняется до шага style/layout, так что чтения там всё равно делают flush; ResizeObserver — хук после layout и до paint для обновлений, реагирующих на размер. Спрогнозируй стадию из свойства и порядка чтения, исправь структурно, затем подтверди в полосе DevTools.