Синтаксис import() и его поддержка в Rollup

Механизм динамического импорта import() в стандарте ECMAScript представляет собой выражение, возвращающее Promise, который резолвит модуль в момент выполнения кода. В отличие от статического import, который обрабатывается на этапе парсинга и компиляции, динамический импорт выполняется во время исполнения, что открывает возможность ленивой загрузки кода и разделения бандла на отдельные чанки.

В контексте сборщика Rollup поддержка import() является ключевым элементом системы code splitting и напрямую влияет на то, как формируются выходные чанки, как организуется граф зависимостей и как осуществляется загрузка модулей в рантайме.


Выражение import(moduleSpecifier) возвращает Promise:

import('./module.js').then((module) => {
  module.init();
});

Особенности:

  • выполняется асинхронно;
  • возвращает объект модуля;
  • поддерживает только строковый спецификатор;
  • не может быть вызвано условно на уровне синтаксиса (но может находиться внутри условий выполнения).

Ключевая характеристика — динамичность разрешения модуля, что делает невозможным его статическую фиксацию без анализа выполнения кода.


Обработка import() в Rollup

Rollup рассматривает import() как точку разделения графа модулей на отдельные чанки. При встрече такого выражения происходит:

  • выделение зависимостей в отдельный асинхронный чанк;
  • построение графа lazy-загрузки;
  • генерация дополнительных файлов в output bundle;
  • замена вызова import() на загрузчик, соответствующий формату вывода.

Пример:

// entry.js
button.addEventListener('click', async () => {
  const module = await import('./heavy-module.js');
  module.run();
});

Rollup преобразует это в:

  • основной чанк (entry);
  • отдельный чанк heavy-module;
  • механизм загрузки чанка в runtime.

Code splitting и граф чанков

Динамический импорт является триггером code splitting. Rollup строит два уровня графа:

  • статический граф зависимостей;
  • граф асинхронных зависимостей.

При этом важно:

  • один модуль может попадать в несколько чанков при сложной структуре зависимостей;
  • Rollup выполняет дедупликацию;
  • общие зависимости могут быть вынесены в отдельные shared chunks (при определённых конфигурациях).

Условия, влияющие на разбиение

На формирование чанков влияют:

  • точка вызова import();
  • пересечение зависимостей между динамическими импортами;
  • конфигурация manualChunks;
  • стратегия output format (ESM, SystemJS, AMD и др.).

Пример manualChunks:

export default {
  input: 'src/main.js',
  output: {
    format: 'es',
    manualChunks(id) {
      if (id.includes('node_modules')) {
        return 'vendor';
      }
    }
  }
};

При этом динамические импорты продолжают формировать отдельные чанки поверх ручного разбиения.


Ограничения синтаксиса import() в Rollup

Rollup требует, чтобы аргумент import() был анализируемым:

Поддерживаемые случаи

import('./module.js');
import(`./modules/${name}.js`);

Ограничения

  • сложные выражения без статической части не поддерживаются без плагинов;
  • вычисляемые строки без шаблонной структуры блокируют анализ;
  • функции внутри import() недопустимы.

Пример проблемного кода:

import(getPath()); // Rollup не может определить зависимость

Динамические шаблоны и плагин dynamicImportVars

Для поддержки шаблонных путей используется механизм анализа переменных импорта:

import(`./pages/${page}.js`);

Rollup не может сам по себе гарантировать полный набор возможных модулей. Для этого используется плагин:

  • @rollup/plugin-dynamic-import-vars

Он выполняет:

  • статический анализ шаблонов;
  • генерацию возможных путей;
  • расширение графа зависимостей на этапе сборки.

Форматы вывода и трансформация import()

Поведение import() зависит от формата output.

ES Modules (es)

import('./module.js')

Остаётся в виде динамического ESM-загрузчика, если окружение поддерживает ESM.

SystemJS

Rollup преобразует:

System.import('./module.js')

или аналогичный runtime-вызов через System loader.

IIFE / UMD

В не-ESM форматах Rollup вынужден эмулировать динамическую загрузку:

  • через runtime loader;
  • через Promise-обёртки;
  • через глобальные функции загрузки чанков.

Это увеличивает объём runtime-кода.


Асинхронная модель загрузки чанков

При использовании import() Rollup генерирует:

  • основной бандл;
  • набор дополнительных чанков;
  • runtime loader (если формат требует).

Процесс выполнения:

  1. вызывается import();
  2. определяется имя чанка;
  3. выполняется загрузка (script tag / fetch / SystemJS);
  4. модуль регистрируется в runtime;
  5. Promise резолвится.

Взаимодействие с tree-shaking

Динамический импорт влияет на tree-shaking следующим образом:

  • код внутри динамически загружаемого модуля анализируется отдельно;
  • неиспользуемые экспорты внутри чанка могут быть удалены;
  • сам факт динамической загрузки предотвращает объединение модуля с основным чанком;
  • зависимости внутри import() становятся “ленивыми” точками графа.

Вложенные динамические импорты

Rollup поддерживает цепочки:

button.oncl ick = async () => {
  const a = await import('./a.js');
  const b = await a.load();
};

Каждый уровень:

  • добавляет новый чанк;
  • расширяет граф зависимостей;
  • может приводить к каскадной загрузке.

Дедупликация и общие зависимости

Если несколько динамических импортов используют один и тот же модуль:

  • Rollup выделяет его в shared chunk;
  • предотвращается дублирование кода;
  • runtime корректно управляет ссылками между чанками.

Ошибки и особенности выполнения

Типичные проблемы при работе с import():

Невозможность анализа пути

import(variable);

Решение: использовать статические шаблоны или плагин анализа.


Конфликты с CommonJS

Если модуль CommonJS используется через динамический импорт:

  • Rollup вставляет interop-обёртки;
  • экспорт становится Promise-совместимым объектом;
  • возможны дополнительные runtime-обвязки.

Задержка загрузки чанков

Каждый import():

  • увеличивает количество HTTP-запросов;
  • требует правильной стратегии кеширования;
  • зависит от конфигурации output.chunkFileNames.

Имя чанков и управление структурой output

Rollup позволяет контролировать структуру файлов:

output: {
  format: 'es',
  chunkFileNames: 'chunks/[name]-[hash].js'
}

Для динамических импортов это критично, поскольку:

  • имя чанка влияет на загрузчик;
  • хеш обеспечивает кеширование;
  • структура определяет организацию lazy-модулей.

Top-level await и import()

При использовании top-level await внутри модулей, загружаемых через import():

  • модуль может оставаться в состоянии pending;
  • Rollup сохраняет порядок зависимостей;
  • выполнение зависимых модулей блокируется до завершения await.

Влияние на архитектуру приложения

Использование import() в Rollup приводит к архитектурным изменениям:

  • переход к ленивой загрузке функциональных блоков;
  • разделение приложения на независимые области;
  • уменьшение начального размера бандла;
  • увеличение сложности графа загрузки.

Rollup в этом контексте выступает не просто сборщиком, а инструментом формирования runtime-архитектуры модульной системы.