Олег Марков
Асинхронные компоненты async-components - практическое руководство
Введение
Асинхронные компоненты (часто их называют async-components) позволяют загружать части интерфейса не сразу, а по мере необходимости. Вы можете показывать пользователю только то, что ему действительно нужно в данный момент, а остальное догружать позже.
Если говорить проще, асинхронный компонент — это компонент, код которого загружается отдельно и обычно только тогда, когда он действительно требуется. Такой подход значительно уменьшает размер первого бандла, ускоряет загрузку страницы и делает интерфейс отзывчивее.
В этой статье я покажу, как устроены async-components на практике, зачем они нужны, какие есть подходы к их реализации и с какими подводными камнями вы можете столкнуться. Мы будем опираться на типичные примеры из экосистемы JavaScript (React, Vue и условно-фреймворк-агностичный подход), но идеи остаются теми же и в других технологиях.
Что такое асинхронный компонент
Синхронный и асинхронный компонент — в чем разница
Сначала давайте зафиксируем терминологию.
Синхронный компонент:
- Импортируется вместе со всем кодом приложения
- Попадает в главный бандл
- Доступен сразу при первом рендере
Пример обычного синхронного импорта:
// Синхронный импорт - компонент попадает в основной бандл
import UserProfile from "./UserProfile.js"
function App() {
// Компонент уже загружен к моменту выполнения этого кода
return <UserProfile />
}
Асинхронный компонент:
- Загружается только при необходимости
- Живет в отдельном чанке (отдельном бандле)
- Может иметь состояние загрузки и ошибки
- Требует асинхронного импорта или специальной фабрики компонента
Упрощенный пример с динамическим импортом:
// Асинхронная функция загрузки компонента
function loadUserProfile() {
// Webpack или другой bundler создаст отдельный чанк под этот модуль
return import("./UserProfile.js")
}
Главная идея в том, что вы не загружаете весь компонент заранее. Вместо этого вы описываете, как его загрузить по требованию.
Зачем нужны асинхронные компоненты
Асинхронные компоненты появляются в проекте по нескольким причинам:
Снижение initial bundle size
Чем меньше код нужно загрузить при первом открытии страницы, тем быстрее пользователь увидит интерфейс.Более быстрый Time To Interactive
Браузер тратит меньше времени на парсинг и выполнение JavaScript. Интерфейс быстрее становится интерактивным.Оптимизация под пользовательские сценарии
Вы можете загружать только те части интерфейса, которые нужны конкретному пользователю. Например, админские панели — только для администраторов.Разделение ответственности в кодовой базе
Легче поддерживать крупное приложение, где тяжелые и редко используемые компоненты вынесены в отдельные модули.
Базовая реализация через dynamic import
Как работает import на уровне JavaScript
В современном JavaScript есть специальная функция import(), которая возвращает промис и загружает модуль динамически.
Смотрите, я покажу вам минимальный пример:
// Здесь мы описываем функцию, которая по запросу загружает модуль
function loadComponent() {
// import() вернет промис с модулем
return import("./HeavyComponent.js")
.then(module => {
// module.default - экспорт по умолчанию из HeavyComponent.js
return module.default
})
}
Что здесь важно:
// import() загружает код по сети только при вызове
// Результат - промис, поэтому дальше мы можем использовать await или then
Сами по себе import() и промис еще не делают компонент «асинхронным» в глазах фреймворка. Нужно встроить этот механизм в систему компонентов конкретной библиотеки.
Асинхронные компоненты в Vue 3
Vue дает специальную функцию defineAsyncComponent, которая заворачивает асинхронную загрузку в привычный для вас компонент.
Простейший пример defineAsyncComponent
Давайте разберемся на примере:
// Импортируем вспомогательную функцию Vue
import { defineAsyncComponent } from "vue"
// Описываем фабрику асинхронного компонента
const AsyncUserProfile = defineAsyncComponent(() =>
// Возвращаем промис с модулем компонента
import("./UserProfile.vue")
)
export default {
components: {
// Регистрируем асинхронный компонент как обычный
AsyncUserProfile
},
template: `
<div>
<!-- Здесь мы используем асинхронный компонент как обычный -->
<AsyncUserProfile />
</div>
`
}
Обратите внимание на несколько деталей:
// defineAsyncComponent принимает функцию, которая возвращает промис
// Vue сама отслеживает состояние «загрузка», «успех», «ошибка»
// Для вас компонент выглядит как обычный - его просто можно вставить в шаблон
Настройки загрузки компонента
У defineAsyncComponent есть расширенная форма, в которой можно явно указать поведение:
import { defineAsyncComponent } from "vue"
import LoadingSpinner from "./LoadingSpinner.vue"
import ErrorBlock from "./ErrorBlock.vue"
const AsyncReport = defineAsyncComponent({
// Фабрика загрузки компонента
loader: () => import("./HeavyReport.vue"),
// Компонент, который показывается во время загрузки
loadingComponent: LoadingSpinner,
// Компонент, который показывается при ошибке загрузки
errorComponent: ErrorBlock,
// Задержка перед показом loadingComponent (мс)
delay: 200,
// Таймаут загрузки - если больше, показываем ошибку (мс)
timeout: 10000,
// Поведение при ошибках - можно вернуть true чтобы повторить попытку
onError(error, retry, fail, attempts) {
// Здесь вы можете логировать ошибку
console.error("Ошибка загрузки компонента", error)
if (attempts <= 3) {
// Пробуем перезагрузить компонент
retry()
} else {
// Прекращаем попытки
fail()
}
}
})
Здесь я размещаю пример, чтобы вам было проще понять, за что отвечают параметры:
// loader - обязателен, это сама асинхронная загрузка компонента
// loadingComponent - что показать, пока идет загрузка
// errorComponent - что показать, если загрузка не удалась
// delay - позволяет избежать «мигания» спиннера при очень быстрой загрузке
// timeout - защита от зависания загрузки вечно
// onError - дает гибкий контроль, например, можно реализовать повторные запросы
Пример использования асинхронных компонентов с условным рендерингом
Часто вы не хотите загружать компонент, пока он вообще не нужен. Например, панель с деталями заказа, которая открывается по кнопке.
<!-- OrderPage.vue -->
<template>
<div>
<button @click="detailsVisible = !detailsVisible">
{{ detailsVisible ? "Скрыть детали" : "Показать детали" }}
</button>
<!-- Компонент загрузится, только когда detailsVisible станет true -->
<OrderDetails v-if="detailsVisible" />
</div>
</template>
<script>
import { defineAsyncComponent, ref } from "vue"
export default {
name: "OrderPage",
components: {
// Асинхронный компонент для деталей заказа
OrderDetails: defineAsyncComponent(() =>
import("./OrderDetails.vue")
)
},
setup() {
const detailsVisible = ref(false) // Флаг отображения панели
return { detailsVisible }
}
}
</script>
Как видите, этот код выполняет простую идею: пока пользователь не открыл панель, бандл с OrderDetails даже не будет запрошен по сети.
Асинхронные компоненты в React
В React подход похожий по идее, но отличается в деталях. Здесь вы будете использовать React.lazy и компонент обертку Suspense.
Базовый пример React.lazy
Теперь вы увидите, как это выглядит в коде:
// Импортируем React и Suspense
import React, { Suspense } from "react"
// Описываем асинхронный компонент
const UserProfile = React.lazy(() => import("./UserProfile"))
function App() {
return (
<div>
{/* Suspense отвечает за состояние загрузки */}
<Suspense fallback={<div>Загрузка профиля...</div>}>
{/* Пока модуль не загрузился, показывается fallback */}
<UserProfile />
</Suspense>
</div>
)
}
export default App
Пояснения:
// React.lazy принимает функцию, которая возвращает import()
// Suspense показывает fallback, пока компонент в состоянии ожидания
// Как только import завершится, React отрендерит реальный компонент
Условная загрузка асинхронного компонента
Вы можете комбинировать lazy с условным рендерингом, чтобы не запрашивать компонент раньше времени. Давайте посмотрим, что происходит в следующем примере:
import React, { Suspense, useState } from "react"
const Chart = React.lazy(() => import("./Chart"))
function Dashboard() {
const [showChart, setShowChart] = useState(false) // Флаг показа графика
return (
<div>
<button onClick={() => setShowChart(prev => !prev)}>
{showChart ? "Скрыть график" : "Показать график"}
</button>
{showChart && (
<Suspense fallback={<div>Загрузка графика...</div>}>
{/* Компонент Chart загрузится только после нажатия кнопки */}
<Chart />
</Suspense>
)}
</div>
)
}
export default Dashboard
Ключевой момент:
// Пока showChart равно false - import("./Chart") даже не будет вызван
// Как только вы нажимаете кнопку - React идет за чанком с графиком
Маршрутизация и асинхронные компоненты в React
Чаще всего асинхронные компоненты в React используют на уровне роутинга. Здесь вы догружаете коду конкретной страницы, только когда пользователь переходит по соответствующему маршруту.
Пример с React Router:
// AppRoutes.jsx
import React, { Suspense } from "react"
import { BrowserRouter as Router, Routes, Route } from "react-router-dom"
// Асинхронно загружаемые страницы
const HomePage = React.lazy(() => import("./pages/HomePage"))
const AdminPage = React.lazy(() => import("./pages/AdminPage"))
const ReportsPage = React.lazy(() => import("./pages/ReportsPage"))
function AppRoutes() {
return (
<Router>
{/* Один общий Suspense для всех ленивых маршрутов */}
<Suspense fallback={<div>Загрузка страницы...</div>}>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/admin" element={<AdminPage />} />
<Route path="/reports" element={<ReportsPage />} />
</Routes>
</Suspense>
</Router>
)
}
export default AppRoutes
Обратите внимание, как этот фрагмент кода решает задачу:
// Каждая страница находится в своем чанке
// При переходе по маршруту грузится только нужная страница
// fallback может быть общим лоудером или skeleton для страниц
Асинхронные компоненты и code splitting на уровне сборщика
Асинхронный компонент почти всегда опирается на функциональность сборщика (Webpack, Vite, Rollup, esbuild). Давайте коротко посмотрим, что важно учесть.
Как сборщик понимает, что нужно сделать отдельный чанк
Когда вы пишете:
const modulePromise = import("./HeavyComponent.js")
сборщик:
// На этапе сборки анализирует динамические импорт
// Создает отдельный чанк для этого модуля
// Встраивает в основной бандл «загрузчик чанков»
От этого поведения зависит, как ваш асинхронный компонент будет реально подгружаться.
Именование чанков и группировка
Вы можете подсказать сборщику, как называть чанки и как их группировать. Например, в Webpack можно использовать магические комментарии.
Покажу вам, как это реализовано на практике:
// Webpack поддерживает специальные комментарии для тонкой настройки
const AsyncAdminPanel = React.lazy(() =>
import(
/* webpackChunkName: "admin-panel" */
/* webpackPrefetch: true */
"./AdminPanel"
)
)
Пояснения:
// webpackChunkName - имя итогового js файла чанка
// webpackPrefetch - говорит браузеру подгрузить чанк фоном, когда будет свободный ресурс
Это полезно, когда вы хотите более точно контролировать загрузку: например, заранее подгрузить страницу, куда пользователь с высокой вероятностью перейдет.
Prefetch и Preload
Асинхронный компонент может догружаться заранее, еще до того, как пользователь реально открыл его. Для этого часто используют:
- Prefetch — «подгрузи, когда будешь свободен»
- Preload — «подгрузи как можно скорее, это важно»
В связке с async-components это дает возможность сгладить пики нагрузки и уменьшить видимую задержку для пользователя.
Паттерны использования асинхронных компонентов
Теперь давайте системно посмотрим, где и как стоит применять async-components.
Маршрутизация и страницы
Это самый классический вариант. Каждая крупная страница — отдельный асинхронный компонент.
Применять стоит, когда:
- У вас много страниц с разной логикой
- Каждая страница содержит свои тяжелые компоненты
- Вы хотите, чтобы первая страница открывалась максимально быстро
Тяжелые виджеты и графики
Компоненты с большим количеством зависимостей (графики, rich text редакторы, карты) логично делать асинхронными.
Например:
- Компонент графика с зависимостью от d3 или chart.js
- Компонент карты с подключением адресного автокомплита
- WYSIWYG редактор с большим количеством плагинов
В таких случаях:
// Асинхронная загрузка снижает размер основного бандла
// Виджет грузится только там, где он реально нужен
Функциональные разделы: админка, настройки, отчеты
Админские панели и сложные отчеты обычно нужны ограниченному кругу пользователей. Логично вынести их в отдельные чанки.
Стандартный подход:
- Основное пользовательское приложение — один бандл
- Админка — отдельный бандл, загружается только по admin маршруту
- Отчеты — отдельный бандл, могут быть даже несколько независимых зон
Управление состоянием загрузки и ошибок
Асинхронные компоненты всегда связаны с состоянием: идет загрузка, произошла ошибка, все успешно.
Общие состояния, которые нужно обрабатывать
Любой async-component имеет минимум три состояния:
- Loading — компонент еще не загружен
- Success — компонент загружен и отрендерен
- Error — при загрузке что-то пошло не так (сеть, сервер, версия бандла и др.)
Правильная работа с этими состояниями — ключ к хорошему UX.
Пример обработки всех состояний в Vue
Здесь я покажу пример с комментариями, чтобы вы могли адаптировать его под свой проект:
import { defineAsyncComponent } from "vue"
import Spinner from "./Spinner.vue"
import ErrorMessage from "./ErrorMessage.vue"
const AsyncChart = defineAsyncComponent({
loader: () => import("./Chart.vue"),
loadingComponent: Spinner, // Компонент со спиннером
errorComponent: ErrorMessage, // Компонент с текстом ошибки
delay: 300, // Ждем 300 мс перед показом спиннера
timeout: 15000, // Через 15 секунд считаем, что это ошибка
onError(error, retry, fail, attempts) {
// Логируем ошибку для аналитики
console.warn("Ошибка загрузки Chart", error)
if (error.message.includes("fetch")) {
// Ошибка сети - пробуем несколько раз
if (attempts <= 2) {
// Повторяем загрузку
retry()
} else {
// Показываем компонент ошибки
fail()
}
} else {
// Ошибка другого типа - сразу показываем ошибку
fail()
}
}
})
Предзагрузка и стратегия «ленивой, но не слишком»
Асинхронный компонент не обязательно ждать до прямого действия пользователя. Часто лучше «подготовить» компонент заранее, чтобы снизить задержку.
Ленивая загрузка по событию наведения
Один популярных подходов — начинать загрузку компонента при наведении курсора на кнопку или ссылку.
Простейший пример на React:
import React, { Suspense, useState } from "react"
const SettingsPanel = React.lazy(() => import("./SettingsPanel"))
function SettingsButton() {
const [open, setOpen] = useState(false) // Открыта ли панель
const [prefetched, setPrefetched] = useState(false) // Начали ли загрузку
// Обработчик наведения мыши
const handleMouseEnter = () => {
if (!prefetched) {
// Запускаем загрузку компонента
import("./SettingsPanel")
setPrefetched(true)
}
}
return (
<>
<button
onMouseEnter={handleMouseEnter}
onClick={() => setOpen(true)}
>
Открыть настройки
</button>
{open && (
<Suspense fallback={<div>Загрузка настроек...</div>}>
<SettingsPanel />
</Suspense>
)}
</>
)
}
export default SettingsButton
Что происходит:
// При первом наведении мыши импорт начинает загружаться
// К моменту клика компонент часто уже загружен
// Для пользователя это выглядит почти как мгновенное открытие
Типичные ошибки при работе с async-components
Теперь давайте разберем распространенные проблемы, с которыми вы можете столкнуться.
Ошибка: асинхронный компонент в критически важном месте
Иногда разработчики делают асинхронными компоненты, которые должны быть доступны сразу (например, хедер или основная форма на странице). В итоге:
- Пользователь видит спиннер или пустой блок там, где ожидает основной контент
- Страница кажется «тяжелой» и дерганой
Что с этим делать:
// Не выносите в async-components элементы, критичные для первого впечатления страницы
// Лучше оптимизируйте их размер, а код-сплитинг применяйте к второстепенным частям
Ошибка: слишком мелкое дробление компонентов
Другой перекос — делать асинхронными очень мелкие компоненты:
- Каждая карточка товара
- Каждая иконка
- Небольшие UI-элементы
Это ведет к:
// Большому количеству HTTP-запросов
// Сложной отладке
// Относительно маленькому выигрышу в размере бандла
Лучше делить по функциональным блокам: целые страницы, крупные виджеты, отдельные разделы.
Ошибка: нет обработки ошибок загрузки
Если вы нигде не обрабатываете ошибки загрузки, пользователь может увидеть:
- Бесконечный спиннер
- Пустое место без объяснения
- Невозможность вернуться к работе без перезагрузки страницы
Решение:
// Всегда добавляйте fallback и error-состояние
// Показывайте пользователю понятный текст и кнопку «Повторить»
// Логируйте ошибки, чтобы видеть реальные проблемы
Практические рекомендации по проектированию async-components
Подытожим практику и сделаем чек-лист, на который вы можете опираться при проектировании.
Что стоит выносить в асинхронные компоненты
- Страницы и маршруты
- Тяжелые виджеты (графики, карты, редакторы)
- Разделы с ограниченным доступом (админка)
- Редко используемые модальные окна с большим количеством логики
- Отдельные вкладки в интерфейсе, если каждая вкладка сильно нагружена логикой
Что обычно не стоит выносить
- Общий layout (шапка, футер, навигация)
- Простые контролы (кнопки, инпуты, небольшие карточки)
- Низкоуровневые утилиты и хелперы
- Глобальное состояние (стор, контексты)
Мини-чек-лист перед тем как сделать компонент асинхронным
Вы можете задать себе несколько вопросов:
- Насколько этот компонент тяжелый по коду и зависимостям
- Используется ли он на всех страницах или только в некоторых сценариях
- Критичен ли он для первого визуального впечатления
- Можно ли предзагрузить его заранее, чтобы скрыть задержку
- Насколько сложным станет код приложения после такого разделения
Если компонент большой, редко используется и не критичен для первого рендера — он почти идеальный кандидат для async-component.
Заключение
Асинхронные компоненты — это не просто удобная возможность фреймворка, а важный инструмент управления производительностью и архитектурой фронтенда. Они позволяют разделять приложение на логические куски и загружать код только тогда, когда он действительно нужен пользователю.
Мы разобрали:
- Чем асинхронный компонент отличается от обычного
- Как реализовать async-components на примере Vue и React
- Как они связаны с динамическими импортами и code splitting
- Какие паттерны применения чаще всего используются
- С какими типичными ошибками можно столкнуться
Дальше, если вы хотите углубиться, имеет смысл посмотреть, как именно ваш сборщик (Webpack, Vite и другие) формирует чанки, как включить prefetch и preload, и как измерять реальный эффект от использования async-components с помощью инструментов производительности браузера.
Частозадаваемые технические вопросы по теме и ответы
Вопрос 1. Как отложить инициализацию глобального состояния при использовании асинхронного компонента
Иногда внутри async-component вы инициализируете глобальный стор или тяжелые контексты. Чтобы не тянуть их в основной бандл, вынесите инициализацию в отдельный модуль и импортируйте его динамически внутри асинхронного компонента:
// Внутри асинхронного компонента
async setup() {
// Здесь мы лениво импортируем модуль состояния только при открытии компонента
const { useReportStore } = await import("../stores/reportStore.js")
const reportStore = useReportStore() // Инициализация стора
// Дальше используем reportStore как обычно
}
Так стор будет загружаться только вместе с тяжелым компонентом.
Вопрос 2. Как обеспечить типизацию асинхронных компонентов в TypeScript
В React и Vue с TypeScript важно подсказать типы для ленивых компонентов. Для React можно сделать вспомогательный тип:
// Обертка над React.lazy с типизацией пропсов
export function lazyTyped<T extends React.ComponentType<any>>(
factory: () => Promise<{ default: T }>
) {
return React.lazy(factory)
}
// Использование
const TypedPage = lazyTyped<typeof Page>(() => import("./Page"))
Так вы сохраните автодополнение пропсов в IDE даже для async-components.
Вопрос 3. Как повторно использовать один и тот же асинхронный компонент в разных местах без дублирования чанков
Сборщик, как правило, сам позаботится о том, чтобы модуль был в одном чанке. Главное — использовать одинаковый путь импорта. Если вы хотите гарантировать единый чанк, вынесите объявление асинхронного компонента в отдельный файл:
// components/AsyncChart.js
import { defineAsyncComponent } from "vue"
export const AsyncChart = defineAsyncComponent(() =>
import("./Chart.vue")
)
И дальше импортируйте AsyncChart везде, где он нужен. Тогда сборщик не будет создавать дубликаты чанков.
Вопрос 4. Что делать, если асинхронный компонент нужно использовать на серверном рендеринге
Для SSR нужно убедиться, что:
- Сборщик умеет генерировать манифест чанков
- Серверный рендерер знает, какие чанки использовать
Во Vue 3 с Vite это делается через manifest.json, который сервер читает и вставляет нужные <script> и <link> теги. В React обычно используют фреймворки уровня Next.js, где поддержка SSR и async-components встроена. Практически это означает, что вы используете их dynamic или аналогичный API вместо ручного lazy.
Вопрос 5. Как принудительно обновить асинхронный компонент при обновлении версии бандла
Иногда пользователю нужно сбросить кеш старого чанка. Один из подходов:
- Встраиваете версию приложения в HTML (например, через meta-тег)
- При загрузке async-component сравниваете текущую версию с сохраненной
- Если версии различаются — выполняете
location.reload()или очищаете Service Worker
Минимальный пример проверки версии перед загрузкой:
export async function loadVersionedComponent() {
// Здесь мы читаем версию из глобальной переменной, внедренной при сборке
const currentVersion = window.__APP_VERSION__
const storedVersion = localStorage.getItem("app_version")
if (storedVersion && storedVersion !== currentVersion) {
// Версия сменилась - обновляем страницу
localStorage.setItem("app_version", currentVersion)
location.reload()
return
}
localStorage.setItem("app_version", currentVersion)
// Дальше обычная загрузка компонента
return import("./HeavyComponent.js")
}
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев