Code splitting и ленивая загрузка в JavaScript-приложениях
Практика3 минРедакция Innovialabs
Коротко
Code splitting делит бандл на части, которые грузятся только когда нужны: маршруты — отдельно, тяжёлые компоненты — через dynamic import, а prefetch заранее подгружает вероятные следующие шаги.
Что это и зачем
Code splitting — разделение JavaScript-бандла на несколько файлов (чанков). Вместо одного большого скрипта пользователь сначала получает только код текущей страницы, а остальное — по мере необходимости. Это называется ленивой загрузкой (lazy loading).
Результат: меньше JavaScript на старте, быстрее парсинг и выполнение, лучше отзывчивость страницы. Цена — дополнительные сетевые запросы позже, поэтому делить нужно с умом.
Основа: dynamic import
Всё строится на import() — функции, которая загружает модуль асинхронно и возвращает Promise. Сборщики (webpack, Vite, Turbopack, Rollup) видят такой вызов и автоматически выносят модуль в отдельный чанк.
button.addEventListener("click", async () => {
const { exportToPdf } = await import("./export-pdf.js");
exportToPdf(data);
});
Код экспорта в PDF не попадёт в основной бандл и загрузится только при клике.
Разделение по маршрутам
Самый выгодный уровень: пользователь на главной не должен качать код личного кабинета.
- Next.js делает это автоматически: каждая страница в
app/илиpages/— отдельная точка входа. Ничего настраивать не нужно. - React с роутером (например, React Router) — оборачивайте страницы в
React.lazy:
import { lazy, Suspense } from "react";
const Dashboard = lazy(() => import("./pages/Dashboard"));
export function App() {
return (
<Suspense fallback={<div>Загрузка…</div>}>
<Dashboard />
</Suspense>
);
}
Разделение по компонентам
Внутри страницы выносите то, что тяжёлое и не нужно сразу:
- модальные окна и формы, открываемые по клику;
- графики, карты, редакторы текста, видеоплееры;
- блоки ниже первого экрана.
В Next.js для этого есть next/dynamic:
import dynamic from "next/dynamic";
const Chart = dynamic(() => import("@/components/chart"), {
loading: () => <p>Загрузка графика…</p>,
ssr: false,
});
Опция ssr: false нужна для компонентов, которые работают только в браузере (используют window). Учитывайте, что в App Router её можно применять только в клиентских компонентах — сверяйтесь с документацией вашей версии.
Не делите мелочи. Кнопка или иконка в отдельном чанке даст лишний запрос без выигрыша.
Prefetching: убрать задержку
Ленивая загрузка добавляет паузу в момент, когда код понадобился. Prefetch загружает его заранее, в фоне.
| Стратегия | Когда загружать | Подходит для |
|---|---|---|
| При появлении ссылки в viewport | Ссылка видна на экране | Навигация между страницами (так по умолчанию делает next/link в production) |
| При наведении / фокусе | Пользователь навёл курсор | Тяжёлые модалки, меню |
| В idle-время | Браузер свободен (requestIdleCallback) | Вероятный следующий шаг сценария |
| Без prefetch | Только по действию | Редкие функции, админские инструменты |
Пример prefetch при наведении:
const loadEditor = () => import("./editor");
button.addEventListener("mouseenter", loadEditor, { once: true });
button.addEventListener("click", async () => (await loadEditor()).open());
Повторный import() того же модуля не скачивает его снова — используется кэш.
Как измерить эффект
- До изменений снимите базу: размер начального JS (bundle analyzer), Lighthouse, метрики LCP и INP.
- Проверьте вкладку Network в DevTools: новые чанки должны появляться только при нужном действии.
- Вкладка Coverage покажет, сколько загруженного JS реально выполняется на старте.
- Сравните field-метрики после релиза — лабораторный прогон может не отражать реальные устройства.
Частые ошибки
- Слишком много мелких чанков — сетевые накладные расходы съедают выигрыш.
- Ленивая загрузка того, что видно на первом экране, — пользователь видит заглушку вместо контента.
- Нет
fallbackи обработки ошибок загрузки чанка (например, после нового деплоя старые чанки могут исчезнуть). - Общая тяжёлая библиотека импортируется статически в общем модуле и всё равно попадает в главный бандл.