JavaScript Dynamic import()
JavaScript dynamic import() — загрузка модулей по требованию: разделение кода, ленивая загрузка, обработка ошибок и примеры.
Динамические импорты в JavaScript — это возможность, появившаяся в ECMAScript 2020 (ES2020), позволяющая загружать модуль во время выполнения, по требованию, вместо того чтобы загружать всё сразу при первоначальном разборе скрипта. В отличие от статической инструкции import, форма import() является функционально-подобным выражением, которое возвращает промис, поэтому вы можете решить когда и нужно ли вообще загружать модуль — в зависимости от условий, действий пользователя или маршрутизации. Это руководство охватывает синтаксис, наиболее распространённые сценарии использования (разделение кода, ленивая загрузка, условная загрузка), обработку ошибок и полноценный рабочий пример.
В этой главе предполагается, что вы знакомы с ES-модулями и async/await.
Статический import и dynamic import()
Статическая инструкция import должна находиться на верхнем уровне модуля и полностью разрешается до того, как начнёт выполняться любой код модуля. Это делает её предсказуемой и удобной для инструментов сборки, однако это также означает, что все статически импортированные модули загружаются заранее — даже тот код, до которого пользователь может никогда не добраться.
import() отличается тремя важными свойствами:
- Это выражение, а не инструкция, поэтому оно может использоваться где угодно — внутри
if, в функции или в обработчике событий. - Оно принимает динамический спецификатор: путь к модулю может быть переменной или вычисляемой строкой, а не только строковым литералом.
- Оно возвращает промис, который разрешается в объект пространства имён модуля (объект, свойствами которого являются именованные экспорты модуля, а также
default).
// Static import — runs at parse time, must be top-level
import { formatDate } from './utils.js';
// Dynamic import — runs when this line executes, can be anywhere
const utils = await import('./utils.js');
utils.formatDate(new Date());Так как import() возвращает промис, результат обрабатывается с помощью await (внутри async-функции) или с помощью .then()/.catch():
// With await
const mod = await import('./utils.js');
// With .then() / .catch()
import('./utils.js')
.then(mod => mod.formatDate(new Date()))
.catch(err => console.error('Failed to load module:', err));Чтение экспортов из объекта модуля
Разрешённое значение — это объект пространства имён модуля. Именованные экспорты являются его свойствами; экспорт по умолчанию доступен под ключом default. Деструктуризация делает это удобнее:
// math.js exports: export function add(a,b){...}, export default function greet(){...}
const { add, default: greet } = await import('./math.js');
console.log(add(2, 3)); // 5
console.log(greet()); // "hello"await import(...) работает только внутри async-функции или на верхнем уровне ES-модуля (top-level await). В обычном <script> или в не-async-функции вместо этого используйте форму с .then().
Распространённые сценарии использования
Динамические импорты особенно эффективны, когда часть приложения используется условно или не нужна немедленно. Ниже приведены наиболее распространённые паттерны.
Разделение кода
Самый распространённый сценарий использования динамических импортов — это разделение кода (code splitting): разбивка бандла на небольшие фрагменты, которые загружаются только при необходимости — как правило, при переходе на маршрут или при использовании функции. В примере ниже тяжёлый скрипт загружается только после клика пользователя, а не раздувает исходную загрузку страницы.
button.addEventListener('click', function () {
import('./heavyScript.js').then(mod => {
mod.runHeavyTask();
});
});Поскольку обработчик клика синхронный, здесь вместо await используется форма с .then(). Браузер (или бандлер) запрашивает heavyScript.js только при первом клике; последующие клики используют кешированный модуль.
Измеряйте перед разделением. Слишком большое количество маленьких динамических фрагментов может ухудшить производительность — каждый из них является отдельным сетевым запросом. Используйте динамические импорты только для действительно большого или редко используемого кода.
Ленивая загрузка компонентов
Такие фреймворки, как React, Angular и Vue, используют динамические импорты под капотом для ленивой загрузки компонентов — компонент загружается только при первом рендере.
// Lazy loading a component in React
const LazyComponent = React.lazy(() => import('./LazyComponent'));
function App() {
return (
<React.Suspense fallback={<div>Loading...</div>}>
<LazyComponent />
</React.Suspense>
);
}React.lazy оборачивает динамический импорт, а React.Suspense показывает fallback до тех пор, пока фрагмент не загрузится. Пользователь видит Loading... лишь на краткое время загрузки компонента.
Продвинутое использование динамических импортов
Условная загрузка
Поскольку import() является выражением, его можно обернуть в любое условие — флаг функциональности, пользовательскую настройку, окружение или даже локаль браузера.
if (user.prefersAdvancedMode) {
const advanced = await import('./advancedEditor.js');
advanced.init();
}Пользователи, которые никогда не включают расширенный режим, никогда не загрузят advancedEditor.js. Можно пойти ещё дальше с динамическим спецификатором — например, загружать разный модуль в зависимости от локали:
const locale = navigator.language.startsWith('fr') ? 'fr' : 'en';
const messages = await import(`./locales/${locale}.js`);
console.log(messages.default.greeting);Бандлерам, таким как Webpack и Vite, необходимо знать, какие файлы могут быть загружены. Полностью произвольный спецификатор (например, путь, сформированный из пользовательского ввода) не может быть обработан бандлером. Сохраняйте литеральную часть спецификатора (директорию и расширение) статической, как в примере с локалями выше.
Поддержка инструментов сборки и Node.js
Когда вы пишете import('./module.js'), бандлеры — такие как Webpack, Rollup и Vite — автоматически создают отдельный фрагмент и загружают его по требованию; как правило, дополнительная настройка не нужна. В браузере нативный import() поддерживается во всех современных браузерах.
import() также работает в Node.js (v12+), в том числе внутри CommonJS-файлов, и является стандартным способом загрузки ES-модуля из CommonJS-кода:
// Loading an ESM module from a CommonJS file
async function run() {
const { default: chalk } = await import('chalk');
console.log(chalk.green('Loaded an ESM package from CommonJS'));
}
run();Метаданные модуля через import.meta
Внутри модуля можно обращаться к import.meta для получения контекстной информации. Наиболее широко поддерживаемое поле — import.meta.url, содержащее URL текущего модуля; это удобно для разрешения соседних ресурсов:
// Resolve a JSON file relative to the current module
const dataUrl = new URL('./data.json', import.meta.url);
const data = await import(dataUrl, { with: { type: 'json' } });Полный пример: динамический виджет погоды
Виджет погоды будет динамически загружать модуль для получения данных о погоде только тогда, когда пользователь запросит их. Это идеальный сценарий для динамических импортов: загрузка потенциально тяжёлого кода взаимодействия с API откладывается до момента, когда он действительно нужен.
В примере используются три файла: HTML-страница, точка входа и лениво загружаемый модуль.
index.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dynamic Weather Widget</title>
</head>
<body>
<h1>Weather Widget</h1>
<button id="loadWeather">Load Weather</button>
<div id="weatherOutput">Click the button to load the weather.</div>
<script src="index.js"></script>
</body>
</html>index.js:
document.getElementById('loadWeather').addEventListener('click', async () => {
const output = document.getElementById('weatherOutput');
try {
const weatherModule = await import('./weatherModule.js');
const data = await weatherModule.loadWeather();
output.textContent = `Weather: ${data.weather}`;
} catch (err) {
output.textContent = 'Failed to load weather data.';
}
});Этот код запускает динамический импорт при взаимодействии с пользователем:
- Обработчик события: Привязывает обработчик клика к кнопке.
- Динамический импорт: Использует
await import()для загрузки модуля только при клике, сохраняя начальный бандл небольшим. - Обработка ошибок:
try...catchоборачивает иimport(), и вызов данных — если загрузка не удалась или запрос отклонён, отображается резервное сообщение.
Такой подход помогает сделать веб-страницы эффективными и отзывчивыми: ресурсы загружаются только тогда, когда это необходимо, и пользователь сразу получает обратную связь.
weatherModule.js:
export async function loadWeather() {
// Simulated API call
return new Promise(resolve => {
setTimeout(() => {
resolve({ weather: 'Sunny, 76°F' }); // Simulating weather data
}, 1000);
});
}Функция имитирует получение данных из удалённого источника без реального API: она разрешается после задержки в одну секунду, чтобы можно было увидеть отложенную загрузку в действии.
Разбор примера
- HTML-структура: Предоставляет кнопку и контейнер для вывода результата.
- Динамический импорт в действии: Клик по кнопке запускает загрузку
weatherModule.jsизindex.jsпо требованию. - Модуль погоды: Имитирует задержку API, демонстрируя, как динамические импорты откладывают тяжёлую или условную логику до момента, когда она действительно нужна.
Типичные ошибки
awaitвне модуля или async-функции. Top-levelawait import()работает только в ES-модулях; в обычных скриптах или не-async-коллбэках используйте.then().- Забытый
.default. Экспорт по умолчанию модуля доступен через свойствоdefaultразрешённого объекта, а не через сам объект. - Полностью динамические пути. Бандлеры не могут обработать путь, который они не могут проанализировать. Сохраняйте литеральную часть спецификатора (директорию и расширение) статической.
- Избыточное разделение. Каждый динамический фрагмент — это отдельный запрос. Разделяйте крупный или редко используемый код, а не каждый небольшой вспомогательный модуль.
Заключение
Динамический import() позволяет загружать модули по требованию, возвращая промис, который разрешается в объект пространства имён модуля. Он лежит в основе разделения кода, ленивой загрузки компонентов, условной загрузки и импортов с учётом локали — улучшая производительность при запуске при осмысленном применении. Сочетайте его с async/await и надёжной обработкой ошибок, и позвольте бандлеру превращать каждый import() в оптимизированный фрагмент.
Для более глубокого изучения обратитесь к ES-модулям: export и import, введению в модули и промисам.