Lazy loading
Lazy loading (ленивая загрузка) — это стратегия, направленная на определение ресурсов как неблокирующих (не критических) для того, чтобы отложить загрузку этих ресурсов на тот момент, когда они действительно необходимы. Так можно сократить длину критических этапов рендеринга, что приводит к уменьшению времени загрузки приложения.
Ленивая загрузка может происходить в разные моменты работы приложения, но, как правило, она запускается во время взаимодействия пользователя и системы, например, при скроллинге или навигации.
Обзор
Вместе с ростом web-приложений драматически вырос объем и размеры ресурсов, отправляемых клиентскому приложению. С 2011 по 2019 медианный рост размеров ресурсов вырос с
400KB для настольных компьютеров и с
350KB для мобильных. А размер изображений вырос с
900KB для настольных компьютеров и со
850KB для мобильных.
Очевидно, что такое повышение объёмов способствует увеличению длительности загрузки приложения. Один из способов её сократить — это отложить загрузку ресурсов, которые не являются критически важными для приложения. Например, вы посещаете приложение интернет-магазина, которое состоит из списка товаров и корзины. Очевидно, что вам не нужны изображения товаров, которые сейчас находится за пределами экрана; очевидно так же, что вам не нужно грузить все данные о содержимом корзины до тех пор, пока пользователь не перешёл к ней.
Стратегии
Ленивая загрузка (Lazy loading) может применяться к разным ресурсам и разными подходами.
Общий подход
Разделение кода (code splitting) JavaScript, CSS и HTML могут быть разделены на небольшие части, называемые чанками (chunks). При первоначальной загрузке приложения вы можете отправлять не цельное приложение, а только необходимые части, например, только каркас разметки. Данные для заполнения этого каркаса могут быть подгружены позже, например, с помощью AJAX. Есть два вида разделения кода:
- Разделение по точкам входа (entrypoint)
- Динамическое (dynamic import())
JavaScript
Указание типа «module» Любой тег скрипта с type=»module» рассматривается как JavaScript module, а его загрузка откладывается по умолчанию.
По умолчанию CSS считается ресурсом, блокирующим рендеринг (render blocking). Это означает, что браузер не будет отображать контент до тех пор, пока не будет построена объектная модель CSS (CSSOM). Поэтому CSS-файл должен быть небольшим, чтобы он был доставлен так быстро, насколько это возможно. Рекомендуется использовать медиавыражения, для того чтобы вместо одного монолитного CSS-файла грузить специализированные.
Также в целях ускорения CSS можно применять оптимизации (CSS optimizations (en-US) ).
Шрифты
По умолчанию, загрузка шрифтов откладывается на тот момент, пока дерево рендера (render tree) не сформировано полностью. Это приводит к тому, что текст страницы может появиться не сразу.
Вы можете переопределить такое поведение и загрузить шрифты заранее, используя <link rel=»preload»> , CSS font-display свойство или Font Loading API (en-US) .
Изображения
Очень часто веб-страницы содержат множество изображений, загрузка которых заметно нагружает канал передачи данных и увеличивает длительность загрузки. Подавляющая часть этих изображений находятся за видимой областью экрана и не являются необходимым (non-critical), а для взаимодействия с пользователем требуют действия (например, прокрутки до них).
Атрибут Loading Атрибут loading элемента <img> (или loading (en-US) атрибут для <iframe> (en-US)) могут быть использованы, чтобы указать браузеру на необходимость отложить загрузку изображений / iframe до тех пор, пока пользователь не доскроллит до них.
Событие load запускается, когда все другие необходимые ресурсы были загружены. В это время, возможно (или даже наиболее вероятно), что изображения не будут загружены, даже если пользователь доскроллит до изображений и они будут в visual viewport (en-US).
Вы можете определить, было ли загружено то или иное изображение, проверив Boolean значение complete (en-US).
Полифил Для использованиях в браузерах, которые не поддерживают данную технологию, рекомендуется использовать полифил: loading-attribute-polyfill
Intersection Observer API Intersection Observers позволяют вам узнать, как наблюдаемый вами элемент входит или выходит из зоны видимости браузера (viewport).
Обработчики событий (Event handlers) Intersection Observer API — относительно молодая технология, которая может не поддерживаться некоторыми устаревшими браузерами. Если поддержка браузеров важна для вас, есть несколько способов получить её:
Руководство: как увеличить скорость загрузки страницы со скриптами с помощью defer и async
JS-скрипты, неудачно расположенные в HTML-коде, могут значительно снизить скорость загрузки страницы. Разберемся, как повысить скорость загрузки в старых версиях браузеров и как правильно использовать async и defer , которые поддерживаются в новых версиях.
Это адаптированный перевод статьи Efficiently load JavaScript with defer and async из блога проекта flaviocopes. Повествование ведется от лица автора оригинала.
Расположение имеет значение
Стандартный способ встраивания скрипта в HTML-код страницы выглядит так:
Каждый раз, когда встретится такая или похожая строка, будет выполнен запрос на получение данных файла, а парсер продолжит свою работу после выполнения скрипта.
Классический подход к обучению HTML предполагает, что теги скрипта должны находиться в <head> :
Однако такой подход приводит к задержкам при загрузке страницы. Когда анализатор доходит до строки со скриптом, он на время останавливается для его извлечения и выполнения, и только после этого переходит к разбору <body> .
Распространенное решение проблемы — перенос скрипта в нижнюю часть страницы, перед закрывающим тегом <body> . В этом случае скрипт выполняется после того, как вся страница уже проанализирована до тега.
Это лучшее решение по ускорению загрузки страницы для старых браузеров, которые не поддерживают атрибуты async и defer . О последних поговорим отдельно.
async и defer
Прежде чем начать, стоит уточнить, что использование обоих атрибутов накладывает некоторые ограничения, а приведенное ниже руководство по времени загрузки — не исчерпывающее.
Синтаксически и async и defer — булевые атрибуты, которые используются следующим образом:
Если в коде есть оба атрибута, async имеет приоритет и выполняется в первую очередь в современных версиях браузеров. В старых версиях, напротив, приоритет будет отдан defer .
Проверить совместимость атрибутов с разными версиями браузеров можно по этим таблицам: раз и два
Важно отметить, что оба атрибута стоит использовать только в верхней части страницы (в <head> ): перенос в <body> делает их совершенно бесполезными.
Производительность
Если async и defer отсутствуют в <head>
Синтаксический анализатор прекращает работу до тех пор, пока скрипт не будет выполнен. Как только этот процесс завершится, анализ продолжится.
Читайте также: Как сохранять фокус на протяжении всего обучения: советы от Хекслета
Если async и defer отсутствуют в <body>
Парсинг выполняется без пауз: сразу по его завершению загружается и выполняется скрипт. Синтаксический анализ выполняется еще до загрузки скрипта, поэтому страница загружается быстрее, чем в предыдущем случае.
Если async находится в <head>
Сценарий загружается асинхронно, а синтаксический анализатор приостанавливает работу на время его выполнения.
Если defer находится в <head>
Скрипт извлекается асинхронно и выполняется только после завершения анализа HTML.
Парсинг проходит с той же скоростью, как если бы скрипт находился в конце тега body , но в целом выполнение скрипта завершается намного раньше, поскольку он загружается параллельно с парсингом HTML. Таким образом этот вариант — наиболее выигрышный с точки зрения скорости загрузки страницы.
Блокировка синтаксического анализа
async приостанавливает синтаксический анализ страницы, а defer — нет.
Блокировка рендеринга
Ни async , ни defer не блокируют рендеринг — этот процесс полностью зависит от кода на странице. Поэтому важно убедиться, что сценарии запускаются после события onLoad .
domInteractive
Скрипты defer выполняются сразу после события domInteractive . Последнее, в свою очередь, происходит после загрузки, анализа и построения DOM HTML.
СSS и изображения на этом этапе еще не проанализированы и не загружены: как только это произойдет, браузер сначала выдаст событие domComplete , а затем — onLoad .
Порядок выполнения
Еще один аргумент за использование defer — скрипты, помеченные как async , выполняются в случайном порядке, тогда как скрипты с defer — в строго определенном.
Как ускорить загрузку страницы
Лучший способ — прописать скрипты в <head> и добавить атрибут defer в тег script . Этот сценарий быстро запускает событие domInteractive :
Никогда не останавливайтесь: В программировании говорят, что нужно постоянно учиться даже для того, чтобы просто находиться на месте. Развивайтесь с нами — на Хекслете есть сотни курсов по разработке на разных языках и технологиях
Эффективная загрузка стороннего кода JavaScript
Избегайте распространенных ошибок, связанных с использованием сторонних скриптов, чтобы уменьшить время загрузки и повысить удобство для пользователей.
Удалить скрипт, если он не приносит явную пользу сайту.
Оптимизировать процесс загрузки.
Использование атрибутов async и defer в тегах <script>
Заблаговременное создание подключений к требуемым источникам
Оптимизация передачи сторонних скриптов
Использование атрибутов async и defer #
Так как синхронные скрипты задерживают создание DOM и рендеринг, следует всегда загружать сторонние скрипты асинхронно за исключением случаев, когда нужно запускать скрипт до рендеринга страницы.
Атрибуты async и defer сообщают браузеру, что он может продолжить синтаксический анализ HTML во время загрузки скрипта в фоновом режиме и выполнить этот скрипт по окончании его загрузки. Таким образом, загрузка скрипта не будет мешать создавать модель DOM и выполнять рендеринг страницы. В результате можно будет отобразить страницу для пользователя еще до завершения загрузки всех скриптов.
Разница между атрибутами async и defer появляется тогда, когда дело доходит до выполнения скриптов.
async #
Скрипты с атрибутом async выполняются при первой возможности после завершения их загрузки и до появления события load окна. Соответственно, возможно (и вероятно), скрипты с атрибутом async не будут выполняться в том порядке, в котором они появляются в HTML. Это также означает, что они могут прервать процесс создания DOM, если их загрузка будет завершена во время работы средства синтаксического анализа.
defer #
Скрипты с атрибутом defer выполняются после полного завершения синтаксического анализа HTML, но до появления события DOMContentLoaded . При использовании атрибута defer скрипты будут гарантированно выполняться в том порядке, в котором они появляются в HTML, и не будут блокировать работу средства синтаксического анализа.
Используйте атрибут async , если важно, чтобы скрипт выполнялся на ранних этапах процесса загрузки.
Используйте атрибут defer для менее важных ресурсов. Например, для видеоплеера в части страницы, расположенной за пределами экрана.
Используя эти атрибуты, можно значительно ускорить загрузку страницы. Например, недавно на веб-сайте Telegraph стали отложенными все скрипты, в том числе связанные с рекламными объявлениями и аналитикой, после чего время загрузки рекламных объявлений уменьшилось в среднем на четыре секунды.
Создание заблаговременных подключений к необходимым источникам #
Вы можете сэкономить 100–500 мс, заблаговременно создавая подключения к важным сторонним источникам.
preconnect #
Атрибут <link rel=»preconnect»> сообщает браузеру, что страница намеревается создать подключение к другому источнику и что нужно начать этот процесс как можно скорее. При создании запроса на получение ресурса из предварительно подключенного источника загрузка начинается немедленно.
Внимание
dns-prefetch #
Атрибут <link rel preconnect»> . При создании подключения выполняется поиск DNS и подтверждение TCP, а для безопасных источников — согласование TLS. Атрибут dns-prefetch сообщает браузеру, что нужно только сопоставлять DNS определенного домена до совершения явного вызова к нему.
Атрибут preconnect лучше всего использовать только для самых важных подключений; для менее важных сторонних доменов используйте атрибут <link rel=dns-prefetch> .
Браузеры поддерживают атрибут dns-prefetch немного по-другому, чем атрибут preconnect , поэтому атрибут dns-prefetch можно использовать в качестве резервного варианта для браузеров, которые не поддерживают атрибут preconnect . Для безопасной реализации этого подхода используйте отдельные теги ссылок:
Отложенная загрузка сторонних ресурсов #
Если внедренные сторонние ресурсы плохо разработаны, они могут значительно снижать скорость загрузки страниц. Если они не являются критически важными или находятся за пределами экрана (то есть если пользователям нужно прокрутить страницу для их просмотра), отложенная загрузка — хороший способ повысить скорость работы страницы и улучшить метрики отображения содержимого. Таким образом можно повысить удобство для пользователей, так как они будут быстрее получать содержимое главной страницы.
Один из эффективных методов — отложенная загрузка стороннего контента после загрузки содержимого главной страницы. Этот метод хорошо подходит для рекламных объявлений.
Рекламные объявления — важный источник дохода для многих сайтов, но пользователи приходят за контентом. За счет отложенной загрузки рекламных объявлений и более быстрой доставки основного контента вы можете увеличить общий процент просмотра объявления. Например, сайт MediaVine перешел на отложенную загрузку рекламных объявлений, после чего скорость загрузки страниц сайта увеличилась на 200%. В официальной документации DoubleClick есть руководство по отложенной загрузке объявлений.
Альтернативный метод — загружать сторонний контент только тогда, когда пользователи прокручивают страницу вниз до соответствующего раздела.
Intersection Observer — это API браузера, который эффективно определяет, когда элемент входит в окно просмотра браузера и выходит из него. Его можно использовать для реализации этого метода. lazysizes — это популярная библиотека JavaScript для отложенной загрузки изображений и элементов iframe . Она поддерживает внедренные элементы YouTube и виджеты. В ней также имеется дополнительная поддержка API IntersectionObserver.
Внимание
Использование атрибута loading для отложенной загрузки изображений и элементов iframe — отличная альтернатива методам с использованием JavaScript, и недавно этот атрибут стал доступен в Chrome 76.
Оптимизация передачи сторонних скриптов #
Размещение скриптов в сторонних CDN #
Сторонние поставщики часто предоставляют URL-адреса размещаемых у них файлов JavaScript (обычно они размещены в сетях доставки контента [CDN]). Преимущество такого подхода заключается в том, что вы можете быстро начать использовать скрипты — достаточно скопировать и вставить URL-адрес. При этом у вас не будет дополнительных затрат на обслуживание. Сторонний поставщик выполняет настройку сервера и обновляет скрипты.
Но так как скрипты не находятся в том же источнике, что и остальные ваши ресурсы, для загрузки файлов из общедоступных CDN требуются затраты на передачу данных по сети. Браузеру необходимо выполнить поиск в DNS, создать подключение HTTP и (при использовании защищенных источников) выполнить подтверждение SSL для сервера поставщика.
Когда вы используете файлы со сторонних серверов, вы можете контролировать кеширование только в редких случаях. Использование чужой стратегии кэширования может привести к слишком частому повторному получению скриптов по сети.
Размещение сторонних скриптов на собственном ресурсе #
- Уменьшение времени поиска DNS и кругового пути.
- Улучшение HTTP-кэширования заголовков.
- Использование операций push сервера HTTP/2.
Например, веб-сайту Casper удалось уменьшить время загрузки на 1,7 секунды, разместив скрипт A/B-тестирования на собственном ресурсе.
Тем не менее у размещения скриптов на собственных ресурсах есть один большой недостаток: скрипты могут устаревать и не будут получать автоматические обновления (при изменении API) или исправления для системы безопасности.
Внимание
Использование служебных сценариев для кеширования скриптов со сторонних серверов #
Альтернатива размещению скриптов на собственных ресурсах, которая позволяет лучше управлять кешированием и при этом пользоваться преимуществами сторонних CDN, — использование служебных сценариев для кэширования скриптов со сторонних серверов. Вы сможете управлять периодичностью повторной загрузки скриптов из сети и создать стратегию загрузки, с помощью которой можно будет ограничивать запросы к несущественным сторонним ресурсам до тех пор, пока страница не достигнет ключевого для пользователя момента. В этом случае можно в некоторой степени уменьшить расходы на передачу данных по сети, создавая заблаговременные подключения с использованием атрибута preconnect .
Скрипты: async, defer
В современных сайтах скрипты обычно «тяжелее», чем HTML: они весят больше, дольше обрабатываются.
Когда браузер загружает HTML и доходит до тега <script>. </script> , он не может продолжать строить DOM. Он должен сначала выполнить скрипт. То же самое происходит и с внешними скриптами <script src=". "></script> : браузер должен подождать, пока загрузится скрипт, выполнить его, и только затем обработать остальную страницу.
Это ведёт к двум важным проблемам:
- Скрипты не видят DOM-элементы ниже себя, поэтому к ним нельзя добавить обработчики и т.д.
- Если вверху страницы объёмный скрипт, он «блокирует» страницу. Пользователи не видят содержимое страницы, пока он не загрузится и не запустится:
Конечно, есть пути, как это обойти. Например, мы можем поместить скрипт внизу страницы. Тогда он сможет видеть элементы над ним и не будет препятствовать отображению содержимого страницы:
Но это решение далеко от идеального. Например, браузер замечает скрипт (и может начать загружать его) только после того, как он полностью загрузил HTML-документ. В случае с длинными HTML-страницами это может создать заметную задержку.
Такие вещи незаметны людям, у кого очень быстрое соединение, но много кто в мире имеет медленное подключение к интернету или использует не такой хороший мобильный интернет.
К счастью, есть два атрибута тега <script> , которые решают нашу проблему: defer и async .
defer
Атрибут defer сообщает браузеру, что он должен продолжать обрабатывать страницу и загружать скрипт в фоновом режиме, а затем запустить этот скрипт, когда DOM дерево будет полностью построено.
Вот тот же пример, что и выше, но с defer :
- Скрипты с defer никогда не блокируют страницу.
- Скрипты с defer всегда выполняются, когда дерево DOM готово, но до события DOMContentLoaded .
Следующий пример это показывает:
- Содержимое страницы отобразится мгновенно.
- Событие DOMContentLoaded подождёт отложенный скрипт. Оно будет сгенерировано, только когда скрипт (2) будет загружен и выполнен.
Отложенные с помощью defer скрипты сохраняют порядок относительно друг друга, как и обычные скрипты.
Поэтому, если сначала загружается большой скрипт, а затем меньшего размера, то последний будет ждать.
Браузеры сканируют страницу на предмет скриптов и загружают их параллельно в целях увеличения производительности. Поэтому и в примере выше оба скрипта скачиваются параллельно. small.js скорее всего загрузится первым.
Но спецификация требует последовательного выполнения скриптов согласно порядку в документе, поэтому он подождёт выполнения long.js .
Атрибут defer будет проигнорирован, если в теге <script> нет src .
async
Атрибут async означает, что скрипт абсолютно независим:
- Страница не ждёт асинхронных скриптов, содержимое обрабатывается и отображается.
- Событие DOMContentLoaded и асинхронные скрипты не ждут друг друга:
- DOMContentLoaded может произойти как до асинхронного скрипта (если асинхронный скрипт завершит загрузку после того, как страница будет готова),
- …так и после асинхронного скрипта (если он короткий или уже содержится в HTTP-кеше)
Так что если у нас есть несколько скриптов с async , они могут выполняться в любом порядке. То, что первое загрузится – запустится в первую очередь:
- Содержимое страницы отображается сразу же : async его не блокирует.
- DOMContentLoaded может произойти как до, так и после async , никаких гарантий нет.
- Асинхронные скрипты не ждут друг друга. Меньший скрипт small.js идёт вторым, но скорее всего загрузится раньше long.js , поэтому и запустится первым. То есть, скрипты выполняются в порядке загрузки.
Асинхронные скрипты очень полезны для добавления на страницу сторонних скриптов: счётчиков, рекламы и т.д. Они не зависят от наших скриптов, и мы тоже не должны ждать их:
Динамически загружаемые скрипты
Мы можем также добавить скрипт и динамически, с помощью JavaScript:
Скрипт начнёт загружаться, как только он будет добавлен в документ (*) .
Динамически загружаемые скрипты по умолчанию ведут себя как «async».
- Они никого не ждут, и их никто не ждёт.
- Скрипт, который загружается первым – запускается первым (в порядке загрузки).
Мы можем изменить относительный порядок скриптов с «первый загрузился – первый выполнился» на порядок, в котором они идут в документе (как в обычных скриптах) с помощью явной установки свойства async в false :
Например, здесь мы добавляем два скрипта. Без script.async=false они запускались бы в порядке загрузки ( small.js скорее всего запустился бы раньше). Но с этим флагом порядок будет как в документе:
Итого
У async и defer есть кое-что общее: они не блокируют отрисовку страницы. Так что пользователь может просмотреть содержимое страницы и ознакомиться с ней сразу же.
Но есть и значимые различия:
Порядок DOMContentLoaded async Порядок загрузки (кто загрузится первым, тот и сработает). Не имеет значения. Может загрузиться и выполниться до того, как страница полностью загрузится. Такое случается, если скрипты маленькие или хранятся в кеше, а документ достаточно большой. defer Порядок документа (как расположены в документе). Выполняется после того, как документ загружен и обработан (ждёт), непосредственно перед DOMContentLoaded . Пожалуйста, помните, что когда вы используете defer , страница видна до того, как скрипт загрузится.
Пользователь может знакомиться с содержимым страницы, читать её, но графические компоненты пока отключены.
Поэтому обязательно должна быть индикация загрузки, нерабочие кнопки – отключены с помощью CSS или другим образом. Чтобы пользователь явно видел, что уже готово, а что пока нет.
На практике defer используется для скриптов, которым требуется доступ ко всему DOM и/или важен их относительный порядок выполнения.
А async хорош для независимых скриптов, например счётчиков и рекламы, относительный порядок выполнения которых не играет роли.