Ленивая загрузка (Lazy Loading) — это подход к загрузке ресурсов, при котором данные, модули, изображения или другие элементы загружаются не во время первоначального открытия страницы, а только в момент фактической необходимости. Главная цель такого механизма заключается в уменьшении объёма первоначально загружаемого кода и ускорении отображения интерфейса.
В современных веб-приложениях значительная часть функциональности может использоваться лишь в отдельных сценариях. Например:
Если весь этот код и все ресурсы загружать сразу, увеличивается размер начального пакета, растёт время загрузки страницы и ухудшается пользовательский опыт.
Rollup поддерживает ленивую загрузку через механизм динамического импорта и автоматическое разделение кода на чанки.
Рассмотрим типичный проект:
import './analytics.js';
import './admin.js';
import './settings.js';
import './charts.js';
import './reports.js';
startApplication();
Во время сборки Rollup объединит все зависимости в единый набор файлов.
Даже если пользователь никогда не откроет раздел отчётов, код:
import './reports.js';
всё равно окажется в начальной загрузке.
При большом количестве функциональности подобный подход приводит к следующим последствиям:
Ленивая загрузка решает эту проблему путём переноса части модулей в отдельные чанки.
Стандарт ECMAScript предоставляет специальный синтаксис:
import('./module.js');
В отличие от обычного импорта:
import { fn } from './module.js';
динамический импорт выполняется во время работы приложения.
Результатом является Promise:
import('./module.js')
.then(module => {
module.fn();
});
или более современный вариант:
const module = await import('./module.js');
module.fn();
Для Rollup такой вызов является сигналом, что модуль необходимо вынести в отдельный чанк.
Основной файл приложения:
const button = document.querySelector('#admin');
button.addEventListener('click', async () => {
const admin = await import('./admin.js');
admin.openPanel();
});
Файл модуля:
export function openPanel() {
console.log('Admin panel');
}
После сборки структура может выглядеть следующим образом:
dist/
├── main.js
├── admin-8f34ab.js
При открытии страницы браузер загрузит только:
main.js
Файл:
admin-8f34ab.js
будет загружен только после нажатия на кнопку.
При обнаружении конструкции:
await import('./feature.js');
Rollup выполняет несколько действий:
Исходный код:
await import('./feature.js');
после сборки остаётся динамическим импортом, но уже указывает на сгенерированный файл:
await import('./feature-a12f7b.js');
Браузер самостоятельно выполняет сетевой запрос и загружает новый модуль.
Один из наиболее распространённых сценариев — загрузка страниц по маршрутам.
Пример простого роутера:
async function navigate(route) {
switch (route) {
case '/':
const home = await import('./pages/home.js');
home.render();
break;
case '/profile':
const profile = await import('./pages/profile.js');
profile.render();
break;
case '/settings':
const settings = await import('./pages/settings.js');
settings.render();
break;
}
}
Каждая страница становится отдельным чанком.
Структура результата:
main.js
home.js
profile.js
settings.js
Пользователь загружает только те части приложения, которые действительно посещает.
Модальные окна часто используются редко, но содержат значительный объём кода.
Без ленивой загрузки:
import { showModal } from './modal.js';
С ленивой загрузкой:
button.addEventListener('click', async () => {
const modal = await import('./modal.js');
modal.showModal();
});
Особенно заметен эффект в случаях, когда модальное окно содержит:
Некоторые зависимости могут занимать сотни килобайт.
Например:
import Chart from 'chart.js';
или:
import Editor from 'some-editor';
Если функциональность используется редко, разумнее загружать её по требованию.
Пример:
async function loadChart() {
const chartModule = await import('chart.js');
return chartModule.default;
}
После сборки Rollup создаст отдельный чанк для библиотеки.
В результате:
Часто модуль требуется только после конкретного события.
Например:
button.addEventListener('click', async () => {
const exportModule =
await import('./export.js');
exportModule.exportToPdf();
});
До нажатия кнопки файл:
export.js
вообще не участвует в работе страницы.
Динамический импорт может использоваться внутри условий.
if (user.isAdmin) {
const admin =
await import('./admin.js');
admin.start();
}
Загрузка происходит только при выполнении условия.
Это позволяет отделить специализированный функционал от основной части приложения.
Иногда различные устройства требуют разного набора модулей.
if (window.innerWidth < 768) {
const mobile =
await import('./mobile.js');
mobile.init();
}
Для настольных устройств этот код никогда не будет загружен.
Не всегда следует ждать момента использования модуля.
Иногда лучше сначала быстро показать интерфейс, а затем догрузить второстепенные части.
Пример:
startApplication();
import('./analytics.js');
В этом случае:
Подобная стратегия часто называется отложенной загрузкой после первого рендера.
После первой загрузки модуль сохраняется браузером.
Первый вызов:
await import('./reports.js');
выполняет сетевой запрос.
Повторный вызов:
await import('./reports.js');
использует уже загруженный экземпляр.
Это означает, что загрузка происходит только один раз за жизненный цикл страницы.
Любой сетевой запрос может завершиться ошибкой.
Поэтому рекомендуется использовать конструкцию:
try {
const module =
await import('./reports.js');
module.show();
}
catch (error) {
console.error(error);
}
Причины ошибок могут быть различными:
После внедрения динамического импорта структура сборки обычно меняется.
Вместо:
main.js
получается:
main.js
chunk-1.js
chunk-2.js
chunk-3.js
chunk-4.js
Каждый файл представляет отдельную часть графа зависимостей.
Подобное разделение позволяет браузеру загружать код постепенно.
Ленивая загрузка не означает, что необходимо разделять каждый модуль.
Плохой пример:
await import('./button.js');
await import('./input.js');
await import('./icon.js');
await import('./label.js');
Такой подход способен создать множество мелких запросов.
Более рационально объединять связанную функциональность:
await import('./admin-panel.js');
где внутри уже находятся:
button.js
input.js
label.js
icon.js
Это уменьшает сетевые накладные расходы.
Грамотно организованная загрузка позволяет добиться нескольких важных преимуществ:
Именно поэтому динамический импорт и автоматическое разделение кода стали одной из важнейших возможностей Rollup при разработке современных браузерных приложений.