DarkRiDDeR12 мин

Изображение в первом экране: сначала контракт разметки, геометрии и наблюдения

FrontendПроизводительность

У большого изображения в первом экране обычно три видимых симптома: контент появляется поздно, блок прыгает после ответа или нужный ресурс оказывается в очереди не там, где его ожидали. Цена не сводится к цифре в отчёте: пользователь сначала видит пустое место или неверный порядок страницы, а команда начинает менять атрибуты вслепую. В следующем релизе та же ошибка возвращается другой картинкой.

Причина часто в смешении четырёх фактов. Разметка говорит, какой ресурс описан. Геометрия говорит, какое место компонент резервирует по своему контракту. Load и decode относятся к платформенному пути, а LCP и layout shift появляются только в наблюдении конкретного документа и браузера. Ниже — учебная модель, где эти слова являются отдельными fields. Она не создаёт img, не запускает сеть и не измеряет LCP или CLS.

Сначала назвать, что именно сломано

Матрица симптомов для одного media slot
СимптомНе делаем выводПервый проверяемый фактДействие
Пустое место до изображения«это точно LCP»есть ли разметка и declared geometry для slotзафиксировать dimensions рядом с источником layout
Карточка сдвинулась после медиа«width/height гарантируют нулевой CLS»меняется ли место slot в выбранном DOM-сценариипроверить правила контейнера и резервирование отдельно
Hero приходит поздно«один attribute задаёт реальный network priority»какое намерение команда присвоила resource и где оно выраженосверить разметку, ресурс и browser observation
Декодирование заметно пользователю«load равен готовому пикселю»какой этап реально наблюдался и в каком браузересобирать отдельный trace, не выводить его из unit fixture

Минимальный контракт до оптимизации

Для одного slot достаточно записать четыре независимые вещи: src и alt как declared markup; width/height как declared geometry; намерение critical-candidate или deferred-candidate; и факт внешнего наблюдения. Последнее особенно важно. Даже когда ресурс важен для первого экрана, локальная модель не может узнать, стал ли он candidate LCP: у неё нет document, paint и ввода пользователя.

Это не отказ от оптимизации. Это способ не сделать ложный rollback. Если slot не имеет геометрии, сначала безопасно добавить её в компонент и проверить выбранные варианты изображения. Если ожидание priority не совпало с trace, не надо сразу объявлять другой label «быстрее»: нужно сохранить условия прогона, браузер, viewport, cache state и список конкурирующих ресурсов. Тогда следующий шаг воспроизводим.

# Запускает только учебную in-memory модель Node.js.
node web/scripts/upgrade-2022-08.mjs --verify-fixture

# PASS fixture: 14/14 assertions
# Нет DOM, CSS layout, сети, decode(), PerformanceObserver, LCP или CLS.

Fixture принимает строку teaching-load-ok и затем teaching-decode-ok. Это нарочно не callback браузера. Она проверяет только порядок локального контракта: без markup нельзя plan load, decode не допускается до declared load, а observation записывается после declared decode. Результат PASS не говорит, что файл был загружен, декодирован, показан или стал LCP-кандидатом.

Схема одного media slot: declared markup ведёт к отдельно declared geometry; затем application intent планирует load, учебный token отмечает load и decode, а внешний observation record хранится отдельной веткой. Внизу явно написано, что это не DOM, не CSS layout и не измерение LCP или CLS.
Контракт не заменяет browser trace: он делает явным, какие факты должен принести следующий слой проверки.

Геометрия — часть компонента, а не последняя правка CSS

Резервирование места полезно рассматривать как вход компонента. В модели reserveGeometry(960, 540) сохраняет ratio 960:540; оно не вычисляет box, не знает object-fit и не видит CSS соседей. Поэтому такой unit test может защитить обязательность dimensions в данных, но не вправе утверждать, что layout shift исчез. Для этого нужен отдельный browser-сценарий с реальной разметкой.

