Ленивая загрузка маршрутов - подходы примеры и лучшие практики

28 января 2026
Автор

Олег Марков

Введение

Ленивая загрузка маршрутов (lazy-loading routes) — это подход, при котором код отдельных страниц или модулей загружается не сразу при старте приложения, а только в момент, когда пользователь действительно переходит по соответствующему маршруту.

Вы экономите трафик, ускоряете первоначальную загрузку и делаете интерфейс отзывчивее, особенно если приложение большое и состоит из десятков страниц.

Смотрите, я покажу вам, как это работает на практике, а также разберу типичные подходы в современных фреймворках:

  • общие принципы ленивой загрузки маршрутов;
  • как это устроено под капотом с точки зрения бандла и динамического импорта;
  • примеры на чистом JavaScript с History API и SPA;
  • примеры с React Router;
  • примеры с Vue Router;
  • примеры с Angular Router;
  • типичные ошибки и подводные камни;
  • рекомендации по структуре проекта и лучшим практикам.

Независимо от того, какой фреймворк вы используете, сама идея ленивой загрузки маршрутов везде примерно одна и та же, так что вы сможете адаптировать подход под свой стек.


Что такое ленивая загрузка маршрутов

Основная идея

Давайте начнем с сильного упрощения. Представьте у вас SPA с 10 страницами:

  • главная;
  • каталог;
  • карточка товара;
  • корзина;
  • профиль;
  • настройки;
  • и так далее.

Без ленивой загрузки весь код всех страниц попадает в один большой бандл, который браузер скачивает сразу. Из-за этого:

  • увеличивается время загрузки;
  • страница дольше становится интерактивной;
  • пользователю приходится ждать, даже если он никогда не зайдет, например, в «настройки администратора».

Ленивая загрузка маршрутов решает это так:

  • стартовый бандл содержит только код, необходимый для запуска приложения и отображения первых экранов;
  • остальные страницы упакованы в отдельные чанки (куски кода);
  • при переходе по маршруту код этой страницы подгружается по сети через динамический импорт;
  • после загрузки страница рендерится как обычно.

Отличие от обычного code splitting

Ленивая загрузка маршрутов — это частный случай разделения кода (code splitting). Разделять можно по-разному:

  • по маршрутам (страницам);
  • по фичам (feature-based);
  • по виджетам (например, модальное окно, которое редко открывается);
  • по внешним библиотекам.

Но ленивую загрузку маршрутов проще всего реализовать и понять, потому что маршруты уже естественно делят приложение на независимые части.

Когда имеет смысл использовать

Ленивая загрузка маршрутов почти всегда полезна, если:

  • у вас SPA или MPA с JavaScript роутером;
  • объем кода значительный;
  • маршрутов больше нескольких штук.

Однако бывают нюансы:

  • если приложение очень маленькое, выгода может быть минимальной;
  • если у вас супер строгие требования к быстроте навигации между всеми разделами, возможно, часть модулей вы захотите предзагружать заранее.

Как это работает под капотом

Динамический импорт

Давайте разберемся на примере. В современном JavaScript есть динамический импорт модулей:

// Здесь мы динамически загружаем модуль только тогда, когда он нужен
import("./pages/ProfilePage.js")
  .then(module => {
    const ProfilePage = module.default
    // Здесь мы, например, рендерим страницу профиля
    render(ProfilePage)
  })
  .catch(error => {
    // Здесь мы обрабатываем ошибку загрузки модуля
    console.error("Не удалось загрузить страницу профиля", error)
  })

Что происходит:

  • при сборке бандлера (Webpack, Vite, esbuild) конструкция import с круглым скобками воспринимается как точка разделения кода;
  • генерируется отдельный файл чанка, например ProfilePage.[hash].js;
  • во время выполнения браузер загружает этот файл только при вызове dynamic import.

Ленивая загрузка маршрутов — это по сути обертка роутера вокруг динамического импорта.

Связь с роутером

Если упростить, роутер делает примерно следующее:

// Пример упрощенного роутера с ленивой загрузкой
const routes = {
  "/profile": () => import("./pages/ProfilePage.js"),
  "/settings": () => import("./pages/SettingsPage.js"),
}

// Здесь мы подписываемся на изменения адресной строки
window.addEventListener("popstate", handleRouteChange)

// Эта функция вызывается и при первом заходе и при клике по ссылкам
function handleRouteChange() {
  const path = window.location.pathname
  const loadComponent = routes[path]

  if (!loadComponent) {
    // Здесь мы показываем страницу 404
    render(NotFoundPage)
    return
  }

  // Здесь мы можем показать спиннер пока грузится модуль
  showLoadingSpinner()

  loadComponent()
    .then(module => {
      const PageComponent = module.default
      // Здесь скрываем спиннер и рендерим загруженную страницу
      hideLoadingSpinner()
      render(PageComponent)
    })
    .catch(error => {
      // Здесь показываем сообщение об ошибке, если модуль не загрузился
      console.error("Ошибка загрузки маршрута", error)
      render(ErrorPage)
    })
}

