Суть Читай реальную разметку, обработчик клика и строку лога web-vitals, предсказывай, какая Core Web Vital ломается, и выбирай фикс с наибольшим рычагом.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 14 min
Проблемы vitals диагностируют в разметке, обработчиках и RUM-логе — не в определении. Читай каждый сниппет, предсказывай, какая vital ломается и почему, и выбирай фикс, который senior-инженер делает первым.
Цель
Отработай цикл, который ты гоняешь на каждой регрессии vitals: читай код или атрибутированную строку лога, предсказывай, по какой метрике она бьёт, называй доминирующую фазу или часть и хватайся за фикс, который бьёт по ней — а не по удобной.
Сниппет 1 — hero без размеров
<!-- hero, LCP-элемент, вставлен после текста статьи --><section class="article"> <p>Параграф один статьи…</p> <img src="/hero.avif" class="hero" alt="Product hero" /> <p>Параграф два…</p></section>
Что здесь ломается, по какой vital бьёт и каков фикс?
Heads-up width: 100% задаёт ширину рендера, но не даёт высоту или intrinsic ratio, поэтому блок нулевой высоты, пока картинка не загрузится. Именно HTML-атрибуты width/height дают браузеру ratio, чтобы зарезервировать место.
Heads-up AVIF — современный эффективный формат; декод — не проблема. Дефект — отсутствующие размеры, вызывающие layout shift (CLS), а не load time у LCP.
Heads-up Статичная картинка не блокирует основной поток для взаимодействий. Проблема — незарезервированное место, вызывающее сдвиг CLS, когда картинка наконец раскладывается.
Размеры корректны, поэтому CLS в порядке — но LCP регрессировал после добавления этого атрибута. Какой атрибут и почему?
Heads-up Размеры предотвращают CLS и не добавляют стоимости LCP — они дают браузеру зарезервировать место. Регрессия — это loading='lazy', откладывающий старт скачивания hero.
Heads-up Слишком большой источник вредит load TIME, но описанная регрессия появилась при добавлении атрибута — это указывает на load DELAY от loading='lazy'. Чинить можно оба, но lazy на hero — непосредственная причина.
Heads-up Ленивая загрузка ничего не делает со скоростью рендера; она откладывает старт загрузки. На LCP-элементе это напрямую задерживает метрику.
INP на этой кнопке ~210 мс, хотя поток простаивает в момент тапа. Какая часть INP доминирует и каков фикс с наибольшим рычагом?
Heads-up В условии сказано, что поток простаивает в момент тапа, поэтому input delay около нуля. Стоимость 180 мс внутри обработчика — processing time — а не задержка в очереди.
Heads-up Перерисовка вносит вклад в presentation delay, но доминирующие 180 мс — это синхронный filter в обработчике (processing time). Сначала убери его с критического пути.
Heads-up Хороший INP ≤200 мс на p75; 210 мс выше порога. И синхронный обработчик на 180 мс — явная починимая проблема processing time.
Читая эту атрибутированную строку RUM, какой фикс правилен и чего делать НЕ надо?
Heads-up TTFB — маленький кусок рядом с loadTime в 3510 мс. Разбиение по фазам решает — чини доминирующую фазу, а это resource load time.
Heads-up Оба прочно в 'good' (INP ≤200 мс, CLS ≤0.1). Единственная плохая метрика здесь — LCP, которую гонит load time картинки.
Heads-up Preload бьёт по обнаружению/load delay, а не по renderDelay — и loadDelay уже 90 мс. Доминирующая фаза — loadTime; вместо этого уменьши картинку.
Итог
Каждую регрессию vitals читают в коде или атрибутированном логе: img без width/height — это сдвиг CLS; loading=‘lazy’ на hero — самонанесённая регрессия load delay у LCP; тяжёлый синхронный обработчик — это processing time у INP, который ты убираешь с критического пути через startTransition или scheduler.yield; а разбиение по фазам в строке web-vitals говорит, какую фазу LCP чинить и какой фикс был бы впустую. Сначала читай атрибуцию, чини доминирующую фазу или часть, затем перемеряй для подтверждения.