Оптимизация изображений в Angular
Команды Angular и Chrome Aurora проработали директиву для изображений, которая поможет разработчикам оптимизировать контент.
Инициатива Chrome Aurora и Angular по созданию Директивы Изображения в Angular направлена на улучшение загрузки изображений в вебе. Об этом подробно написано в блоге Chrome Aurora. Загрузка изображений значительно влияет на основные показатели сайта, которые используются для оценки производительности загрузки страницы. Директива NgOptimizedImage
направлена на улучшение этих показателей, обеспечивая эффективную загрузку изображений.
NgOptimizedImage
— это директива для Angular, которая начинает работать автоматически после того, как вы импортировали ее и заменили атрибут src в теге <img>
на ngSrc
, вот так:
import { NgOptimizedImage } from '@angular/common'; import { Component } from '@angular/core'; @Component({ selector: 'ng-conf-image', template: ` <!-- Will not use the NgOptimizedImageDirective --> <img src="angie.jpg" /> <!-- Will use the NgOptimizedImageDirective --> <img ngSrc="angie.jpg" /> `, imports: [NgOptimizedImage], }) class NgConfImage {}
Даже если весь функционал директивы избыточен, все равно стоит её попробовать. В режиме разработки в консоль будут выводиться предупреждения с предложениями, которые легко добавить шаблон.
Web Vitals
Основные показатели качества веб-страниц (Core Web Vitals, сокр. CWV), о которых мы будем говорить в контексте функциональности директивы изображений, включают: LCP, FID, CLS, TTFB, FCP, TBT и TTI. Каждый из них измеряет определенный аспект производительности загрузки веб-приложения. Некоторые из этих метрик, возможно, вам уже знакомы, например, Largest Contentful Paint (LCP), а некоторые могут быть более узкоспециализированными и измерять что-то, о чем обычно не говорят, как Time to First Byte (TTFB).
Давайте рассмотрим, какие измерения составляют основные показатели качества веб-страниц, прежде чем продолжить обсуждение конкретики директивы изображений.
- Largest Contentful Paint (LCP) - Измеряет время, необходимое для отображения наибольшего изображения или текстового блока, видимого в области просмотра относительно момента начала загрузки страницы. Таким образом, если на сайте большое изображение расположено прямо в верхней части страницы или выше складки, и оно настроено некорректно, это может существенно негативно повлиять на показатель LCP.
- First Input Delay (FID) - Измеряет, насколько быстро браузер реагирует на действия пользователя. На FID могут влиять: сложный JS, активность основного потока (JS однопоточен, поэтому избыточная активность основного потока будет задерживать действия пользователя; изображения загружаются асинхронно, и современные браузеры стараются оптимизировать отрисовку изображений для минимизации фризов для пользователя) и задержки в сети. Изображения обычно не оказывают прямого воздействия на FID.
- Cumulative Layout Shift (CLS) - Измеряет, насколько элементы на странице сдвигают макет во время их отрисовки. Размер изображения (в байтах) не влияет напрямую на CLS, но если они загружены без указания размеров или заменены изображениями другого размера (в случае динамических плейсхолдеров), это может негативно сказаться на показателе CLS.
- Time to First Byte (TTFB) - Время, необходимое браузеру для получения первого байта веб-страницы. Обычно это не связано с изображениями, поскольку первый байт, получаемый браузером пользователя, обычно является index.html, который предоставляется веб-сервером или CDN (до начала любой отрисовки).
- First Contentful Paint (FCP) - Изображения могут напрямую влиять на FCP сайта. FCP измеряет время, необходимое для отрисовки первого элемента содержимого. Если это большое изображение, то это приведет к нежелательному показателю FCP.
- Total Blocking Time (TBT) - Изображения могут косвенно влиять на TBT в некоторых случаях, как обсуждалось выше в пункте FID и при отрисовке изображений после их загрузки.
- Time to Interactive (TTI) - Подобно TBT, TTI может быть косвенно затронут размером изображений, когда браузер их отрисовывает.
Мы сосредоточимся на следующих ключевых показателях веб-страницы: LCP, CLS и FCP, так как они больше всего зависят от изображений. Мы рассмотрим, как директива изображения Angular помогает с этими метриками и какие внутренние механизмы она использует для их достижения.
Чтобы понять, как директива изображения влияет на каждый из ключевых показателей веб-страницы и какие инструменты и настройки мы имеем для настройки загрузки изображений, мы рассмотрим каждое из свойств, поддерживаемых директивой изображения. Некоторые из этих свойств являются встроенными для тега <img>
и встроены в браузер, а некоторые добавляются как @Inputs
при импорте NgOptimizedImage
. Каждое свойство контролирует различные аспекты процесса загрузки изображения, от сетевых соединений до размеров. Они группируются по секциям Priority
и Image Sizing
, каждая из которых имеет свои подразделы, более детально рассматривающие конкретные атрибуты.
Priority
Приоритет загрузки и получения данных
Первым на очереди priority
. Атрибут priority
является первой линией обороны для защиты LCP (и, возможно, FCP). Давайте возьмем URL изображения https://images.unsplash.com/photo-1417325384643-aac51acc9e5d в качестве примера. По умолчанию, если мы рендерим шаблон, например, следующим образом:
<img ngSrc="https://images.unsplash.com/photo-1417325384643-aac51acc9e5d" fill />
Результирующий тег <img>
в DOM
будет выглядеть примерно так (для краткости опущены дополнительные свойства):
<img loading="lazy" fetchpriority="auto" src="https://images.unsplash.com/photo-1417325384643-aac51acc9e5d" />
Обратите внимание, что изображение имеет установленные атрибуты loading="lazy"
и fetchpriority="auto"
. Если это изображение LCP, мы не хотим, чтобы браузер загружал его лениво (поскольку мы знаем, что оно будет необходимо для загрузки страницы сразу же). fetchpriority
также установлен по умолчанию в auto, так как это браузерное значение по умолчанию, если альтернатива не указана. Это сообщает браузеру "загрузи его максимально оптимизированно", но если мы знаем, что нам нужно загрузить его немедленно, мы должны явно указать это. Можно так же установить атрибут priority
, и тогда директива автоматически установит оптимальные значения, чтобы изображение было загружено как можно быстрее.
Укажем браузеру, что изображение должно иметь приоритет при загрузке:
<img ngSrc="https://images.unsplash.com/photo-1417325384643-aac51acc9e5d" fill priority />
в DOM дереве это будет выглядеть следующим образом:
<img priority="" loading="eager" fetchpriority="high" src="https://images.unsplash.com/photo-1417325384643-aac51acc9e5d" />
Теперь мы видим, что loading
и fetchpriority
установлены как eager
и high
соответственно. Это говорит браузеру: «загружай это сразу же, независимо от того, где это находится на странице, и загружай это перед ресурсами low
приоритета». Это гарантирует, что изображение не конкурирует с ненужными ресурсами (или теми, которые менее важны), что может негативно сказаться на LCP.
По умолчанию, при использовании серверного рендеринга (SSR
) Angular автоматически добавит то, что называется preload
в index.html
. Это тег <link>
, добавленный в элемент <head>
собранного приложения, который сообщает браузеру, что это изображение должно быть загружено раньше, поскольку оно будет нужно в начале жизненного цикла приложения.
Несмотря на название, он ничего не загружает; он просто планирует загрузку ресурса, указанного в href, с более высоким приоритетом для кэширования.
Тег preload
будет выглядеть примерно так, как показано ниже, и, как уже упоминалось, будет расположен в конце тега <head>:
<link as="image" href="https://images.unsplash.com/photo-1417325384643-aac51acc9e5d" rel="preload" fetchpriority="high" imagesizes="100vw" />
Обратите внимание на свойство as="image". Это дополнительный контекст, который Angular добавляет, чтобы помочь браузеру понять, какой ресурс загружается, и, таким образом, позволить браузеру оптимизировать процесс загрузки.
Еще одно улучшение производительности, которое Angular предложит внести (но не сделает автоматически), - это добавитьpreconnect
в заголовок, чтобы браузер мог заранее установить соединение с сервером, где хранится изображение. Это позволяет браузеру сэкономить время на установлении соединения.
Таким образом, добавив preconnect
атрибут мы гарантируем, что:
1. Браузер заранее установит соединение с сервером, где хранится изображение.
2. Изображение будет загружено и кэшировано заранее, еще до того, как оно понадобится приложению.
3. Изображение будет загружено раньше других менее важных элементов;
В некоторых случаях может понадобиться игнорировать предупреждения о отсутствии ссылокpreconnect
для определенных источников. Это можно сделать, предоставив значение для DI токенаPRECONNECT_CHECK_BLOCKLIST
. Значение этого токена имеет формат строки или массива строк. Например:
{
provide: PRECONNECT_CHECK_BLOCKLIST,
useValue: https://images.unsplash.com'
}
В теории все понятно, но что с точки зрения практики?
Получилось сэкономить чуть больше 100мс. Конечно, стоит отметить, что это простое приложение, запущенное на настольном компьютере. Эта разница станет более заметной, когда приложение будет загружать множество других устройств, и возможно, будет работать на более медленной сети, например, 2G/3G.
Размеры изображений
Для предотвращения сдвигов макета (CLS) директива обязывает вас либо предоставить width
и height
, либо использовать атрибут fill
, чтобы позволить ему заполнить родительский контейнер.
Использование непосредственно ширины и высоты сообщит браузеру, сколько места будет занимать изображение, что позволит браузеру отрисовать страницу и предотвратить сдвиг макета, вызывающий отрицательные значения CLS.
<img ngSrc="https://images.unsplash.com/photo-1417325384643-aac51acc9e5d" width="100" height="100" />
Настройка ширины и высоты различна, если мы работаем с фиксированным и адаптивным изображением. Адаптивные изображения - это те, которые вы хотите масштабировать относительно размера окна. При настройке изображения на адаптивность важно установить атрибут sizes
изображения. Директива с этим тоже поможет.
В качестве альтернативы, при использовании fill
, изображение полностью заполняет родительский контейнер. В этом примере мы задаем размер div
таким образом, чтобы он был 100 пикселей
на 100 пикселей
.
<div style="width: 100px; height: 100px;"> <img ngSrc="https://images.unsplash.com/photo-1417325384643-aac51acc9e5d" fill /> </div>
fill
атрибут особенно удобен, когда нужно отображать фоновые изображения. Им можно управлять с помощью свойства CSS object-fit
на теге <img>
. Из документации Angular:
Вы можете использовать свойство CSS object-fit для изменения способа заполнения контейнера. Если вы стилизуете изображение с помощью object-fit: "contain", изображение сохранит свое соотношение сторон и будет "вписано в рамку" для соответствия элементу. Если вы установите object-fit: "cover", элемент сохранит свое соотношение сторон, полностью заполнит элемент, и некоторый контент может быть "обрезан".
Все вышеперечисленное изменяет только размер, с которым отрисовывается изображение, а не размер загружаемого изображения. Возможно, вы задаетесь вопросом: "Что если я храню высококачественные копии всех своих изображений на CDN
, но мне нужны только их меньшие версии в веб-приложении? Нужно ли мне все равно загружать все 4 МБ изображения, чтобы отобразить его в элементе 100 x 100 пикселей?". Для этого тоже есть решение.
Размеры и атрибут srcset
Выбор правильного размера изображений может оказать наибольшее влияние на скорость их загрузки. Предоставляя атрибут sizes
в сочетании с атрибутом ngSrcset
в теге <img>
, можно предупредить Angular, какой размер изображения использовать в каких случаях.
Браузеры имеют атрибут srcset.
Это расширение атрибута src
и созданное специально для объявления различных размеров изображений для одного тега <img>
, из которых браузер выберет наиболее подходящее с точки зрения эффективности загрузки изображение. Объявляя значение атрибута sizes
, директива автоматически сгенерирует srcset
, а так же установит его на <img>
, так что при рендеринге изображения в DOM браузер может выбрать и показать оптимизированный размер изображения.
<img src="https://images.unsplash.com/photo-1417325384643-aac51acc9e5d" srcset=" https://images.unsplash.com/photo-1417325384643-aac51acc9e5d?w=1080 1080w, https://images.unsplash.com/photo-1417325384643-aac51acc9e5d?w=400 400w, https://images.unsplash.com/photo-1417325384643-aac51acc9e5d?w=200 200w " sizes="(min-width: 50vw) 1080px, ((min-width: 20vw) and (max-width: 50vw)) 400px, (max-width: 20vw) 200px" alt="A treed park" />
Подобное решение загрузит изображение подходящего размера на основе того, сколько места (в процентах от ширины окна просмотра) занимает изображение на экране. Может показаться сложным обрабатывать каждое изображение, но благодаря Angular и его загрузчикам изображений все становится проще.
В этом примере довольно легко определить, какой размер соответствует какому srcset
, так как значение px
соответствует свойству w
(ширина) каждого изображения. Однако так может быть не всегда. Должно быть строгое соответствие между размерами и URL-адресами изображений в srcset
.
Angular предоставляет набор стандартных адаптивных брекпоинтов [16, 32, 48, 64, 96, 128, 256, 384, 640, 750, 828, 1080, 1200, 1920, 2048, 3840], которые можно переопределить, предоставив IMAGE_CONFIG в корне приложения с массивом чисел[]. Подробнее в документации
Самый эффективный способ использования sizes и srcset - это использование загрузчика изображений. Это отвечает на вопрос "Как я могу подать правильный размер изображения с CDN?".
"Загрузчик изображений" - это функция, предоставляемая Angular, которая форматирует URL-адреса изображений в определенном формате, чтобы они могли быть обработаны максимально эффективно. Используя загрузчик изображений и атрибут srcset
, Angular автоматически создаст URL-адреса изображений для разных адаптивных размеров. К счастью, Angular предоставляет множество предварительно созданных загрузчиков изображений для популярных CDN изображений, таких как: Cloudflare Image Resizing
, Cloudinary
, ImageKit
и Imgix
. Для этой демонстрации мы использовали изображения Unsplash
, которые используют Imgix
в качестве сервиса для их динамического изменения размера.
Ранее, когда мы загружали изображение из Unsplash
, его размер составлял около 3,7 МБ, что слишком много для загрузки для каждого пользователя. Для оптимизации можно использовать встроенный загрузчик изображений Imgix
для уменьшения размера изображения в зависимости от размера экрана.
Первый шаг - добавить провайдер provideImgixLoader
в массив провайдеров в AppModule
или appConfig
import { ApplicationConfig } from '@angular/core'; import { appRoutes } from './app.routes'; import { provideImgixLoader } from '@angular/common'; export const appConfig: ApplicationConfig = { providers: [ // ...other providers // We are using Unsplash here but if you have your own Imgix distribution you should put your own origin here. provideImgixLoader('https://images.unsplash.com'), ], };
Таким образом мы зарегистрируем загрузчикImgix
в приложении, так что каждый раз, когда Angular обнаружит использованиеngSrc
, он будет обрабатывать изображение через загрузчик изображений (со всеми другими настроенными свойствами, такими какsizes
иngSrcset
), чтобы сформироватьURL-адреса
, которые браузер обнаружит в теге<img>
.
Далее остается только использовать директиву:
import { Component } from '@angular/core'; import { NgOptimizedImage } from '@angular/common'; @Component({ selector: 'ngconf-src-v-ngsrc', template: ` <div style="height: 100%; width: 100%;"> <!-- Depending on the image loader you are using the value of ngSrc may differ but usually it is the "name" of the image you want to load from your CDN. --> <img [ngSrc]="'photo-1417325384643-aac51acc9e5d'" style="object-fit: cover;" fill priority ngSrcset="300w, 800w, 1500w" /> </div> `, imports: [NgOptimizedImage], standalone: true, }) export class PriorityNgSrcComponent {}
Теперь Angular будет генерировать srcset
на основе размера окна. В небольших экранах будет запрашиваться изображение с шириной 300, на средних — 800, а на больших — 1500 от CDN Imgix (Unsplash)
Ниже мы можем увидеть результат рендеринга в HTML.
<img fill="" priority="" style="object-fit: cover; position: absolute; width: 100%; height: 100%; inset: 0px;" loading="eager" fetchpriority="high" src="https://images.unsplash.com/photo-1417325384643-aac51acc9e5d?auto=format" srcset=" https://images.unsplash.com/photo-1417325384643-aac51acc9e5d?auto=format&w=300 300w, https://images.unsplash.com/photo-1417325384643-aac51acc9e5d?auto=format&w=800 800w, https://images.unsplash.com/photo-1417325384643-aac51acc9e5d?auto=format&w=1500 1500w " sizes="100vw" />
Изменяя настройки окна в инструментах разработчика Chrome, мы можем увидеть различные URL-адреса, а также сгенерированные srcset
и теги изображений:
Свой загрузчик
В некоторых случаях сценариев Angular может быть недостаточно для решения поставленных задач. К счастью, Angular позволяет нам создать собственную реализацию загрузчика для таких случаев.
Создадим свой загрузчик для Unsplash, который также позволит нам настраивать качество сжатия вместе с шириной и высотой изображения. Для начала создадим простую функцию, принимающую некоторые конфигурации и возвращающую строку:
import { ImageLoader, ImageLoaderConfig } from '@angular/common'; // The origin of the CDN we are going to use to pull images from. const base = 'https://images.unsplash.com'; export const myCustomLoader: ImageLoader = (config: ImageLoaderConfig) => { // Join the value that the user put in the `ngSrc` attribute and the CDN base into a single URL. const url = new URL(config.src, base); if (config.width) { url.searchParams.set('w', config.width.toString()); } if (config.loaderParams?.['compression']) { url.searchParams.set('q', config.loaderParams['compression']); } return url.toString(); };
Разберем, что происходит здесь:
- Объединяем
src
изображения (обычно это ключ, ссылка на изображение, без домена) и домен CDN. - Если для этого изображения настроена ширина, установим параметр запроса
w
(w - это параметр запроса для ширины в Imgix). - Если требуется изменить уровень сжатия через параметры загрузчика (это дополнительные свойства, которые можно передать в директиву и которые передаются в пользовательские загрузчики изображений), установим параметр запроса сжатия
q
Imgix. - Результат преобразуем в строку и вернем обратно.
Чтобы зарегистрировать этот загрузчик в приложении, вместо объявления provideImgixLoader()
в корне, переопределим провайдер на уровне приложения:
import { ApplicationConfig } from '@angular/core'; import { appRoutes } from './app.routes'; import { IMAGE_LOADER } from '@angular/common'; export const appConfig: ApplicationConfig = { providers: [ // ...other providers { provide: IMAGE_LOADER, useValue: myCustomLoader, }, ], };
<img [ngSrc]="'photo-1417325384643-aac51acc9e5d'" [loaderParams]="{ compression: 50 }" ngSrcset="300w, 800w, 1500w" />
В результате к каждому URL изображения в srcset
в теге <img>
после рендеринга будет добавлен параметр запроса q
(сжатие):
<img fill="" priority="" style="object-fit: cover; position: absolute; width: 100%; height: 100%; inset: 0px;" loading="eager" fetchpriority="high" src="https://images.unsplash.com/photo-1417325384643-aac51acc9e5d?q=50" srcset=" https://images.unsplash.com/photo-1417325384643-aac51acc9e5d?w=300&q=50 300w, https://images.unsplash.com/photo-1417325384643-aac51acc9e5d?w=800&q=50 800w, https://images.unsplash.com/photo-1417325384643-aac51acc9e5d?w=1500&q=50 1500w " sizes="100vw" />
Что в итоге?
Теперь, когда настроено окружение и приложение посмотрим на результаты. Слева изображение, использующее загрузчик, ngSrcset
, preconnnect
и все остальные хорошие вещи, которыми автоматически управляет директива Image
. Справа - то же самое изображение, но без использования средств оптимизации.
| | Raw | Optimized | Change | | ----------— | ----— | -------— | ----— | | Size | 3.7MB | 1.0MB | -73% | | Loading Time | 1300ms | 452ms | -65% |
Исходный код всего компонента:
import { Component } from '@angular/core'; import { RouterModule } from '@angular/router'; import { NgOptimizedImage } from '@angular/common'; @Component({ standalone: true, imports: [RouterModule, NgOptimizedImage], selector: 'ngconf-image-directive-article-root', template: ` <div class="container"> <!-- Image on the left, all the optimizations --> <div class="image"> <img [ngSrc]="'photo-1417325384643-aac51acc9e5d'" [loaderParams]="{ compression: 50 }" fill priority ngSrcset="300w, 800w, 1500w" /> </div> <!-- Image on the right, no optimizations --> <div class="image"> <img style="height: 100vh; width: 50vw" [src]="'https://images.unsplash.com/photo-1417325384643-aac51acc9e5d'" /> </div> </div> `, styles: [ ` .container { display: flex; height: 100%; width: 100%; } .image { flex: 1; height: 100vh; width: 50vw; position: relative; object-fit: cover; } `, ], }) export class AppComponent {}
Подытожим, что именно делает директива NgOptimizedImage
1. Добавляет preconnect
ссылок в <head>: подготавливает браузер к быстрой загрузке изображений.
2. Добавляет тег preload
для изображений помеченных как приоритетные, чтобы загрузка началась сразу после получения браузером index.html
приложения Angular (до того, как Angular даже начнет работу).
3. Заполняет изображения контейнером-родителем, чтобы не приходилось беспокоиться о установке ширины и высоты.
4. Создает srcset
, чтобы браузер мог выбрать наиболее оптимальное изображение.
5. Управляет атрибутом priority
, чтобы гарантировать, что браузер загружает приоритетные изображения в нужное время (loading
и fetchpriority
).
Это вольный перевод материала