Сейчас это псевдокод, но на нем хорошо видно общую механику:

  • в таблице маршрутов вместо компонента сразу — функция загрузки;
  • при навигации роутер вызывает функцию, которая возвращает промис динамического импорта;
  • после загрузки, как только модуль доступен, он рендерится.

Базовая реализация ленивой загрузки маршрутов без фреймворка

Чтобы лучше прочувствовать идею, давайте реализуем очень простой клиентский роутер с ленивой загрузкой без использования больших фреймворков.

Структура проекта

Представим, что у вас есть такой набор файлов:

  • index.html
  • main.js
  • router.js
  • pages/
    • HomePage.js
    • AboutPage.js
    • ProfilePage.js

Пример содержимого страниц:

// pages/HomePage.js
export default function HomePage(rootElement) {
  // Здесь мы рендерим простую разметку в корневой элемент
  rootElement.innerHTML = `
    <h1>Главная страница</h1>
    <p>Добро пожаловать на главную</p>
  `
}
// pages/AboutPage.js
export default function AboutPage(rootElement) {
  // Здесь мы рендерим содержимое страницы "О проекте"
  rootElement.innerHTML = `
    <h1>О проекте</h1>
    <p>Здесь вы узнаете подробности о приложении</p>
  `
}
// pages/ProfilePage.js
export default function ProfilePage(rootElement) {
  // Здесь мы рендерим условный профиль пользователя
  rootElement.innerHTML = `
    <h1>Профиль</h1>
    <p>Информация о пользователе</p>
  `
}

Простой роутер с ленивой загрузкой

Теперь создадим файл router.js:

// router.js

// Здесь мы описываем таблицу маршрутов
// Вместо прямой ссылки на компонент у нас функция, которая загружает модуль
const routes = {
  "/": () => import("./pages/HomePage.js"),
  "/about": () => import("./pages/AboutPage.js"),
  "/profile": () => import("./pages/ProfilePage.js"),
}

// Здесь мы указываем корневой элемент, в который будем рендерить страницы
const root = document.getElementById("app")

// Функция рендера загруженного компонента
function renderPage(PageModule) {
  // Здесь получаем компонент по default экспорту
  const PageComponent = PageModule.default
  // Здесь очищаем содержимое корневого элемента перед рендером
  root.innerHTML = ""
  // Здесь вызываем компонент, передавая ему корневой элемент
  PageComponent(root)
}

// Функция обработки маршрута
export function navigateTo(path) {
  // Здесь мы обновляем адресную строку без перезагрузки страницы
  window.history.pushState({}, "", path)
  // Здесь вызываем обработчик маршрута
  handleRoute()
}

// Основная функция, которая обрабатывает текущий путь
export function handleRoute() {
  const path = window.location.pathname
  const loadPage = routes[path]

  if (!loadPage) {
    // Здесь мы отображаем простую 404 страницу
    root.innerHTML = "<h1>Страница не найдена</h1>"
    return
  }

  // Здесь можно отобразить индикатор загрузки
  root.innerHTML = "<p>Загрузка...</p>"

  // Здесь мы вызываем функцию, которая вернет промис с модулем страницы
  loadPage()
    .then(renderPage)
    .catch(error => {
      // Здесь логируем ошибку и показываем сообщение пользователю
      console.error("Ошибка загрузки страницы", error)
      root.innerHTML = "<p>Ошибка загрузки страницы</p>"
    })
}

// Здесь мы подписываемся на событие изменения истории браузера
window.addEventListener("popstate", handleRoute)

Инициализация роутера в main.js:

// main.js
import { navigateTo, handleRoute } from "./router.js"

// Здесь мы настраиваем обработчик кликов по ссылкам
document.addEventListener("click", event => {
  const link = event.target.closest("a")
  if (!link) return

  const href = link.getAttribute("href")

  // Здесь мы проверяем, что ссылка ведет на относительный маршрут приложения
  const isInternalLink = href && href.startsWith("/")
  if (!isInternalLink) return

  // Здесь мы предотвращаем стандартную навигацию браузера
  event.preventDefault()
  // Здесь мы выполняем навигацию через наш роутер
  navigateTo(href)
})

// Здесь мы вызываем обработчик маршрута при первой загрузке страницы
handleRoute()

Теперь вы увидите, как это выглядит в браузере:

  • при первом открытии загрузится только основной бандл (main.js, router.js и та страница, которая нужна сразу, если вы не лениво грузите и ее);
  • при переходе на /profile в сети появится новый запрос к чанку с ProfilePage.js;
  • после загрузки компонент отрисуется.

Ленивая загрузка маршрутов в React Router

Давайте перейдем к более практичным вещам и посмотрим, как это делается в популярных фреймворках. Начнем с React.

React.lazy и Suspense

В React есть встроенный механизм ленивой загрузки компонентов:

  • React.lazy — обертка над динамическим импортом;
  • Suspense — компонент, который показывает fallback (например, "Загрузка…"), пока ленивый компонент загружается.

