Динамический импорт
Здесь будет встроен видео-разбор темы (5–7 минут, YouTube / RuTube / VK Видео) — тем, кто лучше воспринимает на слух, текст ниже не обязателен.
Статический import — договор, заключённый до старта: браузер соберёт все файлы, прежде чем выполнит первую строку. import() переписывает договор — модуль едет тогда, когда он действительно понадобился.
Статический import — часть скелета модуля: движок разбирает его ещё до выполнения кода. Поэтому путь обязан быть строковым литералом, а сама строка — стоять на верхнем уровне файла. import() устроен иначе: это выражение, которое выполняется в свой черёд и возвращает промис.
Промис разрешается объектом модуля — тем самым «пространством имён», где именованные экспорты лежат под своими именами, а export default — под ключом default. Пока промис не разрешился, никаких имён у вас нет: ни деструктуризации, ни вызовов.
Типичная картина: на странице есть кнопка «Открыть редактор», а сам редактор весит больше, чем вся остальная страница. Пока кнопку не нажали, этот код — мёртвый груз в главном потоке.
С import() запрос за файлом уходит в момент клика. Первый клик подождёт сеть лишние доли секунды, зато страница показывается и начинает отвечать раньше — а это ровно то, что человек замечает.
Если фичей пользуется меньше половины людей — её место за import(). Остальное решают замеры, а не вкус.
Загруженные модули живут в реестре: браузер держит их по URL. Второй import() того же адреса в сеть не идёт — он отдаёт уже готовый объект модуля, тот же самый по ссылке. Даже если два вызова стартовали одновременно, файл скачается один раз.
Значит, своё кеширование ради экономии трафика не нужно. Его добавляют по другой причине — чтобы не плодить лишние промисы и не запускать заново обвязку вокруг загрузки: спиннер, блокировку кнопки, обработку ошибки.
Второй повод — код, который нужен не всем. Полифил для старых браузеров, языковой пакет, панель администратора, экспорт в PDF: всё это грузится по условию, вычисленному уже во время работы. Статический import так не умеет — он безусловен по определению.
Путь у import() — обычное выражение, поэтому его можно собрать из переменной или получить из другой функции.
Сеть — вещь ненадёжная, и промис import() отклоняется, если файл не приехал. Ловите ошибку там же, где загружаете: страница обязана пережить отсутствие одной необязательной фичи.
Сборщик — webpack, Vite, esbuild — читает вызов import() как команду «режь тут». Всё, что достижимо только из динамического импорта, уезжает в отдельный чанк и в главный файл не попадает. Как именно идёт разрез, покажет самодельный сборщик в главе про сборку.
Выигрыш считается не в мегабайтах на диске, а во времени до момента, когда со страницей можно работать. Каждый килобайт JavaScript нужно скачать, разобрать и выполнить — и делается это в том же потоке, который рисует интерфейс.
| Что сравниваем | import ... from | import() |
|---|---|---|
| Когда срабатывает | до выполнения кода модуля | в момент вызова |
| Где можно писать | только на верхнем уровне модуля | где угодно, даже в обычном скрипте |
| Путь | только строковый литерал | любое выражение |
| Результат | связанные имена | промис с объектом модуля |
| Вес первой загрузки | входит в главный бандл | уезжает в отдельный чанк |
Задержку первого клика лечат прогревом: начать загрузку заранее — скажем, когда курсор навёлся на кнопку, — и запомнить промис. К моменту клика файл чаще всего уже на месте.
Сборщику нужно заранее понимать, какие файлы вообще могут понадобиться. С import(`./locales/${lang}.js`) он справится: видит фиксированный префикс и расширение, нарежет чанк на каждый файл в папке. А путь, склеенный из произвольной строки или пришедший с сервера, он проанализировать не может — такого чанка в сборке просто не окажется.
Вторая ловушка — считать import() функцией. Это синтаксическая конструкция: её нельзя присвоить переменной, передать в map или вызвать через call. Нужен «загрузчик» как значение — заверните вызов в стрелку, как в примере выше.
И третье: путь считается от файла, в котором написан import(), а не от адреса страницы. Правило то же, что у статического импорта, но при вычисляемых путях о нём забывают чаще.
- import() — выражение, а не объявление: возвращает промис и работает в любом месте кода, включая ветку if и обычный скрипт.
- Промис разрешается объектом модуля: именованные экспорты под своими именами, export default — под ключом default.
- Ленивая загрузка снимает с первой отрисовки код, которым воспользуется меньшинство: страница отвечает раньше.
- Сборщик режет чанки по вызовам import(), но только если путь предсказуем — фиксированный префикс и расширение.
- Загрузка может провалиться: catch рядом с import() обязателен, иначе одна недоехавшая фича роняет страницу.
1/3Что окажется в переменной сразу после строки const m = import("./a.js");
2/3Какое утверждение про import() верно?
3/3Редактор вынесли за import() вместо статического импорта. Что изменится?
Загрузить панель статистики один раз
Функция ниже честно грузит модуль, но каждый вызов начинает загрузку заново. Перепишите её так, чтобы запрос уходил один раз, а все последующие вызовы возвращали тот же самый промис — даже если их сделали, пока файл ещё в пути.
Кешировать нужно сам промис, а не разрешённое значение: тогда два клика подряд получат один и тот же запрос, а не два.
Оператор ??= присваивает только тогда, когда слева null или undefined, — это ровно ленивая инициализация, но короче, чем if (!statsPromise). Сам модуль браузер и так скачал бы один раз: мы экономим не трафик, а лишние промисы и лишние запуски обвязки вокруг загрузки.
Локали по требованию
Напишите loadLocale(lang): для языков из списка ["ru", "en"] она грузит соответствующий файл из папки ./locales/, для любого другого — английский. Путь соберите шаблонной строкой с фиксированным префиксом и расширением, чтобы сборщик смог нарезать чанки заранее.
Ошибку загрузки перехватите и верните заглушку { t: (key) => key } — интерфейс тогда покажет ключи вместо переводов, но не сломается.
Список поддерживаемых языков решает две задачи сразу: подставляет запасной вариант и не даёт утечь в путь произвольной строке.
Обратите внимание на скобки вокруг объекта в стрелке: без них { прочтётся как начало тела функции. И на форму пути: меняется только ${safe}, а ./locales/ и .js зафиксированы — по такому шаблону сборщик соберёт чанк на каждый файл в папке.