8 lines
19 KiB
JSON
8 lines
19 KiB
JSON
{
|
||
"index": 194,
|
||
"slug": "editorial-2022-08-mechanism-media-performance",
|
||
"title": "Медиа без ложных метрик: как отделить контракт компонента от наблюдения браузера",
|
||
"excerpt": "Изображение может иметь правильный размер в данных и всё равно поздно попасть на экран. Разбираем слои media slot, границы LCP-наблюдения, учебный контракт и обратимую проверку.",
|
||
"contentHtml": "<p>Карточка товара занимает большую часть трафика, хотя изображение на экране выглядит маленьким. В другом случае hero получает width и height, но первый экран всё равно меняет геометрию. Команда видит слово <code>loaded</code>, добавляет preload или меняет priority и ждёт улучшения. Через неделю никто не может сказать, что именно изменило результат: источник, разметка, CSS, очередь загрузки или условия замера. Цена ошибки — лишний трафик, поздний главный контент и релиз, который сложнее откатить, чем проверить.</p>\n<p>Тезис статьи простой: контракт media slot и наблюдение браузера — разные факты. Компонент может объявить источник, альтернативный текст и ожидаемую геометрию. Страница может выразить намерение загрузить slot раньше или позже. Браузер сам решает, когда получить ресурс, декодировать его, разместить и нарисовать. Только отдельный прогон страницы показывает, что произошло в конкретной среде. Если склеить эти слои одним флагом, локальная проверка начнёт выдавать обещание о поведении браузера.</p>\n<h2>Механизм: пять слоёв одного media slot</h2>\n<p>Начните с одного slot, например <code>product-card/main-image</code>. У него есть пять независимых вопросов. Какой ресурс описывает разметка? Какую рамку обещают данные? Считает ли приложение slot важным для первого экрана? На каком этапе локальная модель разрешает переход? Что реально увидел браузер при заданных URL, viewport и состоянии кэша?</p>\n<div class=\"table-scroll\"><table><caption>Слои контракта и граница доказательства</caption><thead><tr><th scope=\"col\">Слой</th><th scope=\"col\">Владелец</th><th scope=\"col\">Проверяемый факт</th><th scope=\"col\">Чего он не доказывает</th></tr></thead><tbody><tr><td>Markup</td><td>Компонент</td><td>Есть <code>src</code> и <code>alt</code> для slot</td><td>Ресурс уже запрошен или нарисован</td></tr><tr><td>Geometry</td><td>Данные и layout contract</td><td>Ширина и высота положительны</td><td>Реальный CSS box и отсутствие сдвига</td></tr><tr><td>Load intent</td><td>Композиция страницы</td><td>Slot помечен как critical или deferred candidate</td><td>Фактический приоритет scheduler</td></tr><tr><td>Declared transition</td><td>Учебная модель</td><td>Локальный порядок load → decode</td><td>Сетевой ответ и API декодирования изображения</td></tr><tr><td>External observation</td><td>Браузерный прогон</td><td>Есть запись с условиями и значением</td><td>Причина результата без анализа страницы и trace</td></tr></tbody></table></div>\n<p>Таблица нужна не для усложнения названий. Она не даёт assertion одного слоя использовать как доказательство другого. Положительные dimensions защищают вход компонента. Они не равны отсутствию CLS. Label <code>critical-candidate</code> фиксирует решение приложения. Он не сообщает, какой ресурс браузер выбрал первым. Запись LCP принадлежит наблюдению страницы, а не объекту из unit-теста.</p>\n<h2>Почему loaded недостаточно</h2>\n<p>Слово <code>loaded</code> часто скрывает несколько переходов. Ресурс можно объявить кандидатом на загрузку. Затем можно получить ответ. После ответа изображение нужно декодировать. Только после этого браузер использует результат в отрисовке, а наблюдатель может сохранить запись. Эти этапы не обязаны завершаться одновременно.</p>\n<p>В учебной модели полезно разделить их явно. Она не создаёт DOM, не отправляет запрос, не вызывает <code>PerformanceObserver</code> и не измеряет LCP или CLS. Она проверяет только порядок переходов. Это безопасный пример: он объясняет инвариант и не выдаёт себя за профиль браузера.</p>\n<pre><code>const media = {\n phase: 'planned',\n intent: 'critical-candidate',\n geometry: { width: 960, height: 540 },\n};\n\nfunction markLoad(state, token) {\n if (state.phase !== 'planned' || token !== 'teaching-load-ok') {\n return { ...state, event: 'load-rejected' };\n }\n return { ...state, phase: 'declared-loaded', event: 'load-declared' };\n}\n\nfunction markDecode(state, token) {\n if (state.phase !== 'declared-loaded' || token !== 'teaching-decode-ok') {\n return { ...state, event: 'decode-rejected' };\n }\n return { ...state, phase: 'declared-decoded', event: 'decode-declared' };\n}\n\nconst loaded = markLoad(media, 'teaching-load-ok');\nconst decoded = markDecode(loaded, 'teaching-decode-ok');</code></pre>\n<p>Имена <code>teaching-load-ok</code> и <code>teaching-decode-ok</code> намеренно говорят об ограничении. Это токены локальной модели. В реальном коде их заменят события, Promise или адаптер платформы. Нельзя вызвать эту функцию вместо загрузки картинки и затем утверждать, что браузер показал изображение.</p>\n<p>Та же граница относится к геометрии. Проверка <code>width > 0</code> полезна, когда данные приходят из CMS или API. Она ловит пустое значение до рендера. Но она не видит <code>aspect-ratio</code>, grid, container query, смену шрифта и динамический контент. Поэтому честное имя проверки — <code>geometryDeclared</code>, а не <code>noLayoutShift</code>.</p>\n<figure><img src=\"/assets/editorial/2022/media-performance-2022-state-boundary.svg\" alt=\"Схема media slot: markup и geometry принадлежат контракту компонента, load intent принадлежит странице, declared load и decode относятся к учебной модели, а external observation приходит из отдельного браузерного прогона\" loading=\"lazy\" /><figcaption>Слои media slot нельзя свести к одному флагу. Схема показывает границу между данными компонента, намерением страницы и наблюдением браузера.</figcaption></figure>\n<h2>Как наблюдать реальную страницу</h2>\n<p>Когда нужен LCP, запускайте наблюдение в странице, а не в объекте контракта. Следующий код — пример регистрации наблюдателя. Он показывает, откуда берётся запись, но сам по себе не даёт результата для вашего URL. Для корректного вывода нужно сохранить маршрут, viewport, браузер, состояние кэша и момент завершения сценария.</p>\n<pre><code>const entries = [];\n\nconst observer = new PerformanceObserver((list) => {\n entries.push(...list.getEntries());\n});\n\nobserver.observe({\n type: 'largest-contentful-paint',\n buffered: true,\n});\n\n// В конце сценария сохраняют последний кандидат\n// вместе с URL, viewport и состоянием кэша.</code></pre>\n<p>Запись наблюдателя отвечает на вопрос «какой кандидат и в какой момент был замечен». Она не отвечает на вопрос «почему он пришёл поздно». Причину ищут по цепочке: время ответа, выбор варианта, момент обнаружения ресурса, декодирование, блокирующие ресурсы и layout. Не называйте число LCP результатом оптимизации, пока не определены условия сравнения и не повторён тот же сценарий.</p>\n<p>Намерение страницы тоже нужно проверять отдельно. Если hero помечен critical, это полезная подсказка для архитектуры и code review. Но browser scheduler учитывает документ, сеть, кэш, тип ресурса и конкурентов. Строка в конфигурации не заменяет trace. Если наблюдение расходится с intent, сначала зафиксируйте расхождение, затем меняйте один слой.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><caption>Карта диагностики медиа и ложных performance-выводов</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Маленькая картинка скачивает большой файл</td><td>Slot получил неподходящий source или variant</td><td>Сопоставить slot, URL, размер ответа и выбранный формат</td><td>Исправить mapping и повторить тот же сценарий</td></tr><tr><td>Карточка сдвигает соседний контент</td><td>Геометрия отсутствует или меняется после рендера</td><td>Проверить dimensions в данных и фактический DOM/layout</td><td>Задать устойчивую рамку; отдельно проверить layout shift</td></tr><tr><td>Hero объявлен critical, но поздно появляется</td><td>Intent принят за факт приоритета</td><td>Сохранить trace с route, viewport и cache state</td><td>Проверять resource discovery и конкурентов, а не только label</td></tr><tr><td>Unit-тест говорит «медиа готово»</td><td>Локальный переход назван браузерным результатом</td><td>Проверить, создаются ли DOM, сеть и observer</td><td>Переименовать assertion и добавить отдельный page-level прогон</td></tr><tr><td>После правки нет сравнимого эффекта</td><td>Одновременно изменены source, CSS и priority</td><td>Сравнить diff и условия запуска</td><td>Откатить лишние изменения и повторить одну гипотезу</td></tr></tbody></table></div>\n<h2>Порядок действий</h2>\n<ol><li><strong>Запишите симптом.</strong> Выберите один URL, один media slot и один наблюдаемый дефект: поздний hero, лишний объём или сдвиг layout.</li><li><strong>Отделите намерение от факта.</strong> Выпишите ожидаемые source, dimensions и intent. Рядом укажите, что действительно видно в браузерном прогоне.</li><li><strong>Проверьте контракт.</strong> Убедитесь, что markup содержит корректный <code>src</code> и <code>alt</code>, geometry положительна, а названия assertion не обещают LCP или CLS.</li><li><strong>Проверьте отрицательный путь.</strong> Удалите dimensions, подставьте старый ответ, выберите неверный variant или воспроизведите позднее появление. Система должна отказать явно, а не показать ложную готовность.</li><li><strong>Снимите наблюдение.</strong> Зафиксируйте browser/version, route, viewport, cache state, сетевые условия и trace. Не смешивайте данные разных сценариев.</li><li><strong>Измените один слой.</strong> Выберите source mapping, geometry, композицию страницы или intent. Сохраните прежнее значение для отката.</li><li><strong>Повторите тот же прогон.</strong> Сравнивайте одинаковые условия. Если гипотеза не подтверждена, верните узкую правку и расширьте проверку.</li><li><strong>Оставьте критерий.</strong> Запишите, какой факт считается исправлением и какой отрицательный путь обязан оставаться безопасным.</li></ol>\n<h2>Ограничения</h2>\n<p>Эта модель не описывает все способы доставки медиа. За её пределами остаются <code>srcset</code> и sizes, CSS background, poster видео, lazy loading, preload, CDN-переговоры, кэш, ошибки декодирования, cross-origin timing, EXIF orientation и доступность. Каждый новый слой требует собственного входа и собственной проверки.</p>\n<p>Положительная geometry не гарантирует отсутствие layout shift. Правильный source не гарантирует лучший LCP. Наблюдение LCP не объясняет всю воспринимаемую скорость и не заменяет полевые данные. Учебный код не доказывает, что конкретная страница стала быстрее. Без реального прогона нельзя сообщать проценты, секунды или изменение конверсии.</p>\n<p>Отрицательный путь тоже предметен. Если источник недоступен, нужен согласованный fallback. Если dimensions не пришли, компонент должен выбрать безопасную рамку или явно показать ошибку. Если браузер не поддерживает нужный тип наблюдения, отчёт должен отметить отсутствие данных. Не подменяйте неизвестность нулём и не называйте пропущенное наблюдение успешным.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Изменение готово, если для выбранного slot сохранены контракт и browser report. Контракт отдельно подтверждает markup, положительную geometry и намерение страницы. Report содержит одинаковые условия запуска и показывает наблюдаемый результат. Неверный порядок переходов, отсутствующая геометрия и недоступный source не приводят к ложному состоянию «готово». Изменён один слой, его эффект можно сравнить, а прежний mapping можно вернуть без переписывания остальных слоёв.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://www.w3.org/TR/largest-contentful-paint/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: Largest Contentful Paint</a> — спецификация интерфейса и записей LCP.</li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/API/LargestContentfulPaint\" target=\"_blank\" rel=\"noopener noreferrer\">MDN: LargestContentfulPaint</a> — официальное описание свойств записи и примера с <code>PerformanceObserver</code>.</li><li><a href=\"https://web.dev/articles/optimize-lcp\" target=\"_blank\" rel=\"noopener noreferrer\">web.dev: Optimize Largest Contentful Paint</a> — документация Chrome о диагностике LCP в trace и полевых данных.</li></ul>"
|
||
}
|