Поле output.inlineDynamicImports

Поле output.inlineDynamicImports управляет поведением Rollup при обработке динамических импортов (import()). По умолчанию Rollup воспринимает динамический импорт как точку разделения кода и создаёт отдельные чанки. При включении inlineDynamicImports все модули объединяются в один итоговый файл, а динамические импорты перестают генерировать отдельные чанки.

Поле относится к секции output:

export default {
    input: 'src/main.js',

    output: {
        file: 'dist/bundle.js',
        format: 'esm',
        inlineDynamicImports: true
    }
};

Как работает динамический импорт без inlineDynamicImports

Исходный код:

// main.js
button.addEventListener('click', async () => {
    const module = await import('./dialog.js');

    module.openDialog();
});
// dialog.js
export function openDialog() {
    console.log('dialog opened');
}

Стандартное поведение Rollup:

dist/
├── main.js
└── dialog-8d1f4.js

Главный файл содержит ссылку на дополнительный чанк:

import('./dialog-8d1f4.js');

Такой режим называется code splitting — разделение кода на независимые части.


Поведение при inlineDynamicImports: true

Если включить опцию:

output: {
    file: 'dist/bundle.js',
    format: 'esm',
    inlineDynamicImports: true
}

Rollup перестанет создавать дополнительные чанки:

dist/
└── bundle.js

Содержимое dialog.js будет встроено в итоговый бандл.


Что именно меняется внутри сборки

Без inlineDynamicImports

Rollup:

  1. Находит import()
  2. Создаёт отдельный чанк
  3. Генерирует механизм ленивой загрузки
  4. Оставляет асинхронную границу

Пример:

await import('./dialog-8d1f4.js');

С inlineDynamicImports

Rollup:

  1. Загружает все модули заранее
  2. Встраивает их в основной файл
  3. Удаляет физическое разделение чанков
  4. Сохраняет Promise-интерфейс динамического импорта

Пример результирующего кода:

Promise.resolve().then(() => dialogModule);

или:

async function () {
    return dialogModule;
}

Асинхронность формально сохраняется, но реальной загрузки файла уже нет.


Основная цель inlineDynamicImports

Главное назначение — получение единственного итогового файла, даже если проект использует import().

Это особенно важно в следующих случаях:

  • библиотеки;
  • CLI-инструменты;
  • standalone-скрипты;
  • userscript;
  • плагины;
  • embeddable widgets;
  • среды без поддержки дополнительных файлов;
  • серверные single-file deployment;
  • Electron preload scripts.

Ограничение: поддерживается только один entry

inlineDynamicImports несовместим с множественными входными точками.

Ошибка:

Invalid value for option "output.inlineDynamicImports" -
multiple inputs are not supported when "output.inlineDynamicImports" is true.

Нельзя:

export default {
    input: {
        app: 'src/app.js',
        admin: 'src/admin.js'
    },

    output: {
        dir: 'dist',
        inlineDynamicImports: true
    }
};

Разрешён только один input:

export default {
    input: 'src/main.js',

    output: {
        file: 'dist/bundle.js',
        inlineDynamicImports: true
    }
};

Совместимость с output.file и output.dir

Обычно inlineDynamicImports используют вместе с output.file.

Пример:

output: {
    file: 'dist/app.js',
    format: 'cjs',
    inlineDynamicImports: true
}

Использование с output.dir технически возможно, но теряет смысл, потому что Rollup всё равно генерирует один файл.


Почему Rollup по умолчанию не включает inlineDynamicImports

Разделение кода — одна из ключевых возможностей Rollup.

Стандартное поведение обеспечивает:

  • lazy loading;
  • уменьшение initial bundle size;
  • ускорение загрузки;
  • кэширование чанков;
  • независимую загрузку модулей;
  • оптимизацию SPA-приложений.

inlineDynamicImports отключает все преимущества code splitting.


Разница между статическим и динамическим импортом

Статический импорт

import { api } from './api.js';

Rollup всегда объединяет такой импорт в основной бандл.


Динамический импорт

const api = await import('./api.js');

По умолчанию создаётся отдельный чанк.


Динамический импорт с inlineDynamicImports

output: {
    inlineDynamicImports: true
}

Результат:

  • дополнительный файл не создаётся;
  • код api.js встраивается внутрь бандла;
  • import() превращается во внутренний Promise.

Практический пример

Исходная структура

src/
├── main.js
├── editor.js
└── preview.js

main.js

async function loadEditor() {
    const editor = await import('./editor.js');

    editor.start();
}

async function loadPreview() {
    const preview = await import('./preview.js');

    preview.render();
}

Конфигурация без inlineDynamicImports

export default {
    input: 'src/main.js',

    output: {
        dir: 'dist',
        format: 'esm'
    }
};

Результат:

dist/
├── main.js
├── editor-4f2a1.js
└── preview-a9c33.js

Конфигурация с inlineDynamicImports

export default {
    input: 'src/main.js',

    output: {
        file: 'dist/app.js',
        format: 'esm',
        inlineDynamicImports: true
    }
};

Результат:

dist/
└── app.js

Влияние на размер бандла

inlineDynamicImports почти всегда увеличивает размер стартового файла.

Причина очевидна:

  • весь код загружается сразу;
  • исчезает lazy loading;
  • исчезает deferred execution chunks;
  • все зависимости включаются в initial bundle.

