Angular

NgOptimizedImage и Core Web Vitals

Lighthouse-аудит интернет-магазина показывает performance 42 из 100. Код компонентов чистый, change detection на OnPush, бандл ужат. Виновник один: первый экран грузит баннер-герой на 1.8 МБ без размеров, без приоритета и без современного формата. Браузер узнаёт ширину картинки только после загрузки, страница дёргается, и метрика LCP уходит за 4 секунды. Команда Angular столкнулась с тем же на тысячах проектов и в версии 15 вынесла лучшие практики в одну директиву - NgOptimizedImage.

  • Land's End и другие e-commerce: после перехода на NgOptimizedImage в кейсах команды Angular LCP первого экрана улучшался на десятки процентов
  • Chrome UX Report: изображения - самый частый LCP-элемент на вебе, поэтому их оптимизация напрямую двигает рейтинг в поиске
  • Google Search: Core Web Vitals входят в сигналы ранжирования, медленный LCP роняет позиции коммерческих страниц
  • Любой каталог товаров: сетка из сотен превью без lazy-loading тянет мегабайты трафика и забивает сеть на мобайле
  • Новостные сайты: герой-картинка без зарезервированной высоты сдвигает текст под ней, читатель промахивается по ссылке - это CLS

Предварительные знания

  • Понимание Angular-компонента и его шаблона (template)
  • Базовое знание HTML-тега img и атрибутов src, width, height
  • Идея о том, что страница грузится по сети и часть ресурсов важнее других
  • Компоненты Angular

Откуда взялась NgOptimizedImage

Директива выросла из проекта Aurora - совместной инициативы команд Chrome и фреймворков по встраиванию лучших практик производительности прямо в инструменты. Инженеры заметили, что разработчики раз за разом забывают одни и те же вещи: задать width и height, пометить главную картинку приоритетной, не лениво грузить то, что видно сразу. Вместо документации, которую никто не читает, решили закодировать правила в директиву с предупреждениями в консоли. NgOptimizedImage появилась в developer preview в Angular 14 (2022) и стала стабильной в Angular 15 (ноябрь 2022).

Что такое Core Web Vitals и при чём тут картинки

Core Web Vitals - это три метрики реального опыта пользователя, которые Google измеряет на живых посещениях и учитывает в ранжировании. LCP (Largest Contentful Paint) отвечает на вопрос когда отрисовался самый крупный элемент экрана. CLS (Cumulative Layout Shift) измеряет насколько прыгает макет во время загрузки. INP (Interaction to Next Paint) показывает задержку отклика на действия. Картинки бьют сразу по двум первым.

МетрикаЧто измеряетХороший порогКак картинки её портят
LCPВремя отрисовки крупнейшего элементадо 2.5 сТяжёлый баннер-герой грузится медленно и поздно
CLSНакопленный сдвиг макетадо 0.1Картинка без width/height вдруг занимает место и толкает контент
INPЗадержка отклика на вводдо 200 мсКосвенно: лишний трафик и декодирование занимают главный поток

По данным Chrome UX Report, в большинстве сайтов LCP-элементом оказывается именно изображение - чаще всего баннер или главное фото товара. Поэтому ускорение одной этой картинки часто двигает метрику сильнее, чем недели работы над JavaScript.

Корень проблемы CLS виден на примере. Браузер встречает тег img без размеров и не знает, сколько места занять, поэтому ставит ноль высоты и рисует текст вплотную. Когда картинка загрузилась, она раздвигает блоки, и весь контент ниже прыгает вниз. Резервирование размеров заранее убирает этот сдвиг полностью.

Изображение-баннер на первом экране загружается без атрибутов width и height. Какую метрику Core Web Vitals это в первую очередь ухудшает и почему?

Директива NgOptimizedImage: ngSrc, width, height

NgOptimizedImage - это standalone-директива из @angular/common. Она применяется к тегу img через атрибут ngSrc вместо обычного src. Подмена имени не случайна: пока директива готовит оптимизированную загрузку, она сама подставит правильный src, preload и lazy-loading. Разработчик импортирует директиву в standalone-компонент и пишет ngSrc, width и height - последние два обязательны, и без них директива бросит ошибку в консоль во время разработки.

Обязательные width и height задают внутреннее соотношение сторон (intrinsic aspect ratio). Браузер сразу резервирует пропорцию, а реальный размер на экране задаётся через CSS. Это убирает CLS. Если у картинки неизвестны точные размеры или она должна заполнять контейнер, вместо width/height используется атрибут fill, и тогда родителю задают position и нужные размеры в CSS.

