Суть Прочитай четыре сниппета браузера — порядок microtask, layout thrash, hydration mismatch и блокирующий обработчик — и выбери поведение или фикс с наибольшим рычагом.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Trace говорит, куда ушло время; код говорит, почему. Читай каждый сниппет так, как читал бы на ревью медленной страницы, и выбирай поведение или фикс, к которому senior-инженер тянется первым.
Цель
Потренируйся отображать код обратно на слой, который он нагружает: порядок очередей event loop, связку layout/paint в render pipeline, детерминизм гидратации и бюджет INP на main thread.
Heads-up Синхронны только A и E. setTimeout и промис планируют callback'и на потом; они не могут выполниться до E, который ещё в текущем синхронном прогоне.
Heads-up Таймер на 0 мс всё равно macrotask. Очередь microtask (C, D) всегда осушается полностью до того, как event loop возьмёт следующий macrotask (B).
Heads-up И C, и D — microtask в одной очереди; они выполняются в порядке постановки. Промисный .then (C) был поставлен до queueMicrotask (D).
Сниппет 2 — цикл ресайза
function fitAll(boxes) { for (const box of boxes) { const w = box.offsetWidth; // READ — forces layout box.style.width = w + 10 + "px"; // WRITE — invalidates layout }}
Викторина
Completed
При 500 боксах этот обработчик — длинная задача, разрушающая INP. В чём дефект и фикс?
Heads-up getBoundingClientRect тоже форсирует layout — смена API не помогает. Дефект — это чередование чтений и записей, которое форсирует reflow на итерацию независимо от того, какой read-API используется.
Heads-up Здесь почти нет аллокаций; стоимость — это повторный forced layout, а не сборка мусора. Trace показывает полосы recalculate-style/layout, а не GC.
Heads-up Класс всё равно инвалидирует layout и, прочитанный в том же цикле, всё равно вызовет thrash. Фикс — разделить фазу чтения и фазу записи, а не механизм стилизации.
Сниппет 3 — рендер на сервере/клиенте
function Price({ cents }) { // runs on both server and client during hydration const formatted = new Intl.NumberFormat(navigator.language, { style: "currency", currency: "USD", }).format(cents / 100); return <span id="price">{formatted}</span>;}
Викторина
Completed
Этот компонент периодически мигает ценой и поднимает CLS при гидратации. В чём межслойный баг?
Heads-up Intl здесь достаточно быстр; сбой — это недетерминизм, а не стоимость. Сервер и клиент производят разные строки, что и триггерит ре-рендер mismatch и сдвиг.
Heads-up Дубликат id невалиден, но не он мигает значением. Мигание — это React, примиряющий рассинхрон текста сервер/клиент: баг детерминизма, а не id.
Heads-up Деление float — не причина периодического мигания при гидратации. Периодичность отслеживает различие локали между сервером и клиентом — проблема детерминизма рендера.
Сниппет 4 — обработчик клика
button.addEventListener("click", () => { cart.add(item); renderCartBadge(); // small DOM update const report = JSON.parse(hugeBlob); // ~120 ms on a mid-range phone analytics.sendSync(report); // synchronous beacon});
Викторина
Completed
Badge обновляется, но INP этого клика ~180 мс даже на свободном потоке. Какой фикс уважает бюджет INP?
Heads-up rAF планирует callback перед следующим paint, так что тяжёлая работа всё равно выполнится до этого paint и всё равно засчитается в INP. Уступать надо ПОСЛЕ визуального обновления, а не просто отложить всё на один кадр.
Heads-up Перестановка не убирает parse ~120 мс и sync beacon из участка перед paint. Badge обновился бы позже, а не раньше — INP был бы таким же или хуже.
Heads-up 180 мс в пределах бюджета, но целиком вызван избегаемой синхронной работой до paint. Уступка после обновления UI и вынос parse режут его до стоимости обновления badge.
Итог
Четыре сниппета, четыре слоя. Event loop осушает все microtask (промис, queueMicrotask) до следующего macrotask (setTimeout), поэтому порядок — синхронное, затем microtask, затем macrotask. Чередование чтений и записей DOM форсирует reflow на итерацию — сначала чтения, потом записи. Компонент, рендерящий локаль- или браузер-зависимое значение на сервере, — это hydration mismatch: сделай серверный рендер детерминированным, а клиент-only форматирование отложи в useEffect. И любая синхронная тяжёлая работа в обработчике до paint засчитывается в INP — обнови UI минимально, затем уступи. Навык — читать код прямо обратно на трек, который он нагружает.