Без inlineDynamicImports

main.js          80 KB
editor chunk    200 KB
preview chunk   150 KB

Initial load:

80 KB

С inlineDynamicImports

app.js 430 KB

Initial load:

430 KB

Когда inlineDynamicImports полезен

Single-file deployment

Некоторые среды требуют один JS-файл:

plugin.js

без:

chunk-XYZ.js

Node.js CLI

CLI-инструменты часто распространяются как единый файл:

node cli.js

Дополнительные чанки усложняют публикацию.


Browser extension

Расширения браузера иногда ограничивают структуру файлов.

Особенно:

  • content scripts;
  • injected scripts;
  • isolated world scripts.

Electron preload

Preload-скрипты обычно хотят:

  • один файл;
  • отсутствие runtime loader;
  • предсказуемую структуру.

Embeddable widgets

Виджеты для вставки на сторонние сайты часто публикуются так:

<script src="widget.js"></script>

Наличие дополнительных чанков ломает такую модель.


Когда inlineDynamicImports вреден

Большие SPA

Для фронтенд-приложений почти всегда лучше code splitting.

Иначе:

  • ухудшается TTI;
  • растёт initial load;
  • теряется lazy loading;
  • увеличивается расход памяти;
  • ухудшается кэширование.

Маршрутное разделение

Пример:

const page = await import('./pages/admin.js');

С inlineDynamicImports административная часть загрузится сразу, даже если пользователь никогда туда не перейдёт.


Большие зависимости

Особенно проблемно:

  • Monaco Editor;
  • Three.js;
  • Chart.js;
  • PDF.js;
  • WASM-модули.

Взаимодействие с format

format: esm

Работает наиболее естественно.

output: {
    format: 'esm',
    inlineDynamicImports: true
}

format: cjs

Rollup преобразует динамические импорты под CommonJS.

output: {
    format: 'cjs',
    inlineDynamicImports: true
}

format: iife

Частый сценарий для standalone-виджетов.

output: {
    file: 'dist/widget.js',
    format: 'iife',
    name: 'Widget',
    inlineDynamicImports: true
}

format: umd

Тоже поддерживается:

output: {
    format: 'umd',
    inlineDynamicImports: true
}

Взаимодействие с preserveModules

inlineDynamicImports несовместим с preserveModules.

Нельзя одновременно:

output: {
    preserveModules: true,
    inlineDynamicImports: true
}

Причина:

  • preserveModules сохраняет файловую структуру;
  • inlineDynamicImports объединяет всё в один файл.

Это противоположные стратегии сборки.


Внутренняя трансформация import()

Rollup не удаляет import() полностью.

Вместо этого создаётся внутренняя обёртка.


Исходный код

const module = await import('./feature.js');

Возможный результат

const module = await Promise.resolve().then(function () {
    return featureModule;
});

Так сохраняется совместимость с асинхронной моделью.


Влияние на tree shaking

Tree shaking продолжает работать.

Даже с inlineDynamicImports Rollup:

  • удаляет неиспользуемые exports;
  • устраняет dead code;
  • минимизирует итоговый бандл.

Однако весь используемый код всё равно попадает в основной файл.


Отличие от manualChunks

manualChunks

Позволяет вручную разделять код:

output: {
    manualChunks: {
        vendor: ['react']
    }
}

inlineDynamicImports

Наоборот, запрещает разделение:

output: {
    inlineDynamicImports: true
}

Отличие от preserveModules

preserveModules

Сохраняет отдельные файлы:

dist/
├── main.js
├── utils.js
└── api.js

inlineDynamicImports

Создаёт единый файл:

dist/
└── bundle.js

Типичные ошибки

Попытка использовать несколько input

Ошибка:

multiple inputs are not supported

Использование вместе с preserveModules

Ошибка конфликта стратегий сборки.


Ожидание lazy loading

Многие ошибочно предполагают, что import() продолжит лениво загружать код.

При inlineDynamicImports этого не происходит.


Неожиданное увеличение bundle size

После включения опции размер initial bundle может вырасти в несколько раз.


Типичный сценарий сборки библиотеки

export default {
    input: 'src/index.js',

    output: {
        file: 'dist/library.js',
        format: 'umd',
        name: 'MyLibrary',
        inlineDynamicImports: true
    }
};

Такой подход обеспечивает:

  • единый distributable-файл;
  • отсутствие внешних чанков;
  • простую публикацию;
  • совместимость с CDN;
  • простое подключение через <script>.

Типичный сценарий для Node.js CLI

export default {
    input: 'src/cli.js',

    output: {
        file: 'bin/cli.js',
        format: 'cjs',
        banner: '#!/usr/bin/env node',
        inlineDynamicImports: true
    }
};

Результат:

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

Архитектурная суть inlineDynamicImports

Опция меняет фундаментальную модель сборки:

Обычная модель

entry
 ├── chunk A
 ├── chunk B
 └── chunk C

inlineDynamicImports

entry
 └── single bundle

Краткая характеристика поведения

Поведение inlineDynamicImports: false inlineDynamicImports: true
Code splitting Да Нет
Дополнительные чанки Да Нет
Lazy loading Да Нет
Один итоговый файл Нет Да
Поддержка multiple input Да Нет
Совместимость с preserveModules Да Нет
Размер initial bundle Меньше Больше
Простота деплоя Ниже Выше