Динамічний імпорт
Динамічний імпорт, це виклик функції import(), який повертає Promise і дозволяє завантажувати модулі під час виконання програми, а не під час збірки. Завдяки цьому в початковий бандл потрапляє лише те, що потрібно одразу, а решта коду довантажується на вимогу.
Теорія
TL;DR
import()це функціоподібний вираз, який повертає Promise з об'єктом модуля.- Статичний
importвиконується на етапі збірки і завжди потрапляє в бандл; динамічний виконується в runtime. - Шлях до модуля може бути змінною або шаблонним рядком, чого статичний
importне вміє. - Складальники використовують
import()як точку розрізу для code splitting. - Типові сценарії: lazy loading, локалізація, плагіни, важкі залежності, A/B тести.
- Помилку завантаження ловлять через
try/catchабо.catch(), бо це звичайний Promise.
Швидкий приклад
// Dynamic import: the module is fetched only when this line runs
const module = await import('./utils.js');
module.sayHello();
// Static import for comparison: resolved at build time, always in the bundle
// import { sayHello } from './utils.js';Синтаксис і що повертає import()
import() можна використовувати як з .then(), так і з await:
import(modulePath)
.then(module => {
// the module is loaded
module.default();
})
.catch(err => console.error(err));const module = await import('./math.js');
const { sum } = module;
console.log(sum(2, 3));Promise резолвиться об'єктом модуля, у якого є всі іменовані експорти та властивість default:
// math.js
export function add(a, b) { return a + b; }
export const PI = 3.14;const math = await import('./math.js');
console.log(math.add(2, 3)); // 5
console.log(math.PI); // 3.14Оскільки це звичайний Promise, помилки обробляються стандартно:
try {
const module = await import('./missing.js');
} catch (err) {
console.error('Module loading failed:', err);
}Умовне завантаження і динамічні шляхи
Модуль можна вибрати залежно від ситуації, наприклад для локалізації:
if (navigator.language.startsWith('fr')) {
const messages = await import('./lang/fr.js');
console.log(messages.default.hello);
} else {
const messages = await import('./lang/en.js');
console.log(messages.default.hello);
}Шлях може бути змінною або шаблонним рядком, що зі статичним import неможливо:
const theme = 'dark';
const module = await import(`./themes/${theme}.js`);
module.applyTheme();Важливо: складальник (наприклад, Webpack) все одно має вгадати шаблон шляху, інакше він не включить потрібні файли у збірку. Тому частина шляху має бути статичною, як ./themes/ у прикладі вище.
Класичний випадок, завантаження за дією користувача:
button.addEventListener('click', async () => {
const { openModal } = await import('./modal.js');
openModal();
});Модуль modal.js завантажиться лише при першому кліку, а не на старті сайту, що економить трафік і пришвидшує запуск.
Коли використовувати динамічний імпорт
| Сценарій | Чому корисно |
|---|---|
| Lazy loading компонентів | Завантажуємо компонент тільки при переході на потрібну сторінку |
| Переклади та локалізація | Імпортуємо лише потрібну мову |
| Важкі залежності | Відкладене завантаження великих бібліотек (chart.js, moment, three.js) |
| Плагіни та модульна архітектура | Підключаємо функціональність на запит |
| SSR і code splitting | У Next.js, Vite, Webpack автоматичний розріз бандлів |
| Feature flags та A/B тести | Вантажимо різні модулі залежно від прапорця |
Як це працює під капотом
Складальники (Webpack, Rollup, Vite, esbuild) роблять три речі:
- Аналізують вирази
import('./path')у коді. - Виконують code splitting, тобто розбивають проєкт на окремі чанки.
- Під час виклику
import()довантажують потрібний чанк по мережі.
Приблизний вигляд результату збірки:
main.js // the main bundle
vendors~chart.js // the shared library chunk
chart.js // fetched on clickУ React і Next.js це загорнуто в готові хелпери:
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('../components/Chart'), {
ssr: false,
loading: () => <p>Loading...</p>,
});
export default function Page() {
return (
<div>
<h1>Data</h1>
<Chart />
</div>
);
}Компонент <Chart> підвантажиться лише у браузері і лише коли користувач відкриє сторінку.
Відмінності від require()
| Критерій | import() | require() |
|---|---|---|
| Тип | Асинхронний (Promise) | Синхронний |
| Працює в | ESM (браузер, Node.js) | CommonJS (Node.js) |
| Динамічний шлях | Так | Обмежено |
| Підтримує tree-shaking | Так | Ні |
| Code splitting у складальниках | Так | Ні |
Підсумкова таблиця по самому import():
| Питання | Відповідь |
|---|---|
| Що робить | Завантажує модулі асинхронно, на вимогу |
| Що повертає | Promise з об'єктом модуля |
| Де працює | ES Modules (Node.js, браузер) |
| Для чого | Lazy loading, code splitting, A/B тести, динамічна локалізація |
| Синтаксис | const module = await import('./file.js') |
Типові помилки
- Забувають, що це Promise.
const m = import('./a.js'); m.foo()не спрацює, потрібенawaitабо.then(). - Повністю динамічний шлях.
import(userInput)складальник не зможе проаналізувати, і чанк не потрапить у збірку. Залишайте статичний префікс каталогу. - Немає обробки помилок. Мережа може впасти, і відхилений Promise без
catchперетворюється на unhandled rejection. - Динамічний імпорт усюди. Якщо модуль потрібен одразу на старті,
import()лише додає зайвий раунд-тріп по мережі. Дрібні утиліти краще імпортувати статично. - Виклик у циклі без кешу. Повторний
import()того самого шляху бере модуль з кешу модулів, але зайвийawaitу гарячому циклі все одно сповільнює код. - Очікують, що
import()дасть іменований експорт напряму. Він повертає весь об'єкт модуля, тому потрібна деструктуризація:const { sum } = await import('./math.js').
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.