Пример:

// Здесь мы лениво загружаем компонент страницы профиля
const ProfilePage = React.lazy(() => import("./pages/ProfilePage"))

// Здесь мы описываем маршруты
function AppRouter() {
  return (
    // Здесь Suspense показывает запасной контент пока грузятся ленивые компоненты
    <React.Suspense fallback={<div>Загрузка...</div>}>
      <Routes>
        {/* Здесь мы подключаем ленивый компонент к маршруту */}
        <Route path="/profile" element={<ProfilePage />} />
      </Routes>
    </React.Suspense>
  )
}

Основное, на что стоит обратить внимание:

  • функция в React.lazy должна возвращать промис import;
  • Suspense обязателен, без него React не знает, что показывать, пока компонент грузится;
  • в fallback вы можете разместить любой JSX, обычно это спиннер или skeleton.

Ленивая загрузка по маршрутам с React Router v6

Давайте пример чуть расширим.

Структура:

  • pages/HomePage.jsx
  • pages/ProductsPage.jsx
  • pages/ProductDetailsPage.jsx
  • pages/ProfilePage.jsx

Конфигурация маршрутов:

// AppRouter.jsx
import { BrowserRouter, Routes, Route } from "react-router-dom"
import React from "react"

// Здесь мы лениво загружаем страницы
const HomePage = React.lazy(() => import("./pages/HomePage"))
const ProductsPage = React.lazy(() => import("./pages/ProductsPage"))
const ProductDetailsPage = React.lazy(() => import("./pages/ProductDetailsPage"))
const ProfilePage = React.lazy(() => import("./pages/ProfilePage"))

export function AppRouter() {
  return (
    <BrowserRouter>
      {/* Здесь мы оборачиваем все маршруты в Suspense */}
      <React.Suspense fallback={<div>Загрузка страницы...</div>}>
        <Routes>
          {/* Здесь каждый маршрут использует ленивый компонент */}
          <Route path="/" element={<HomePage />} />
          <Route path="/products" element={<ProductsPage />} />
          {/* Здесь мы используем параметр маршрута id */}
          <Route path="/products/:id" element={<ProductDetailsPage />} />
          <Route path="/profile" element={<ProfilePage />} />
          {/* Здесь мы добавляем маршрут 404 */}
          <Route path="*" element={<div>Страница не найдена</div>} />
        </Routes>
      </React.Suspense>
    </BrowserRouter>
  )
}

Как видите, код довольно простой:

  • все страницы лениво загружаются;
  • первая навигация к каждой новой странице приведет к загрузке соответствующего чанка;
  • последующие переходы на уже загруженную страницу не требуют повторной загрузки, компонент уже в памяти.

Группировка маршрутов в чанки

Иногда выгоднее не дробить страницу на слишком мелкие чанки, а загружать «пакет» страниц, связанных логически. Например, создать один чанк для всех админских страниц.

Давайте посмотрим, как это сделать.

// Здесь мы лениво загружаем общий модуль админской части
const AdminLayout = React.lazy(() => import("./admin/AdminLayout"))
const AdminDashboardPage = React.lazy(() => import("./admin/AdminDashboardPage"))
const AdminUsersPage = React.lazy(() => import("./admin/AdminUsersPage"))

Если вы используете бандлер с поддержкой magic comments, вы можете подсказать ему, что эти модули должны быть в одном чанке. Например, для Webpack:

// Здесь мы используем webpackChunkName чтобы сгруппировать модули
const AdminLayout = React.lazy(() =>
  import(/* webpackChunkName: "admin" */ "./admin/AdminLayout")
)
const AdminDashboardPage = React.lazy(() =>
  import(/* webpackChunkName: "admin" */ "./admin/AdminDashboardPage")
)
const AdminUsersPage = React.lazy(() =>
  import(/* webpackChunkName: "admin" */ "./admin/AdminUsersPage")
)

Теперь при первом заходе в админку загрузится один чанк admin.js, содержащий все три модуля. Это уменьшит количество сетевых запросов и может быть выгодно, если эти страницы тесно связаны.


Ленивая загрузка маршрутов во Vue Router

Теперь давайте посмотрим на Vue Router (v3 или v4, подход очень похож).

Простая конфигурация ленивых маршрутов

В Vue Router ленивая загрузка маршрута выглядит так:

// router.js
import { createRouter, createWebHistory } from "vue-router"

// Здесь мы задаем ленивую загрузку компонентов страниц
const routes = [
  {
    path: "/",
    name: "Home",
    // Здесь component - функция которая возвращает dynamic import
    component: () => import("./pages/HomePage.vue"),
  },
  {
    path: "/about",
    name: "About",
    component: () => import("./pages/AboutPage.vue"),
  },
  {
    path: "/profile",
    name: "Profile",
    component: () => import("./pages/ProfilePage.vue"),
  },
]

export const router = createRouter({
  history: createWebHistory(),
  routes,
})