У одного источника может быть несколько отображений: hero, карточка, миниатюра. Не переносите размеры оригинала как универсальный ответ. Для каждого отображения команда должна назвать ожидаемую рамку, источник пропорции и поведение при неизвестной высоте. Если это невозможно сформулировать рядом с компонентом, optimisation пока не имеет стабильной границы, а не «нуждается в ещё одном CSS hack».

Очередь ресурса: намерение не равно факту браузера

В contract есть priorityIntent, но это только label решения: critical или deferred candidate. Он помогает reviewer спросить, почему этот ресурс должен конкурировать в ранней части загрузки. Он не обещает, что user agent назначит конкретный приоритет, начнёт скачивание первым или сохранит порядок при другом cache state. Такой запрет на сильный вывод дешевле последующего разбора «почему в лаборатории было иначе».

Для non-critical media разумнее сначала убедиться, что оно действительно не требуется до действия пользователя или ниже первого сценария просмотра. Слово deferred не означает «можно потерять»: у slot остаются alt, geometry, error state и проверка возвращения на экран. Если продукту важен placeholder, он тоже становится отдельной разметкой и геометрией, а не тайным следствием неуспешной загрузки.

Маршрут: симптом → причина → проверка → действие

  1. Симптом. Выберите один slot и один видимый дефект: поздний hero, скачок карточки или ошибочное раннее медиа. Не соединяйте их общим словом «производительность».
  2. Причина. Разложите текущий компонент на markup, geometry, resource intent, load/decode и observation. Отметьте, какое поле отсутствует или меняется не там.
  3. Проверка контракта. Запустите fixture. PASS проверяет только порядок учебных labels и отсутствие metric claim.
  4. Проверка реализации. В выбранном браузере соберите отдельный сценарий: URL, viewport, cache state, список конкурирующих ресурсов и фактический layout. Это уже другой артефакт.
  5. Действие. Добавьте geometry в component API, назовите resource intent и оставьте источник данных для responsive variant.
  6. Откат. Если новый responsive источник показывает неверный кадр или ломает доступность, верните прежнее соответствие resource-to-slot. Не маскируйте проблему искусственным priority label без записи наблюдения.

Что проверяет LCP-документ, а что остаётся за ним

FPWD LCP от 24 мая 2022 года определяет наблюдение кандидатов и прямо называет ограничения эвристики. Это полезная граница: LCP — не синоним «любой большой картинки» и не обещание, что unit-модель может рассчитать пользовательское восприятие. HTML 5.2 фиксирует норму для img, но тоже не описывает продуктовую очередь и не выбирает за команду важный ресурс.

Следующий проверяемый шаг — приложить к PR два артефакта. Первый: unit test данных slot, где отсутствующая или нулевая geometry отклоняется. Второй: короткий browser report с условиями и тем, что было реально замечено. Не заменяйте второй первым. Так изменение остаётся маленьким, но команда не делает вид, что локальный object уже является профилем браузера.

Ограничение

Модель не охватывает srcset selection, CSS background, video poster, CDN negotiation, cache, network error, EXIF orientation, accessibility tree, server rendering и реальные callbacks. Она не утверждает, что width/height сами по себе устраняют CLS, а priority intent — что-либо о scheduler. Для каждой из этих границ потребуется конкретная реализация и самостоятельная проверка.

Проверяемые источники

  • Largest Contentful Paint, W3C First Public Working Draft от 24 мая 2022 года — датированный первичный документ. Он определяет API наблюдения candidate largest contentful paint и отдельно перечисляет его ограничения; fixture ниже этот API не вызывает и LCP не измеряет.
  • HTML 5.2, W3C Recommendation от 14 декабря 2017 года — неизменяемая нормативная версия, доступная в августе 2022 года. Она задаёт элемент img и его атрибуты; проектные labels, операции модели и выводы о метриках в ней не определены.