Vue

Асинхронные компоненты и code-splitting

В приложении есть редактор разметки с подсветкой синтаксиса и эмодзи-пикером: триста килобайт кода, которые открывает один пользователь из двадцати. Если этот код в основном бандле, его скачивают все, включая тех, кто редактор ни разу не откроет. Ленивые маршруты решают это для страниц, а defineAsyncComponent для отдельных компонентов внутри страницы. Расширенные опции добавляют управление: что показать во время загрузки, что при ошибке, через сколько миллисекунд и с каким таймаутом.

  • Редактор кода: Monaco грузится отдельным чанком только когда пользователь открыл редактор
  • Дашборд: тяжёлая библиотека графиков подгружается по требованию, а не на каждой странице
  • Модалка экспорта: PDF-генератор это ленивый компонент с loading-плейсхолдером и обработкой ошибки чанка
  • Чат: эмодзи-пикер с delay, чтобы спиннер не мигал при быстрой загрузке из кэша
  • Админка: тяжёлая таблица с виртуализацией грузится с timeout и error-компонентом на случай обрыва сети

Предварительные знания

  • Ленивые маршруты через динамический import из предыдущего урока
  • Краткая форма defineAsyncComponent и идея Suspense
  • Понимание сборки: что бандлер режет динамический import на отдельные чанки

Ленивая загрузка тяжёлых компонентов

Ленивые маршруты разбивают бандл по страницам. Но тяжёлый компонент может жить внутри обычной страницы: редактор кода, библиотека графиков, эмодзи-пикер. defineAsyncComponent с динамическим import выносит код такого компонента в отдельный чанк, который скачивается только при первом рендере, а не на старте приложения.

Решение принимают по двум критериям: вес компонента и частота использования. Тяжёлый и редко открываемый компонент это идеальный кандидат на ленивую загрузку. Лёгкий компонент, который виден всегда, выносить смысла нет: лишний сетевой запрос за чанком не окупится.

Динамический import это сигнал бандлеру (Vite, webpack) создать отдельный чанк. Сам по себе import() возвращает промис с модулем, а defineAsyncComponent оборачивает его в полноценный компонент Vue с управлением состоянием загрузки.

Какой компонент лучше всего подходит для ленивой загрузки через defineAsyncComponent?

loadingComponent и errorComponent

Расширенная форма defineAsyncComponent принимает объект опций. loadingComponent показывается, пока чанк скачивается, errorComponent при неудаче загрузки (например, обрыв сети или удалённый со временем старый чанк). Так компонент управляет своим состоянием загрузки сам, без обёртки в Suspense.

  • Объектная форма — Компонент сам показывает loading и error через опции. Не требует Suspense, состояние загрузки локально для этого компонента
  • Внутри Suspense — Состоянием загрузки руководит граница: единый fallback на группу async-зависимостей, ошибки через onErrorCaptured

errorComponent срабатывает не только на сетевой обрыв. После деплоя старые чанки могут исчезнуть, и у пользователя с открытой вкладкой ленивый импорт упадёт. errorComponent с кнопкой перезагрузки страницы это типичная защита от такого сценария.

Когда срабатывает errorComponent у асинхронного компонента?

delay и timeout

delay задаёт, через сколько миллисекунд показать loadingComponent. По умолчанию это 200 мс: если чанк пришёл из кэша мгновенно, спиннер вообще не мелькнёт, что убирает раздражающее мигание. timeout ограничивает максимальное ожидание: по его истечении вместо вечной загрузки показывается errorComponent.

ОпцияНазначениеТипичное значение
delayЗадержка перед показом loading200 мс
timeoutЛимит ожидания до error5000-10000 мс
loadingComponentUI во время загрузкиСпиннер, скелетон
errorComponentUI при провале загрузкиСообщение, кнопка повтора

delay против мигания, timeout против зависания. Маленький delay (или 0) держит спиннер с первой миллисекунды и подходит для заведомо медленных чанков. Без timeout медленная сеть оставит пользователя в бесконечной загрузке без обратной связи.

Зачем нужен ненулевой delay перед показом loadingComponent?

Стратегия разбиения бандла

Code-splitting это компромисс, а не бесплатная победа. Каждый чанк это отдельный сетевой запрос. Если нарезать приложение на сотни крошечных чанков, накладные расходы на запросы перевесят экономию веса. Разумная стратегия: дробить по крупным редко используемым кускам (маршруты, тяжёлые виджеты), а не по каждому мелкому компоненту.

Для часто открываемых тяжёлых компонентов помогает предзагрузка: чанк скачивают в фоне после первого рендера или по наведению на кнопку, чтобы к моменту клика он уже был в кэше. Так совмещают лёгкий старт и мгновенное открытие при реальном использовании.

Метрику смотрят в анализаторе бандла: какие чанки самые тяжёлые и какие тянут лишние зависимости. Ленивую загрузку наводят на доказанно тяжёлые куски, а не на каждый компонент подряд.

Почему дробление приложения на сотни крошечных чанков считается плохой стратегией?

Связь с другими темами

Урок раскрывает defineAsyncComponent. Рядом стоят Suspense и оптимизация рендеринга:

  • Suspense и асинхронные компоненты — Suspense берёт на себя состояние загрузки, когда async-компонент живёт внутри границы
  • Vapor Mode — Другой рычаг производительности: снижение стоимости рендеринга, а не веса бандла

Итог

  • defineAsyncComponent с динамическим import режет тяжёлый компонент в отдельный чанк, который грузится по требованию
  • Расширенный объект задаёт loadingComponent, errorComponent, delay и timeout для самостоятельного состояния компонента
  • delay задерживает показ loading-компонента, чтобы спиннер не мигал при мгновенной загрузке из кэша
  • timeout ограничивает ожидание: по его истечении показывается errorComponent вместо бесконечной загрузки
  • Объектная форма управляет состоянием сама, без Suspense; внутри Suspense состоянием загрузки руководит граница

Связанные уроки

  • vue-31-suspense-async — Краткая форма defineAsyncComponent и Suspense вводятся раньше, тут раскрываются её расширенные опции
  • vue-35-vapor-mode — И code-splitting, и Vapor Mode это способы снизить вес и стоимость рендеринга, но на разных уровнях
Асинхронные компоненты и code-splitting

0

1

Войти