Нельзя одновременно задавать width/height и fill - это взаимоисключающие режимы, и директива сообщит об ошибке. Атрибут fill снимает требование размеров, но тогда контейнер обязан иметь position relative или absolute и собственные размеры, иначе картинка схлопнется.

  1. Импортировать NgOptimizedImage в массив imports компонента
  2. Заменить src на ngSrc у тега img
  3. Указать width и height (или fill для контейнерного режима)
  4. Проверить консоль разработки: директива подскажет про пропущенные атрибуты и плохие размеры

Почему NgOptimizedImage требует атрибут ngSrc вместо обычного src и делает width/height обязательными?

Приоритет, lazy-loading и LCP

По умолчанию NgOptimizedImage помечает картинки атрибутом loading lazy: то, что ниже первого экрана, не грузится, пока не приблизится к области видимости. Это экономит трафик на каталогах. Но для LCP-картинки первого экрана lazy-loading вреден: её надо показать как можно раньше. Для этого есть атрибут priority. Он отключает ленивую загрузку, ставит высокий приоритет выборки (fetchpriority high) и добавляет в head тег preload, чтобы браузер начал тянуть картинку до парсинга разметки.

Правило простое: priority получает ровно та одна картинка, которая, скорее всего, будет LCP-элементом - обычно баннер-герой или главное фото на первом экране. Помечать priority всё подряд бессмысленно: если приоритетно всё, то не приоритетно ничего, и браузер снова грузит вперемешку.

Если LCP-картинка не помечена priority, директива выведет предупреждение в консоль в режиме разработки. Это сделано намеренно: команда Angular встроила детектор частой ошибки прямо в инструмент, чтобы разработчик увидел проблему до релиза, а не в Lighthouse-аудите после.

Положение картинкиАтрибутПоведение
LCP-элемент первого экранаprioritypreload, fetchpriority high, без lazy
Контент ниже первого экранапо умолчаниюloading lazy, грузится при приближении
Декоративная мелочь вне LCPпо умолчаниюlazy, минимальное влияние на метрики

Разработчик помечает атрибутом priority все 30 картинок на странице каталога, надеясь ускорить загрузку. Почему это ошибка?

Responsive srcset и image loaders

Одна и та же картинка не должна весить одинаково на телефоне и на 4K-мониторе. Атрибут ngSrcset перечисляет доступные ширины, а атрибут sizes описывает, сколько места картинка займёт на экране при разных условиях. Браузер сам выбирает наименьший подходящий вариант. Для мобильного экрана это означает в разы меньше байтов и более быстрый LCP.

Сами по себе ширины из ngSrcset ничего не значат без сервиса, который умеет отдавать картинку нужного размера. Эту роль играет image loader - функция, которая по базовому пути и запрошенной ширине строит конечный URL к CDN. Angular поставляет готовые лоадеры для популярных сервисов (Cloudflare, Cloudinary, Imgix, ImageKit, Netlify), а при необходимости пишется свой.

Лоадер для CDN решает вторую половину задачи: современный формат. Параметр format auto заставляет CDN отдавать WebP или AVIF браузерам, которые их поддерживают, а старым - JPEG. Один и тот же ngSrc в шаблоне при этом не меняется, вся логика форматов уходит в лоадер.

Тип ImageLoaderConfig уже описывает поля src и опциональный width. Поэтому проверка width через width ?? 0 и optional chaining достаточна - не нужны ни приведения as, ни non-null. Строгая типизация Angular здесь работает на стороне разработчика.

В шаблоне указан ngSrcset с набором ширин, но картинки всё равно грузятся в одном исходном размере. Какая часть настройки, скорее всего, пропущена?

Связь с другими темами

Урок открывает модуль про рендеринг и производительность. Дальше связи такие:

  • Сборка и application builder — Сборщик на esbuild/Vite копирует и хеширует ассеты, к нему подключают image loader для CDN
  • Change detection — Картинки не зависят от CD, но общий бюджет производительности страницы складывается из обоих факторов

Итог

  • Изображения - самый частый LCP-элемент в вебе, поэтому именно с них начинается оптимизация загрузки
  • NgOptimizedImage (стабильна с Angular 15) кодирует лучшие практики в директиву ngSrc и предупреждает об ошибках в консоли
  • Обязательные width и height резервируют место под картинку и убирают сдвиг макета - метрику CLS
  • Атрибут priority отключает lazy-loading и добавляет preload для LCP-картинки первого экрана
  • ngSrcset вместе с sizes отдаёт каждому устройству картинку подходящего размера, экономя трафик и ускоряя загрузку
  • Кастомный image loader подключает CDN с авто-форматами WebP/AVIF без правки шаблонов

Связанные уроки

  • ng-03-components — Директива применяется к шаблону компонента, поэтому нужна базовая модель компонентов и привязок
  • ng-43-cli-build-internals — Application builder отвечает за сборку ассетов, и понимание сборки помогает выстроить пайплайн картинок
NgOptimizedImage и Core Web Vitals

0

1

Войти