8 lines
18 KiB
JSON
8 lines
18 KiB
JSON
{
|
||
"index": 195,
|
||
"slug": "editorial-2022-08-practice-media-performance",
|
||
"title": "Медиа в первом экране: разделите разметку, геометрию и измерение",
|
||
"excerpt": "Поздний hero и скачок карточки часто начинаются с одной ошибки: команда смешивает описание ресурса, резервирование места и браузерное наблюдение. Разбираем контракт media slot, отрицательный путь и проверяемый критерий готовности.",
|
||
"contentHtml": "<p>У большого изображения в первом экране обычно видны три симптома: hero появляется поздно, карточка прыгает после загрузки или браузер выбирает не тот ресурс. Цена ошибки выше одной медленной картинки. Пользователь видит пустое место или меняющийся контент. Команда меняет <code>loading</code>, размеры и приоритет одновременно, а затем не может понять, что сработало.</p>\n<p>Тезис простой: медиа-слот нужно проверять как несколько независимых контрактов. Разметка описывает ресурс. Геометрия резервирует место. Загрузка и декодирование относятся к пути платформы. LCP и layout shift появляются только в наблюдаемом браузерном сценарии. Если свести всё к флагу <code>loaded</code>, тест легко выдаст правильный ответ на неправильный вопрос.</p>\n<h2>Сначала отделите симптом от причины</h2>\n<p>Возьмём карточку товара. В данных есть изображение размером 2400×1600, а компонент показывает его в области 320×213. Ниже по странице лежат ещё двадцать таких карточек. Пользователь открывает страницу на телефоне. Сеть получает тяжёлый файл, контейнер сначала не имеет высоты, а после ответа соседний текст сдвигается.</p>\n<p>Эта ситуация содержит как минимум три разные задачи. Нужно выбрать источник, подходящий слоту. Нужно объявить ожидаемую геометрию. Нужно проверить, что произошло в реальной странице. Уменьшение файла не исправит отсутствие высоты. <code>width</code> и <code>height</code> не докажут, что картинка стала кандидатом LCP. Учебный unit-тест не покажет, что layout shift исчез.</p>\n<h2>Механизм media slot</h2>\n<p>Для одного изображения зафиксируйте четыре поля.</p>\n<ul><li><strong>Markup.</strong> Компонент знает <code>src</code>, альтернативный текст и, если нужно, набор responsive-источников.</li><li><strong>Geometry.</strong> Контракт знает положительные <code>width</code> и <code>height</code> или ratio. Это вход для layout, а не снимок фактического CSS-box.</li><li><strong>Intent.</strong> Приложение может назвать слот критичным для первого сценария или отложенным. Такая метка объясняет решение команды, но не назначает реальный приоритет scheduler.</li><li><strong>Observation.</strong> Отдельный прогон браузера сообщает, какой ресурс загрузился, когда он отрисовался и менялась ли геометрия. Без этого нельзя утверждать, что оптимизация улучшила LCP или CLS.</li></ul>\n<p>Порядок важен. Без разметки нельзя планировать ресурс. Без геометрии нельзя обещать стабильный контейнер. После загрузки ещё не следует автоматически делать вывод о готовых пикселях. А запись с числом, переданная в локальную модель, не становится измерением браузера.</p>\n<h2>Учебный пример: контракт, а не браузер</h2>\n<p>Следующая модель ограничена памятью процесса. Она не создаёт DOM, не открывает сеть, не вызывает <code>decode()</code> и не запускает <code>PerformanceObserver</code>. Её задача — проверить порядок переходов и не дать названию функции обещать больше, чем оно делает.</p>\n<pre><code>const model = {\n markup: null,\n geometry: null,\n request: { phase: 'not-planned', intent: null },\n decode: 'not-requested',\n};\n\nfunction declareMarkup(state, descriptor) {\n if (!descriptor.src.startsWith('/media/')) {\n return { ...state, event: 'markup-rejected' };\n }\n return { ...state, markup: descriptor, event: 'markup-declared' };\n}\n\nfunction reserveGeometry(state, width, height) {\n if (!Number.isInteger(width) || !Number.isInteger(height)\n || width < 1 || height < 1) {\n return { ...state, event: 'geometry-rejected' };\n }\n return { ...state, geometry: { width, height }, event: 'geometry-reserved' };\n}\n\nfunction planLoad(state, intent) {\n if (!state.markup) return { ...state, event: 'load-blocked-no-markup' };\n if (!['critical-candidate', 'deferred-candidate'].includes(intent)) {\n return { ...state, event: 'intent-rejected' };\n }\n return { ...state, request: { phase: 'planned', intent }, event: 'load-planned' };\n}\n\nfunction markDecode(state) {\n if (state.request.phase !== 'declared-loaded') {\n return { ...state, event: 'decode-rejected-before-load' };\n }\n return { ...state, decode: 'declared-decoded', event: 'decode-declared' };\n}</code></pre>\n<p>В настоящей реализации переход <code>declared-loaded</code> должен быть связан с платформенным событием или адаптером приложения. В примере этого перехода нет намеренно: он показывает границу модели. Вызов <code>markDecode</code> до загрузки отклоняется. Нулевая геометрия не резервирует место. Неверный источник не попадает в контракт.</p>\n<figure><img src=\"/assets/editorial/2022/media-performance-2022-slot-contract.svg\" alt=\"Схема media slot: разметка и геометрия отделены от намерения загрузки, учебных переходов load и decode и внешнего наблюдения\" loading=\"lazy\" /><figcaption>Контракт делает границы явными. Схема не является DOM-деревом, browser trace или измерением LCP и CLS.</figcaption></figure>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><caption>Диагностика проблем одного media slot</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Пустое место до hero</td><td>Разметка или источник появляются поздно</td><td>Проверить HTML, resource timing и порядок запросов</td><td>Исправить композицию и сопоставление source-to-slot</td></tr><tr><td>Карточка сдвигается после ответа</td><td>У контейнера нет устойчивой geometry</td><td>Проверить dimensions в данных и фактический DOM в выбранном viewport</td><td>Добавить ratio или положительные width/height; затем проверить страницу</td></tr><tr><td>Hero приходит после второстепенных файлов</td><td>Intent приняли за реальный priority</td><td>Сопоставить resource trace, cache state и конкурирующие запросы</td><td>Уточнить источник и проверить browser-сценарий, а не менять label вслепую</td></tr><tr><td>Тест сообщает «готово» слишком рано</td><td>Fixture смешивает load, decode и paint</td><td>Посмотреть, какой API и какой слой реально вызывается</td><td>Разделить локальные переходы и вынести метрику в отдельный прогон</td></tr><tr><td>После ошибки слот исчезает</td><td>Нет отрицательного пути для недоступного медиа</td><td>Отключить ресурс или вернуть ошибку загрузки</td><td>Сохранить geometry, alt и понятный fallback</td></tr></tbody></table></div>\n<h2>Почему <code>width</code> и <code>height</code> не закрывают весь вопрос</h2>\n<p>Атрибуты размеров дают браузеру исходные данные для соотношения сторон. Это полезная часть контракта. Но компонент может обрезать изображение через <code>object-fit</code>, менять рамку в grid или скрывать слот после действия пользователя. Соседний блок может загрузить шрифт и изменить высоту. Поэтому unit-проверка должна говорить <code>geometryReserved</code>, а не <code>noLayoutShift</code>.</p>\n<p>Для разных представлений нужны разные размеры. Hero, карточка и миниатюра могут использовать один оригинал, но не одну рамку. Если высота неизвестна, запишите правило fallback. Не переносите размеры исходного файла в UI автоматически. Иначе большой оригинал создаст ложное чувство точности, а маленький слот всё равно будет рассчитан неверно.</p>\n<h2>Почему intent не равен приоритету</h2>\n<p>Метка <code>critical-candidate</code> полезна в code review. Она заставляет ответить, почему ресурс нужен до первого действия пользователя. Но это не команда браузеру скачать файл первым. На порядок влияют разметка, другие ресурсы, кеш, соединение, браузер и версия движка. В отчёте разделяйте намерение и факт: «компонент пометил слот критичным» — одно утверждение; «в этом запуске ресурс получил такой путь и такую отрисовку» — другое.</p>\n<p>Для отложенного медиа отрицательный путь не исчезает. Отложенный слот всё ещё должен иметь alt, geometry, состояние ошибки и понятное поведение при возвращении в viewport. Если продукту нужен placeholder, его тоже нужно описать. Не прячьте отсутствие ресурса за бесконечным skeleton без условия завершения.</p>\n<h2>Порядок действий</h2>\n<ol><li><strong>Запишите один симптом.</strong> Укажите URL, slot, viewport и действие пользователя. Формулировка «страница медленная» слишком широка.</li><li><strong>Разложите контракт.</strong> Найдите markup, geometry, intent и источник observation. Отметьте отсутствующее поле.</li><li><strong>Проверьте отрицательный путь.</strong> Подайте неизвестный source, нулевую высоту, недоступный ответ и повторный вход в отложенный слот.</li><li><strong>Запустите локальную проверку.</strong> Убедитесь, что geometry отклоняет невалидные значения, decode не следует до declared load, а fixture не называет себя LCP.</li><li><strong>Проведите браузерный прогон.</strong> Зафиксируйте браузер, версию, viewport, cache state, сеть, URL и список конкурирующих ресурсов.</li><li><strong>Сделайте одну правку.</strong> Меняйте geometry, source mapping или композицию. Не меняйте все слои одновременно.</li><li><strong>Повторите тот же сценарий.</strong> Сравните только наблюдаемые факты. Если результат не подтверждает гипотезу, верните одну правку и обновите причину.</li><li><strong>Оставьте доказательство.</strong> Сохраните локальный результат, browser report и правило отката рядом с изменением.</li></ol>\n<h2>Ограничения</h2>\n<p>Эта схема не моделирует <code>srcset</code> selection, CSS background, video poster, CDN negotiation, preload, lazy loading, кеширование, ошибки декодирования, server rendering и accessibility tree. Она не обещает, что размеры устранили CLS, и не определяет, какой ресурс станет LCP.</p>\n<p>Текущий LCP остаётся свойством конкретного документа и запуска. Кандидат может измениться до пользовательского ввода, а результат зависит от содержимого viewport и условий загрузки. Поэтому учебное число <code>value: 1</code> или зелёный PASS не являются production-результатом. Без реального trace нельзя писать «LCP улучшился на N миллисекунд».</p>\n<p>Не всякая проблема требует немедленной смены источника. Если hero поздний из-за серверной композиции, новый формат файла не устранит причину. Если layout меняется из-за контейнера, атрибуты изображения не заменят исправление CSS или данных. Контракт помогает выбрать слой, но не принимает решение за продукт.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Изменение готово, если для одного media slot выполнены все условия: markup содержит ожидаемый ресурс и alt; geometry принимает только валидную рамку; intent объяснён и не выдан за browser priority; отрицательный путь сохраняет понятный fallback; локальная fixture проверяет только свой порядок; браузерный прогон содержит условия и наблюдаемый результат. В отчёте отдельно названы факт, интерпретация и оставшееся ограничение. Если хотя бы один слой подтверждён только словом «работает», задача не готова.</p>\n<h2>Проверяемые источники</h2><ul><li><a href=\"https://html.spec.whatwg.org/dev/embedded-content.html#the-img-element\" target=\"_blank\" rel=\"noopener noreferrer\">WHATWG HTML Living Standard: The img element</a> — официальный стандарт по элементу изображения, его размерам, загрузке и декодированию.</li><li><a href=\"https://www.w3.org/TR/largest-contentful-paint/\" target=\"_blank\" rel=\"noopener noreferrer\">W3C: Largest Contentful Paint</a> — актуальная спецификация наблюдения крупнейшей отрисовки; документ имеет статус Working Draft и может меняться.</li><li><a href=\"https://developer.mozilla.org/en-US/docs/Web/API/LargestContentfulPaint\" target=\"_blank\" rel=\"noopener noreferrer\">MDN: LargestContentfulPaint</a> — официально поддерживаемое разработчиками браузеров описание API и его ограничений.</li></ul>"
|
||
}
|