Связь code splitting с форматом вывода

Механизм code splitting в Rollup тесно связан с выбранным форматом генерации выходных файлов. Возможность разделения кода определяется не только настройками сборщика, но и тем, способен ли целевой формат представлять несколько взаимосвязанных модулей.

При использовании одного входного файла Rollup обычно создаёт один выходной файл. Однако при наличии нескольких точек входа или динамических импортов сборщик может разбивать проект на набор отдельных чанков. Такие чанки должны сохранять связи между собой, а это возможно не во всех форматах модулей.

Основные форматы вывода Rollup:

export default {
    input: 'src/main.js',
    output: {
        dir: 'dist',
        format: 'es'
    }
};

Поддержка code splitting зависит от значения свойства format.


Формат ES Modules (es)

Формат es является наиболее естественной средой для code splitting.

Каждый сгенерированный чанк остаётся полноценным ES-модулем, а связи между файлами оформляются через стандартные инструкции import и export.

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

// main.js
import('./dashboard.js');

После сборки:

dist/
├── main.js
└── dashboard-8f7c1a.js

Содержимое основного файла:

import('./dashboard-8f7c1a.js');

Содержимое чанка:

export function init() {
    console.log('Dashboard');
}

Преимущества формата:

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

По этой причине большинство современных проектов используют именно format: 'es'.


Формат CommonJS (cjs)

Формат CommonJS также поддерживает разделение кода.

При генерации нескольких чанков Rollup создаёт набор файлов, которые взаимодействуют через функцию require.

Конфигурация:

export default {
    input: 'src/main.js',
    output: {
        dir: 'dist',
        format: 'cjs'
    }
};

Результат:

dist/
├── main.js
├── dashboard.js
└── vendor.js

Внутри файлов используются конструкции:

const vendor = require('./vendor.js');

или

Promise.resolve().then(() => require('./dashboard.js'));

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

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

Формат AMD (amd)

Формат AMD исторически создавался для загрузки модулей по требованию, поэтому способен эффективно работать с code splitting.

Конфигурация:

export default {
    input: 'src/main.js',
    output: {
        dir: 'dist',
        format: 'amd'
    }
};

Генерируемые файлы используют конструкцию:

define(['./chunk.js'], function(chunk) {
    // код
});

Для динамических импортов применяется механизм загрузчика AMD:

require(['./dashboard.js'], function(module) {
    module.init();
});

Такой подход хорошо работает совместно с загрузчиками вроде RequireJS, однако сегодня используется значительно реже, чем ES Modules.


Формат SystemJS (system)

Формат system предназначен для работы через загрузчик SystemJS и полностью поддерживает разбиение проекта на чанки.

Конфигурация:

export default {
    input: 'src/main.js',
    output: {
        dir: 'dist',
        format: 'system'
    }
};

Сгенерированные файлы имеют вид:

System.register([], function(exports) {
    return {
        execute() {
            // код
        }
    };
});

При динамическом импорте создаются отдельные файлы, которые загружаются через SystemJS.

Преимущества:

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

Недостатком является необходимость подключения дополнительной библиотеки SystemJS.


Почему формат IIFE не поддерживает code splitting

Формат iife предназначен для генерации одного самодостаточного файла.

Пример:

export default {
    input: 'src/main.js',
    output: {
        file: 'bundle.js',
        format: 'iife'
    }
};

Результат представляет собой немедленно вызываемую функцию:

(function () {
    'use strict';

    console.log('Application');
})();

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

Попытка использовать code splitting приводит к ошибке:

Invalid value for option "output.format" -
UMD and IIFE output formats are not supported
for code-splitting builds.

Причина проста: формат не предусматривает существование связанных файлов.


Почему формат UMD не поддерживает code splitting

UMD разрабатывался как универсальная оболочка для различных систем модулей.

Пример:

(function (global, factory) {
    typeof exports === 'object'
        ? factory(exports)
        : typeof define === 'function' && define.amd
        ? define(['exports'], factory)
        : factory(global.library = {});
})(this, function (exports) {
    // код
});

Каждый UMD-файл считается самостоятельной библиотекой.

Если бы проект был разделён на несколько чанков, возникли бы проблемы:

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

Поэтому Rollup запрещает code splitting для umd.


Связь между output.file и output.dir

Одним из признаков использования code splitting является необходимость применения свойства output.dir.

Для одного файла:

output: {
    file: 'dist/bundle.js',
    format: 'es'
}

Для нескольких чанков:

output: {
    dir: 'dist',
    format: 'es'
}

Причина очевидна: сборщик не знает заранее количество генерируемых файлов.

При разбиении проекта могут появляться:

  • входные чанки;
  • общие чанки;
  • динамические чанки;
  • файлы ресурсов.

Поэтому необходимо указывать каталог, а не конкретное имя файла.


Множественные точки входа и ограничения форматов

Code splitting часто возникает при использовании нескольких входных модулей.

Конфигурация:

export default {
    input: {
        admin: 'src/admin.js',
        client: 'src/client.js'
    },
    output: {
        dir: 'dist',
        format: 'es'
    }
};

Rollup анализирует зависимости:

admin.js
   └── utils.js

client.js
   └── utils.js

Общий модуль автоматически выносится:

dist/
├── admin.js
├── client.js
└── utils-3f91ab.js

Такой механизм работает только для форматов, поддерживающих связи между несколькими файлами.

Для iife и umd подобная конфигурация невозможна.


Динамические импорты и формат вывода

Особенно важна связь между динамическими импортами и форматом сборки.

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

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

    module.start();
});

При формате es создаётся отдельный файл:

main.js
analytics.js

При формате cjs:

main.js
analytics.js

с использованием require.

При формате amd:

main.js
analytics.js

с использованием механизма define и require.

При формате iife Rollup не сможет корректно представить такой импорт и завершит сборку ошибкой.


Автоматическое создание общих чанков

Поддерживающие code splitting форматы позволяют Rollup выделять общие зависимости в отдельные файлы.

Например:

import lodash from 'lodash';

используется в нескольких точках входа.

Вместо дублирования кода Rollup создаёт:

dist/
├── app.js
├── admin.js
└── vendor.js

Оба входных файла получают ссылки на общий чанк:

import './vendor.js';

или

const vendor = require('./vendor.js');

в зависимости от формата.

Подобная оптимизация невозможна для форматов, предполагающих существование только одного итогового файла.


Выбор формата для проектов с code splitting

На практике сложились следующие рекомендации:

Формат Поддержка code splitting
es Полная
cjs Полная
amd Полная
system Полная
iife Нет
umd Нет

Для браузерных приложений предпочтителен формат es.

Для серверных приложений Node.js чаще используется cjs либо современные ES-модули.

Для проектов с устаревшими загрузчиками могут применяться amd и system.

Если требуется использовать динамические импорты, множественные точки входа или автоматическое выделение общих зависимостей, выбор необходимо делать среди форматов es, cjs, amd и system, поскольку только они обладают встроенной моделью взаимодействия между несколькими генерируемыми чанками.