- JavaScript: предзагрузка изображений, упрощаю код
- Используем предварительную загрузку изображений
- Различные варианты использования предварительной загрузки
- Hero-изображение
- Загрузка WebP
- Предварительная загрузка scrSet
- Предварительная загрузка JSON
- Предварительная загрузка запросов
- Предварительное подключение
- Предварительная загрузка JavaScript
- В заключении
- Полезно к прочтению
JavaScript: предзагрузка изображений, упрощаю код
Для чего я создал массив imgs ? Я сохраняю в нем ссылки на HTML-элементы img , которые нужны лишь для предзагрузки заданных изображений. Эти HTML-элементы не добавляются на HTML-страницу.
Я полагал, что эти HTML-элементы img нужно временно где-то хранить, пока изображения не предзагрузятся в кэш браузера полностью и пока не исполнится функция-обработчик onLoad , повешенная на события load и error каждого временно созданного HTML-элемента img . Я думал, что, если ссылки на эти HTML-элементы нигде не сохранить, то они будут стерты сборщиком мусора и это нарушит работу моей функции.
Однако, как оказалось, такое сохранение временных объектов img в данном случае не требуется.
1) Предзагрузка изображения браузером запускается при записи адреса изображения в свойство src временного объекта img . После этой записи с самим объектом img можно, как я понимаю, делать всё, что угодно, хоть попытаться его затереть. Предзагрузка изображения в кэш браузера всё равно будет выполнена. Чтобы это проверить, я написал такой код:
let img = document.createElement("img"); img.src = "https://en.js.cx/images-load/1.jpg" + "?" + Math.random(); img = "";
Здесь в первой строке создан временный объект, ссылка на который записана в переменную img . Во второй строке записываем адрес изображения в свойство src объекта. Это заставляет браузер запустить загрузку изображения в кэш. В третьей строке затираем ссылку на объект, созданный в первой строке, значением «» . Как я понимаю, после этого сборщик мусора должен стереть объект, созданный в первой строке, из памяти, так как на этот объект больше нет ссылки из нашего скрипта. При этом загрузка изображения в кэш браузера заканчивается благополучно, я проверил с помощью инструмента разработчика «Network» в моём браузере (у меня — «Microsoft Edge» на движке «Chromium»).
2) А как тогда будет срабатывать функция-обработчик onLoad ? Ведь, по идее, на момент, когда она будет запущена, локальная переменная img уже, скорее всего, не будет существовать (каждая ее версия существует только в лексическом окружении своей итерации цикла for ).
Тут, как я понимаю, сработает то, что функция-обработчик onLoad является замыканием, то есть помнит значения переменных в окружающем ее коде. Благодаря этой своей способности каждая функция onLoad помнит ссылку на свой (своей итерации) объект img , поэтому (по идее) сборщик мусора не будет удалять этот объект из памяти.
Ссылки на сами функции onLoad , полагаю, хранятся в неких списках (массивах), создаваемых браузером, куда они попадают при регистрации посредством метода addEventListener .
Таким образом, массив imgs из кода можно убрать и функцию упростить:
function preloadImages(sources, callback) < let loaded = []; // массив HTML-элементов img с загруженной картинкой // цикл выполнения предзагрузки заданных картинок for (let i = 0; i < sources.length; i++) < // запуск предзагрузки очередной картинки let img = document.createElement("img"); img.src = sources[i]; // после окончания предзагрузки картинки поместить ее в массив загруженных, // и проверить, если это была последняя из заданных картинок, то запустить // функцию callback img.addEventListener("load", onLoad); img.addEventListener("error", onLoad); function onLoad() < loaded.push(this); if (loaded.length == sources.length) callback(); >> >
Я проверил, эта версия кода работает так же, как и предыдущая версия.
Массив loaded тоже можно убрать, заменив на переменную-счетчик. Но от массива loaded может быть польза: HTML-элементы img в него записываются в том порядке, в котором заканчивается предзагрузка соответствующих изображений.
Если такая информация не нужна, то заменяем массив loaded на переменную-счетчик loaded . В результате код функции становится еще проще:
function preloadImages(sources, callback) < let loaded = 0; // счетчик предзагруженных картинок // цикл выполнения предзагрузки заданных картинок for (let i = 0; i < sources.length; i++) < // запуск предзагрузки очередной картинки let img = document.createElement("img"); img.src = sources[i]; // после окончания предзагрузки картинки увеличить значение счетчика, // и проверить, если это была последняя из заданных картинок, то запустить // функцию callback img.addEventListener("load", onLoad); img.addEventListener("error", onLoad); function onLoad() < loaded++; if (loaded == sources.length) callback(); >> >
Этот вариант кода очень близок к варианту решения от авторов задачи.
* * *
Дополнение от 8 октября 2021 года. Логично будет вынести функцию onLoad за пределы цикла. Это не уменьшит длину скрипта, зато, по идее, уменьшит нагрузку на память: мы объявим функцию один раз за пределами цикла, а не будем объявлять ее в каждой итерации цикла. Меняем код:
function preloadImages(sources, callback) < let loaded = 0; // счетчик предзагруженных картинок // после окончания предзагрузки картинки увеличить значение счетчика, // и проверить, если это была последняя из заданных картинок, то запустить // функцию callback function onLoad() < loaded++; if (loaded == sources.length) callback(); >// цикл выполнения предзагрузки заданных картинок for (let i = 0; i < sources.length; i++) < // запуск предзагрузки очередной картинки let img = document.createElement("img"); img.src = sources[i]; // запланируем запуск функции onLoad после предзагрузки картинки img.addEventListener("load", onLoad); img.addEventListener("error", onLoad); > >
Этот вариант кода выдает в тесте такой же результат, как и предыдущий вариант кода.
Однако, работает он несколько по-другому. В предыдущем варианте кода в замыкание функции onLoad попадала переменная img , создаваемая в соответствующей итерации цикла. Из-за этого функция onLoad могла видеть эту переменную, несмотря на то, что к моменту запуска функции onLoad соответствующая итерация цикла была закончена (я это проверил) и переменная img должна была быть уничтожена сборщиком мусора.
В текущем на данный момент поста варианте кода функция onLoad находится вне цикла и переменная img уже не попадает в замыкание функции onLoad . Поэтому функция onLoad не видит переменную img , но всё-таки может получить ссылку на объект, ссылка на который хранилась в этой переменной. Я проверил это, внеся следующие изменения в функцию onLoad :
function onLoad() < console.log(img); // ошибка console.log(this); // нет ошибки loaded++; if (loaded == sources.length) callback(); >
В рабочем коде, конечно, следует оставить только один из этих двух вызовов функции console.log , а другой закомментировать. И наоборот.
Как так получилось, что функция onLoad всё же видит объект, созданный в закончившейся на момент запуска функции onLoad итерации цикла? (Она видит его посредством ключевого слова this .) Я полагаю, что в момент регистрации функции onLoad в качестве функции-обработчика некоего события с помощью метода addEventListener в список (массив) регистрации записывается не только ссылка на саму функцию, но и ссылка на объект, с которым связано целевое событие. Поэтому эту ссылку на объект позже можно получить с помощью ключевого слова this .
Используем предварительную загрузку изображений
Предварительная загрузка позволяет вам как можно скорее информировать браузер о важных данных, которые вы хотите загрузить, до того, как они будут найдены в HTML. Если вы оптимизируете Largest Contentful Paint (LCP), то предварительная загрузка может значительно увеличить приоритет для изображений или ресурсов, которые запрашиваются через JavaScript.
Предварительная загрузка может значительно улучить LCP, особенно, если вам нужно загружать Hero-изображения раньше, чем остальные. В то время как браузер изо всех сил старается определить приоритет для загрузки в видимой области, мы можем значительно помочь ему, используя .
Изображения, которым отдается меньший приоритет:
- Изображения, которые загружаются с помощью JavaScript из локального источника.
- React, Vue или Angular компоненты, загружающие тэг на клиентской стороне (CSR).
- CSR, отвечающий за загрузку изображений.
Предварительная загрузка может существенно ускорить отображение изображений. Ключевой идеей будет то, что вы сможете избежать тот момент, когда браузеру придется дожидаться исполнения скрипта загрузки, а ведь это может значительно отсрочить показ нужного изображения пользователю.
Я использую во многих своих одностраничных приложениях для оптимизации Core Web Vitals. Особенно для ускорения загрузки основных изображений в видимую область браузера.
На картинке выше, показан пример аналитики с сайта WebPageTest, на которую было отправлено простое веб-приложение на React. Приложение использует CSR (client-side rendering), а также делает запрос к API для получения списка фильмов в формате JSON (movies.json). Это означает, что браузеру потребуется обработать app.js, до того, как он начнет загружать movies.json и только потом определит Hero-изображение (poster.jpg), исходя из movies.json.
Используя предварительную загрузку для Hero-изображения, Largest Contentful Paint (на картинке выделено оранжевой рамкой) выполняется на 1 секунду быстрее при 4G. Для пользователя сайт выглядит более производительным, поскольку меньше времени тратится на ожидание появления контента в области просмотра.
Различные варианты использования предварительной загрузки
Теперь рассмотрим другие варианты использования .
Hero-изображение
Предварительно загружаем Hero-изображение, чтобы оно было обнаружено до того, как JS выведет .
Загрузка WebP
Так как теперь поддержка WebP браузерами была улучшена, то вы можете также передзагружать WebP изображения:
Предварительная загрузка scrSet
С помощью предварительной загрузки вы можете загрузить адаптивные изображения с srcset
Предварительная загрузка JSON
Помимо изображений, предварительная загрузка доступна также и для JSON.
Предварительная загрузка запросов
В моем случае, movies.json требует cross-origin запрос, который вы также можете предварительно запустить, если используете:
Предварительное подключение
Дополнительно, вы так же можете предварительно подключится к хосту, используя
Предварительная загрузка JavaScript
В дополнение ко всему вышеперечисленному, вы так же можете использовать предварительную загрузку для JavaScript
В заключении
Предварительная загрузка позволяет гарантировать, что важные изображения и ресурсы будут показаны пользователям как можно скорее. Чтобы узнать, действительно ли она улучшит производительность вашего приложения, вы можете воспользоваться Lighthouse или PageSpeed Insights.
Полезно к прочтению