Перейти к содержанию
Innovialabs

Code splitting и ленивая загрузка в JavaScript-приложениях

Практика3 мин

Коротко

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() того же модуля не скачивает его снова — используется кэш.

Как измерить эффект

  1. До изменений снимите базу: размер начального JS (bundle analyzer), Lighthouse, метрики LCP и INP.
  2. Проверьте вкладку Network в DevTools: новые чанки должны появляться только при нужном действии.
  3. Вкладка Coverage покажет, сколько загруженного JS реально выполняется на старте.
  4. Сравните field-метрики после релиза — лабораторный прогон может не отражать реальные устройства.

Частые ошибки

  • Слишком много мелких чанков — сетевые накладные расходы съедают выигрыш.
  • Ленивая загрузка того, что видно на первом экране, — пользователь видит заглушку вместо контента.
  • Нет fallback и обработки ошибок загрузки чанка (например, после нового деплоя старые чанки могут исчезнуть).
  • Общая тяжёлая библиотека импортируется статически в общем модуле и всё равно попадает в главный бандл.

Частые вопросы

Разделение по страницам Next.js делает сам. Вручную стоит выносить только тяжёлые компоненты внутри страниц через next/dynamic или import().

Поможем с этим

Если хотите не разбираться самим, а получить результат — этим мы занимаемся каждый день.

Обсудить проект

Читайте также

Есть идея или задача? Расскажите нам

Ответим в течение рабочего дня и предложим решение. Консультация бесплатная.