Files
progcode/editorial/agent-rewrites/194.json
T

8 lines
21 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 194,
"slug": "editorial-2022-08-mechanism-media-performance",
"title": "Медиа без ложных метрик: как отделить контракт компонента от наблюдения браузера",
"excerpt": "Изображение может иметь правильный размер в данных и всё равно поздно попасть на экран. Разбираем слои media slot, границы LCP-наблюдения, учебный контракт и обратимую проверку.",
"contentHtml": "<p>Изображение в карточке занимает большую часть сетевого трафика, хотя на экране выглядит маленьким. В другом случае в разметке hero-блока указаны <code>width</code> и <code>height</code>, но первый экран всё равно меняет геометрию. Команда видит слово <code>loaded</code>, добавляет <code>preload</code> или меняет <code>fetchpriority</code> и ждёт улучшения. Через неделю уже трудно установить, что именно изменило результат: источник, разметка, CSS, очередь загрузки или условия замера. Цена ошибки — лишний трафик, поздний главный контент и релиз, который сложнее откатить, чем проверить.</p>\n<p>Тезис статьи простой: контракт media slot и наблюдение браузера — разные факты. LCP (Largest Contentful Paint) — наблюдаемая метрика отрисовки крупнейшего видимого элемента до пользовательского ввода; она не хранится в объекте контракта. Компонент может объявить источник, альтернативный текст и ожидаемую геометрию. Страница может выразить намерение загрузить slot раньше или позже. Браузер сам решает, когда получить ресурс, декодировать его, разместить и нарисовать. Только отдельный прогон страницы показывает, что произошло в конкретной среде. Если склеить эти слои одним флагом, локальная проверка начнёт выдавать обещание о поведении браузера.</p>\n<h2>Механизм: пять слоёв одного media slot</h2>\n<p>Начните с одного media 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 одного слоя не использовался как доказательство другого. Проверка <code>dimensions &gt; 0</code> защищает вход компонента, но не доказывает наличие такого же CSS box и отсутствие CLS (Cumulative Layout Shift, неожиданных сдвигов layout). Label <code>critical-candidate</code> фиксирует решение приложения, но не сообщает, какой ресурс браузер выбрал первым. Запись LCP принадлежит наблюдению страницы, а не объекту из unit-теста.</p>\n<h2>Почему loaded недостаточно</h2>\n<p>Слово <code>loaded</code> часто скрывает несколько разных фактов. Страница может объявить намерение загрузить ресурс раньше. Если браузер отправит запрос, он получит ответ; затем данные изображения должны стать доступными для декодирования и отрисовки. Эти этапы могут перекрываться и не равны одной записи наблюдателя. Поэтому <code>load</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 rejected = markDecode(media, 'teaching-decode-ok');\nconst loaded = markLoad(media, 'teaching-load-ok');\nconst decoded = markDecode(loaded, 'teaching-decode-ok');\n\nconsole.log({ rejected, decoded });</code></pre>\n<p>Имена <code>teaching-load-ok</code> и <code>teaching-decode-ok</code> намеренно говорят об ограничении. Это токены локальной модели. Первый вызов в примере показывает отрицательный путь: декодирование до объявления загрузки отклоняется. В реальном коде токены заменят события, Promise или адаптер платформы. Нельзя вызвать эту функцию вместо загрузки картинки и затем утверждать, что браузер показал изображение.</p>\n<p>Та же граница относится к геометрии. Проверка <code>width &gt; 0</code> и <code>height &gt; 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 = [];\nconst supportsLcp =\n typeof PerformanceObserver !== 'undefined' &&\n PerformanceObserver.supportedEntryTypes?.includes(\n 'largest-contentful-paint',\n );\n\nconst observer = supportsLcp\n ? new PerformanceObserver((list) =&gt; {\n entries.push(...list.getEntries());\n })\n : null;\n\nobserver?.observe({\n type: 'largest-contentful-paint',\n buffered: true,\n});\n\n// После завершения сценария сохраняют последний кандидат.\nconst lastEntry = entries[entries.length - 1] ?? null;\nobserver?.disconnect();\nconsole.log({ supportsLcp, lastEntry });</code></pre>\n<p>Набор записей отвечает на вопрос «какой кандидат и в какой момент был замечен». Пока страница загружается, API может добавить несколько кандидатов; последняя запись — текущий последний кандидат на момент чтения. Если записей нет, это отсутствие данных, а не LCP, равный нулю. Записи не отвечают на вопрос «почему кандидат пришёл поздно». Причину ищут по цепочке: время ответа, выбор варианта, момент обнаружения URL браузером, декодирование, блокирующие ресурсы и layout. Не называйте число LCP результатом оптимизации, пока не определены условия сравнения и не повторён тот же сценарий.</p>\n<p>Намерение страницы тоже нужно проверять отдельно. Если hero помечен critical, это полезная подсказка для архитектуры и code review. Но планировщик браузера учитывает документ, сеть, кэш, тип ресурса и конкурентов. Строка в конфигурации не заменяет 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> Зафиксируйте браузер и его версию, маршрут, viewport, состояние кэша, сетевые условия и 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 сохранены контракт и отчёт браузерного прогона. Контракт отдельно подтверждает разметку, положительную geometry и намерение страницы. Отчёт содержит одинаковые условия запуска и показывает наблюдаемый результат. Неверный порядок переходов, отсутствующая геометрия и недоступный source не приводят к ложному состоянию «готово». Изменён один слой; его эффект можно сравнить, а прежнее сопоставление вернуть без переписывания остальных слоёв.</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><li><a href=\"https://web.dev/articles/cls\" target=\"_blank\" rel=\"noopener noreferrer\">web.dev: Cumulative Layout Shift</a> — определение CLS, причины сдвигов и различия лабораторных и полевых измерений.</li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/img\" target=\"_blank\" rel=\"noopener noreferrer\">MDN: <code>&lt;img&gt;</code></a> — требования к <code>alt</code>, размерам изображения и responsive-атрибутам.</li></ul>"
}