Обратите внимание:

  • вы не используете import в начале файла для страниц;
  • вместо этого в поле component прописывается функция, возвращающая динамический импорт;
  • Vue Router сам вызовет эту функцию при первой навигации к маршруту.

Ленивая загрузка и nesting (вложенные маршруты)

Давайте посмотрим, как это выглядит с вложенными маршрутами, например, в админской панели:

// router.js
const routes = [
  {
    path: "/admin",
    component: () => import("./layouts/AdminLayout.vue"),
    children: [
      {
        path: "",
        name: "AdminDashboard",
        component: () => import("./pages/admin/AdminDashboardPage.vue"),
      },
      {
        path: "users",
        name: "AdminUsers",
        component: () => import("./pages/admin/AdminUsersPage.vue"),
      },
    ],
  },
]

В таком случае:

  • сначала при заходе на любой маршрут /admin загрузится AdminLayout.vue;
  • затем подгрузится соответствующая дочерняя страница (Dashboard или Users).

Здесь я размещаю пример, чтобы вам было проще понять, как разделять код по уровням маршрутов.

Группировка чанков во Vue (Webpack)

Если вы используете Vue CLI с Webpack, вы можете использовать именование чанков:

{
  path: "/admin",
  component: () =>
    import(/* webpackChunkName: "admin" */ "./layouts/AdminLayout.vue"),
  children: [
    {
      path: "",
      name: "AdminDashboard",
      component: () =>
        import(/* webpackChunkName: "admin" */ "./pages/admin/AdminDashboardPage.vue"),
    },
    {
      path: "users",
      name: "AdminUsers",
      component: () =>
        import(/* webpackChunkName: "admin" */ "./pages/admin/AdminUsersPage.vue"),
    },
  ],
}

Так все админские компоненты попадут в один чанк admin.js.


Ленивая загрузка маршрутов в Angular Router

В Angular ленивую загрузку маршрутов обычно применяют не к одиночным компонентам, а к модулям (feature modules). Это несколько меняет картину.

Структура для ленивой загрузки

Частая структура:

  • app.module.ts
  • app-routing.module.ts
  • features/
    • admin/
      • admin.module.ts
      • admin-routing.module.ts
    • profile/
      • profile.module.ts
      • profile-routing.module.ts

Вы объявляете feature-модуль, который содержит свои компоненты и свой routing module.

Пример admin-routing.module.ts:

// admin-routing.module.ts
import { NgModule } from "@angular/core"
import { RouterModule, Routes } from "@angular/router"
import { AdminDashboardComponent } from "./admin-dashboard/admin-dashboard.component"
import { AdminUsersComponent } from "./admin-users/admin-users.component"

// Здесь мы описываем маршруты внутри админского модуля
const routes: Routes = [
  {
    path: "",
    component: AdminDashboardComponent,
  },
  {
    path: "users",
    component: AdminUsersComponent,
  },
]

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})
export class AdminRoutingModule {}

И сам модуль admin.module.ts:

// admin.module.ts
import { NgModule } from "@angular/core"
import { CommonModule } from "@angular/common"
import { AdminRoutingModule } from "./admin-routing.module"
import { AdminDashboardComponent } from "./admin-dashboard/admin-dashboard.component"
import { AdminUsersComponent } from "./admin-users/admin-users.component"

// Здесь мы объявляем компоненты и подключаем роутинг для админского модуля
@NgModule({
  declarations: [AdminDashboardComponent, AdminUsersComponent],
  imports: [CommonModule, AdminRoutingModule],
})
export class AdminModule {}

Настройка ленивой загрузки в основном роутере

Теперь самое важное — как настроить ленивую загрузку маршрутов в корневом роутере.

В app-routing.module.ts:

// app-routing.module.ts
import { NgModule } from "@angular/core"
import { RouterModule, Routes } from "@angular/router"

// Здесь мы описываем ленивую загрузку модулей через loadChildren
const routes: Routes = [
  {
    path: "admin",
    // Здесь мы используем dynamic import для модуля
    loadChildren: () =>
      import("./features/admin/admin.module").then(m => m.AdminModule),
  },
  {
    path: "profile",
    loadChildren: () =>
      import("./features/profile/profile.module").then(m => m.ProfileModule),
  },
  {
    path: "",
    pathMatch: "full",
    redirectTo: "home",
  },
  // Здесь может быть ленивый модуль для home или сразу компонент
]

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

Теперь Angular:

  • не включает код admin.module.ts и profile.module.ts в стартовый бандл;
  • создает отдельные чанки для этих модулей;
  • подгружает их в момент, когда пользователь впервые переходит по маршруту /admin или /profile.

Важные нюансы в Angular

Несколько моментов, о которых часто спрашивают:

  • путь в loadChildren должен быть относительным от файла роутера и без расширения файла;
  • внутри then вы должны вернуть класс модуля, а не default export;
  • для ленивых модулей рекомендуется использовать RouterModule.forChild, а не forRoot.

Предзагрузка и оптимизация навигации

Ленивая загрузка маршрутов ускоряет первый рендер, но может добавить небольшую задержку при первом входе на конкретную страницу. Иногда это критично, особенно на медленном интернете. Здесь на помощь приходят разные варианты предзагрузки.

