После добавления оптимизации медиа часто возникает странный спор: один разработчик показывает unit test, другой — trace, а оба называют это «готовым LCP fix». Проблема такой подмены в том, что решение нельзя ни подтвердить, ни откатить: не ясно, что именно изменилось — markup, размеры, очередь, декодирование или условия запуска. При следующем изменении команда сравнит несравнимые результаты. Цена ошибки — потратить время на «ускорение», которое не меняет путь первого экрана.
Причина в том, что у этих фактов разные владельцы. Компонент владеет описанием slot и может требовать geometry. Приложение может назначить намерение важности. Браузер выполняет загрузку, decode и layout; измерительный код наблюдает его результат при конкретных условиях. Учебный контракт ниже не притворяется браузером: он хранит declared transitions и принимает внешний record только как supplied data.
Пять слоёв вместо одного флага loaded
| Слой | Владелец | Наблюдаемый факт | Чего факт не доказывает |
|---|---|---|---|
| Markup | компонент | slot описан src и alt | ресурс запрошен или показан |
| Geometry | component API и layout contract | есть положительные width и height | реальный box и отсутствие shift |
| Load intent | приложение | slot помечен critical/deferred candidate | приоритет scheduler и время ответа |
| Declared load/decode | учебная fixture | локальный порядок transitions | сетевой load или decode API |
| External observation | отдельный browser прогон | в record есть значение и условия | причину изменения без анализа trace |
Почему load и decode надо разделить даже в учебном коде
Слово loaded удобно, но оно склеивает минимум два вопроса: ресурс уже доступен по пути загрузки и он готов для отображения в конкретном контексте. Настоящие платформенные алгоритмы сложнее этой заметки. Поэтому model не пытается их воспроизвести: markLoad принимает только teaching token, а markDecode требует ранее объявленный load. Их разделение полезно не для симуляции браузера, а чтобы review увидел неверный порядок переходов.
В реальном коде граница будет другой: события, Promise или framework adapter. Нельзя заменить их вызовом fixture и сказать, что браузер прошёл decode. Но можно заранее договориться, что логика UI не покажет «готово» только потому, что создан descriptor. Это небольшой, проверяемый контракт: описание slot не равно завершённой работе платформы.
# Запускает только учебную 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.
Геометрия: проверяем вход, не рисуем несуществующий layout
Функция reserveGeometry допускает только положительные целые width и height. Это намеренно узкое правило. Оно помогает найти данные, где component API вообще не знает ожидаемый ratio. Но функция не вычисляет CSS box, не учитывает aspect-ratio, grid, container query и не знает, чем закончится рендер. Поэтому assertion называется geometryReserved, а не noLayoutShift.
Такое именование снижает риск плохой аналитики. Если тест говорит «CLS fixed», его потом используют как аргумент против реального наблюдения. Если тест говорит «в contract есть declared geometry», он остаётся честным: дальше можно провести page-level проверку. В архитектуре это полезнее красивого флага, потому что граница ответственности указана прямо в имени.
Наблюдение — вход другого контура
В модели recordExternalObservation принимает record с kind и числом. Она не создаёт PerformanceObserver и не называет число LCP. Его source явно равен external-observation-supplied-by-caller. Это заставляет следующий слой указать происхождение: какой браузер, какой URL, какие условия кэша, какой viewport и какой trace или запись лежит рядом. Без этих условий число остаётся неподписанным сигналом.
FPWD LCP полезен именно этим различием: API наблюдает кандидатов в документе, а не принимает обещание компонента. В документе есть input, viewport, paint и условия, которых нет в plain JavaScript object. Поэтому даже правильная component schema не равна измерению. Она лишь делает измерение проще интерпретировать: reviewer понимает, какой slot был объявлен critical и какая geometry ожидалась.
Неверный priority: как искать, не придумывая scheduler
Когда команда говорит «картинка грузится не с тем приоритетом», сначала нужно заменить оценку на вопрос. Какая разметка описывает ресурс? Почему продукт считает slot ранним или отложенным? Какие другие ресурсы конкурируют в конкретном прогоне? Что зафиксировано в trace? Три первых вопроса относятся к коду и contract, четвёртый — к браузерному наблюдению. Ни один label в object не является ответом на все четыре.
Если наблюдение расходится с intent, rollback должен быть обратимым. Верните прежний mapping resource-to-slot или прежнюю стратегию выбора variant, сохраните report и сформулируйте новую гипотезу. Не меняйте одновременно source, CSS, CDN и priority hint: тогда следующий trace не позволит установить причину. Один осмысленный эксперимент важнее серии атрибутов, добавленных в надежде на быстрый эффект.
Маршрут: симптом → причина → проверка → действие
- Симптом. Зафиксируйте один конфликт: slot объявлен ранним, но в browser report ресурс виден иначе; либо geometry есть в данных, но страница меняет место.
- Причина. Проверьте, не заменены ли markup, geometry, load и observation флагом loaded или одной «performance» метрикой.
- Проверка модели. Убедитесь, что переход decode до load отклоняется, а readiness никогда не утверждает real metric measured.
- Проверка браузера. Отдельно сохраните среду запуска и trace. Укажите, какой вывод сделан из наблюдения, а какой остаётся гипотезой.
- Действие. Назовите owner каждого слоя: component API, page composition, platform adapter, performance observer/report.
- Откат. Откатывайте ровно один mapping или attribute-намерение, затем повторяйте тот же контролируемый сценарий. Не откатывайте «до хорошей цифры» без условия сравнения.
Что можно использовать в production после модели
Из fixture в production переносится не token и не число record. Переносится дисциплина: source media, geometry и intent должны иметь явную точку владения; browser observation должен быть подписан условиями; assertion должен называться ровно по тому факту, который проверяет. Это помогает и без Performance API: code review сразу видит, где component API допускает неизвестную высоту или где deferred slot неожиданно участвует в первом сценарии.
HTML 5.2 описывает img, но не создаёт за продукт policy для всех вариантов изображений. LCP FPWD 2022 года — Working Draft, а не обещание одинакового результата для каждого документа. Эти статусы важнее рекламного языка: архитектурная заметка должна оставить место для браузерной разницы, версий и измерения, а не выдавать учебную схему за finished optimisation.
Ограничение и следующий шаг
Контракт не покрывает lazy loading, preload, responsive selection, background-image, animated formats, decoding errors, cross-origin timing, caching и ограничения устройств. Он также не строит accessibility tree и не проверяет alt в screen reader. Добавлять любое из этих свойств стоит только как новый layer со своим input и отрицательными assertions.
Следующий шаг — выбрать один page-level media slot и оформить короткий evidence packet: скрин разметки/данных, ожидаемая geometry, rationale intent и протокол browser observation. Если после этого нужна правка, сделайте её одной: например, исправьте geometry contract или выбор source для этого slot. Затем повторите именно описанный сценарий. Так у оптимизации будет причина, проверка и безопасный путь назад.
Проверяемые источники
- 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, операции модели и выводы о метриках в ней не определены.