Предзагрузка на idle

Один из подходов — загружать дополнительные маршруты, когда браузер «отдохнул», то есть нет активных задач. В браузерах есть API requestIdleCallback.

Пример с динамическим импортом:

// Здесь мы объявляем карту ленивых маршрутов
const lazyRoutes = {
  profile: () => import("./pages/ProfilePage.js"),
  settings: () => import("./pages/SettingsPage.js"),
}

// Здесь мы планируем предзагрузку при простое браузера
if ("requestIdleCallback" in window) {
  window.requestIdleCallback(() => {
    // Здесь мы проходимся по ленивым маршрутам и начинаем их подгружать
    Object.values(lazyRoutes).forEach(loader => loader())
  })
}

Что здесь происходит:

  • при простое браузера запускается колбэк;
  • внутри вы вызываете loaders для заранее выбранных маршрутов;
  • браузер загрузит соответствующие чанки заблаговременно;
  • при реальной навигации код уже будет в кэше.

Предзагрузка по user intent

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

Пример:

// Здесь мы навешиваем обработчик наведения на ссылки
document.addEventListener("mouseover", event => {
  const link = event.target.closest("a[data-prefetch]")
  if (!link) return

  const href = link.getAttribute("href")

  // Здесь по href мы находим нужный ленивый маршрут
  if (href === "/profile") {
    // Здесь мы начинаем загружать модуль страницы профиля
    import("./pages/ProfilePage.js")
  }
})

Подход особенно полезен на десктопе. На мобильных устройствах вместо hover иногда используют prefetch по visibility — например, при появлении ссылки в зоне видимости.

Prefetch линками

Некоторые сборщики (напрямую или через плагины) умеют автоматически вставлять теги <link rel="prefetch"> для ленивых чанков. Тогда браузер сам решает, когда их скачивать.

Вручную это тоже можно сделать:

<!-- Здесь браузер при свободных ресурсах заранее загрузит указанный файл -->
<link rel="prefetch" href="/assets/ProfilePage.[hash].js" as="script">

Но обычно лучше полагаться на автоматические механизмы сборщика, чтобы не привязываться к конкретным именам файлов.


Типичные проблемы и подводные камни

Теперь давайте посмотрим, с чем чаще всего сталкиваются разработчики, когда начинают использовать ленивую загрузку маршрутов.

1. Ошибки при загрузке чанков

Иногда сеть нестабильна, или пользователь обновляет страницу в момент деплоя, и чанк с новым хэшем становится недоступен. В результате dynamic import может завершиться с ошибкой.

Как минимум, стоит обработать ошибку и показать пользователю понятное сообщение или кнопку «Попробовать снова».

Пример на React с ErrorBoundary:

// ErrorBoundary.jsx
import React from "react"

// Здесь мы создаем классический Error Boundary для перехвата ошибок
export class RouteErrorBoundary extends React.Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false }
  }

  static getDerivedStateFromError() {
    // Здесь мы указываем что произошла ошибка
    return { hasError: true }
  }

  componentDidCatch(error, errorInfo) {
    // Здесь можно отправить ошибку в систему логирования
    console.error("Ошибка в ленивом маршруте", error, errorInfo)
  }

  render() {
    if (this.state.hasError) {
      // Здесь показываем простой fallback интерфейс
      return (
        <div>
          <p>Не удалось загрузить страницу</p>
          <button onClick={() => window.location.reload()}>Перезагрузить</button>
        </div>
      )
    }

    return this.props.children
  }
}

Использование:

// AppRouter.jsx
import { RouteErrorBoundary } from "./RouteErrorBoundary"

export function AppRouter() {
  return (
    <BrowserRouter>
      <RouteErrorBoundary>
        <React.Suspense fallback={<div>Загрузка...</div>}>
          <Routes>
            {/* Здесь ваши маршруты */}
          </Routes>
        </React.Suspense>
      </RouteErrorBoundary>
    </BrowserRouter>
  )
}

2. SEO и серверный рендеринг

В SPA-подходе ленивая загрузка маршрутов не всегда хорошо сочетается с поисковой оптимизацией, особенно если вы рендерите все на клиенте. Если вы используете SSR (Next.js, Nuxt, Angular Universal), важно:

  • убедиться, что серверная сборка тоже корректно обрабатывает ленивые компоненты;
  • проверить, что критичный для SEO контент доступен без необходимости загружать дополнительные чанки после первого рендера.

В большинстве современных SSR-фреймворков поддержка ленивых маршрутов уже встроена, но иногда приходится дополнительно настраивать серверный бандл.

3. Слишком много маленьких чанков

Иногда разработчики чрезмерно увлекаются и создают десятки очень маленьких чанков по 1–2 килобайта. Каждый чанк — это отдельный HTTP-запрос, и их слишком большое количество может привести к дополнительным накладным расходам.

Рекомендации:

  • анализируйте сборку (webpack-bundle-analyzer, Vite visualizer и аналоги);
  • группируйте логически связанные страницы и компоненты в один чанк;
  • не дробите код слишком агрессивно.

4. Сложность отладки

Когда приложение разбито на чанки, карта соответствия исходников и скомпилированного кода усложняется. Обычно sourcemaps решают проблему, но:

  • убедитесь, что sourcemaps включены в дев-сборке;
  • в продакшене можно оставить их с ограничением доступа, если нужны глубокие прод отладки.

Рекомендации по проектированию маршрутов с ленивой загрузкой

Теперь давайте соберем практические советы, которые помогут вам использовать ленивую загрузку маршрутов более осмысленно.

Делите по смыслу, а не механически

Лучший критерий для ленивого маршрута:

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

Типичные кандидаты:

  • админские разделы;
  • страницы отчетов и аналитики;
  • сложные формы и мастера (wizard);
  • справочные разделы и документация внутри приложения.

Не выносите в ленивые маршруты критически важные части UI

Например:

  • шапка (header) и навигация;
  • футер;
  • базовая сетка страниц;
  • элементы, без которых приложение не имеет смысла (например, домашняя страница, если на нее всегда попадает большинство пользователей).

Обычно их лучше оставить в стартовом бандле.

Планируйте стратегию предзагрузки

Особенно если:

  • вы знаете типичные пути пользователя;
  • у вас высокие требования к скорости отклика при навигации.

Подходы:

  • предзагрузка при простое (idle);
  • предзагрузка по hover или focus;
  • предзагрузка ближайших логически следующих шагов (например, после корзины — страница оплаты).

Анализируйте результат

После включения ленивой загрузки маршрутов стоит:

  • проверить размер стартового бандла до и после;
  • измерить время до первого рендера (First Contentful Paint, Time to Interactive);
  • убедиться, что пользовательская навигация не стала субъективно медленнее.

Инструменты:

  • Lighthouse;
  • DevTools вкладка Network и Performance;
  • анализаторы бандла.

Заключение

Ленивая загрузка маршрутов — один из самых эффективных и при этом простых способов оптимизации производительности в SPA и крупных фронтенд-приложениях. Вместо того чтобы загружать весь код сразу, вы делите приложение на логические части и подгружаете их по мере необходимости.

В статье вы увидели:

  • общие принципы ленивой загрузки маршрутов и то, как они завязаны на динамический импорт модулей;
  • пример упрощенного роутера на чистом JavaScript;
  • практические конфигурации для React Router с React.lazy и Suspense;
  • конфигурации для Vue Router с динамическими компонентами в маршрутах;
  • реализацию ленивой загрузки модулей в Angular Router;
  • подходы к предзагрузке (idle, user intent, prefetch);
  • типичные проблемы и способы их обхода.

Теперь, когда идея и базовые шаблоны понятны, вы можете адаптировать их под свою архитектуру, добавлять предзагрузку, объединять маршруты в чанки и постепенно улучшать скорость работы приложения.


Частозадаваемые технические вопросы по теме статьи и ответы на них

Как отложить загрузку только части компонента а не всей страницы

Иногда вам нужно лениво загрузить не страницу, а тяжелый виджет на ней. В этом случае используйте ленивые компоненты внутри страницы. В React это React.lazy для конкретного виджета, в Vue — defineAsyncComponent. Маршрут может быть обычным, а внутри компонента маршрута вы рендерите ленивый дочерний компонент. Так вы комбинируете ленивую загрузку маршрутов и ленивую загрузку отдельных частей.

Как кэшировать данные вместе с лениво загружаемым маршрутом

Код маршрута кэшируется браузером автоматически, но данные нет. Если вы хотите кэшировать ответы API для ленивых страниц, используйте слой состояния — например, React Query, Vue Query или Redux Toolkit Query. Настройте кэш с подходящим временем жизни, тогда при повторном входе на маршрут данные загрузятся из кэша, а не из сети.

Как обрабатывать обновление версии приложения когда хеши чанков поменялись

При деплое новые чанки получают другие имена, и старые ссылки на них могут ломаться. Для решения добавьте обработчик ошибки загрузки чанка. В React ErrorBoundary при ошибке dynamic import можно предложить пользователю перезагрузить страницу. На уровне сервис-воркера тоже можно реализовать стратегию «обновить и перезагрузить», когда появляется новая версия.

Можно ли лениво грузить маршруты в серверно рендеримых фреймворках типа Next.js

Да, но подход отличается. В Next.js маршруты формируются по файлам, а код сплиттинг по страницам есть по умолчанию. Дополнительно вы можете использовать dynamic с опцией ssr false для клиентовых виджетов. Важно проверять, чтобы критичный для SEO контент рендерился на сервере и не зависел от ленивых клиентских модулей.

Как организовать типизацию ленивых маршрутов в TypeScript

Основной момент — правильно типизировать функции dynamic import. В TS используйте синтаксис import для модулей с указанием типа, например type для типов и обычный import для компонентов. В Angular Router типы маршрутов и модулей уже описаны, в React и Vue вы можете типизировать лэйауты и пропсы компонентов, а также создавать отдельный тип RouteConfig с полями path и loader чтобы IDE подсказывала корректные маршруты и минимизировала опечатки.

Стрелочка влевоНавигационные хуки navigation guards - полный практический разборДинамические маршруты dynamic-routes в современных фреймворкахСтрелочка вправо

Постройте личный план изучения Vue до уровня Middle — бесплатно!

Vue — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по Vue

Руководство по валидации форм во Vue.jsИнтеграция Tiptap для создания редакторов на VueРабота с таблицами во Vue через TanStackИнструкция по установке и компонентам Vue sliderУправление пакетами Vue js с помощью npmУправление пакетами и node modules в Vue проектахКак использовать meta для улучшения SEO на VueПолный гайд по компоненту messages во Vuejs5 правил использования Inertia с Vue и LaravelРабота с модулями и пакетами в VueИнструкция по работе с grid на VueGithub для Vue проектов - подробная инструкция по хранению и совместной работеНастройка ESLint для Vue проектов и поддержка качества кодаОбработка ошибок и отладка в Vue.jsИспользование Vue Devtools для отладки и мониторинга приложенийРабота с конфигурационными файлами и скриптами VueСоздание и настройка проектов Vue с помощью Vue CLI3 способа интеграции Chart.js с Vue для создания графиковРабота с Canvas во VueИнструкция по реализации календаря во VueРабота с Ant Design Vue для создания UI на Vue
Vuex - полное руководство по управлению состоянием во Vue приложенияхРеактивные ссылки ref - полный разбор для разработчиковРеактивные объекты reactive-objects - подробное руководство с примерамиРеактивные переменные - концепция reactive и практические примерыМеханизм Provide Inject - как он работает и когда применятьPinia современный менеджер состояния для VueЛокальное состояние local state в веб разработкеГлобальное состояние в приложениях - global state
Обзор и использование утилит Vue для удобной разработкиРабота с обновлениями компонента и жизненным циклом updateРазрешение конфликтов и ошибок с помощью Vue resolveИспользование query-параметров и их обработка в маршрутах VueЗагрузка и управление состоянием загрузки в VueИспользование библиотек Vue для расширения функционалаРабота с JSON данными в приложениях VueКак работать с экземплярами компонента Instance во VueПолучение данных и API-запросы во Vue.jsЭкспорт и импорт данных и компонентов в VueОбработка событий и их передача между компонентами VuejsГайд по defineEmits на Vue 3Понимание core функционала Vue и его применениеПонимание и применение Composition API в Vue 3Понимание и работа с компилятором VueКогда и как использовать $emit и call во VueВзаимодействие с внешними API через Axios в Vue
Веб приложения на Vue архитектура и лучшие практикиИспользование Vite для быстрого старта и сборки проектов на Vue 3Работа с URL и ссылками в приложениях на VueРабота с пользовательскими интерфейсами и UI библиотеками во VueОрганизация и структура исходных файлов в проектах VueИспользование Quasar Framework для разработки на Vue с готовыми UI-компонентамиОбзор популярных шаблонов и стартовых проектов на VueИнтеграция Vue с PHP для создания динамичных веб-приложенийКак организовать страницы и маршруты в проекте на VueNuxt JS и Vue 3 для SSR приложенийСоздание серверных приложений на Vue с помощью Nuxt jsИспользование Vue Native для разработки мобильных приложенийОрганизация и управление индексной страницей в проектах VueИспользование Docker для контейнеризации приложений на VueИнтеграция Vue.js с Django для создания полноценных веб-приложенийСоздание и работа с дистрибутивом build dist Vue приложенийРабота со стилями и CSS в Vue js для красивых интерфейсовСоздание и структурирование Vue.js приложенияКак исправить ошибку cannot find module vueНастройка и сборка проектов Vue с использованием современных инструментовИнтеграция Vue с Bitrix для корпоративных решенийРазработка административных панелей на Vue js
Функция append в Go GolangОтображение компонента mounted - практическое руководствоХуки жизненного цикла компонентов - полное руководство для разработчиковУничтожение компонента destroyed - как правильно очищать ресурсы и подпискиИнициализация данных в состоянии created - как и когда подготавливать данные в приложенииОбновление компонента beforeUpdate во VueМонтирование компонента - хук beforeMount в VueРазрушение компонента во Vue - beforeDestroy и beforeUnmountСоздание экземпляра beforeCreate - полный разбор жизненного цикла
5 библиотек для создания tree view во VueИнтеграция Tailwind CSS с Vue для современных интерфейсовИнтеграция Vue с серверной частью и HTTPS настройкамиКак обрабатывать async операции с Promise во VueИнтеграция Node.js и Vue.js для разработки приложенийРуководство по интеграции Vue js в NET проектыПримеры использования JSX во VueГайд по импорту и регистрации компонентов на VueМногоязычные приложения на Vue с i18nИнтеграция FLIR данных с Vue5 примеров использования filter во Vue для упрощения разработки3 примера реализации drag-and-drop во Vue
Слоты компонента - концепция и практическое использованиеРегистрация компонентов component-registration в приложениях с внедрением зависимостейProps компонента в React - полный разбор с примерамиФункциональные компоненты в React - функциональный подход к построению интерфейсовСобытия компонента - events в современных интерфейсахДинамические компоненты - dynamic-componentsСоздание компонента component - практическое руководствоАсинхронные компоненты async-components - практическое руководство
Наблюдатели watchers - от паттерна до практических реализацийУправление переменными и реактивными свойствами во VueПрименение v-bind для динамической привязки атрибутов в VueИспользование v for и slot в VueУправление пользователями и их данными в Vue приложенияхСоздание и использование UI Kit для Vue приложенийТипизация и использование TypeScript в VuejsШаблоны Vue templates - практическое руководство для разработчиковИспользование шаблонов в Vue js для построения интерфейсовИспользование Swiper для создания слайдеров в VueРабота со стилями и стилизацией в VueСтруктура и особенности Single File Components SFC в VueРабота со SCSS в проектах на Vue для стилизацииРабота со скроллингом и прокруткой в Vue приложенияхПрименение script setup синтаксиса в Vue 3 для упрощения компонентовИспользование scoped стилей для изоляции CSS в компонентах Vue3 способа улучшить навигацию Vue с push()Обработка запросов и асинхронных операций в VueРеактивность Vue reactivity - как это работает под капотом и как этим пользоватьсяПонимание и использование provide inject для передачи данных между компонентамиПередача и использование props в Vue 3 для взаимодействия компонентовПередача данных между компонентами с помощью props в Vue jsУправление property и функциями во Vue.jsРабота со свойствами компонентов VueУправление параметрами и динамическими данными во VueОпции компонента в Go - паттерн component-optionsРабота с lifecycle-хуком onMounted во VueОсновы работы с объектами в VueПонимание жизненного цикла компонента Vue js на примере mountedИспользование модальных окон modal в Vue приложенияхИспользование методов в компонентах Vue для обработки логикиИспользование метода map в Vue для обработки массивовИспользование хуков жизненного цикла Vue для управления состоянием компонентаРабота с ключами key в списках и компонентах VueОбработка пользовательского ввода в Vue.jsРабота с изображениями и их оптимизация в VueИспользование хуков жизненного цикла в VueОрганизация сеток и гридов для верстки интерфейсов на VueСоздание и управление формами в VueОрганизация файлов и структура проекта Vue.jsКомпоненты Vue создание передача данных события и emitРабота с динамическими компонентами и данными в Vue3 способа манипулирования DOM на VueРуководство по div во VueИспользование директив в Vue и их расширенные возможностиОсновы и применение директив в VueИспользование директив и их особенности на Vue с помощью defineИспользование компонентов datepicker в Vue для выбора датОрганизация циклов и итераций во VueКак работает компиляция Vue CoreВычисляемые свойства computed во Vue.jsСоздание и использование компонентов в Vue JSОбработка кликов и пользовательских событий в VueИспользование классов в Vue для организации кода и компонентовИспользование директивы checked для управления состоянием чекбоксов в VueГайд на checkbox компонент во VueОтображение данных в виде графиков с помощью Vue ChartСоздание и настройка кнопок в VueСоздание и настройка кнопок в Vue приложенияхРабота с lifecycle-хуками beforeCreate и beforeMount во VueОсновы Vue - vue-basics для уверенного стартаИспользование массивов и методов их обработки в VueИспользование массивов и их обработка в Vue
Использование Vuetify для создания современных интерфейсов на VueТестирование компонентов и приложений на VueИспользование transition во VueТелепортация - архитектура и реализация в серверных приложенияхРабота с teleport для управления DOM во VueSuspense в React - управление асинхронными данными и ленивой загрузкойПять шагов по настройке SSR в VuejsИспользование Shadcn UI компонентов с Vue для продвинутых интерфейсовИспользование router-link для навигации в Vue RouterКак использовать require в Vue для динамического импорта модулейРабота с динамическим рендерингом и виртуальным DOM на Vue.jsИспользование ref для управления ссылками и реактивностью в Vue 3Использование Vue Pro и его преимущества для профессиональной разработкиПлагины Vue vue-plugins - полное практическое руководствоРуководство по nextTick для работы с DOMМиксины - mixins в современном программированииJSX в Vue с использованием плагина vue-jsxСоздание и использование компонентов с помощью Vue js и CУправление состоянием и реактивностью через inject и provideДинамическое обновление компонентов и данных на VueГлубокое изучение документации Vue и как эффективно её использоватьКастомные элементы - Custom Elements в современном JavaScriptИспользование Crystal с Vue для разработкиИспользование вычисляемых свойств для динамического отображения данных на Vue jsОптимизация производительности и предупреждения в Vue
Открыть базу знаний

Лучшие курсы по теме

изображение курса

Vue 3 и Pinia

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.9
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажеры
Практика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий