Bundled vs external зависимости

Во время сборки проекта Rollup анализирует граф модулей и определяет, какие зависимости должны быть включены в итоговый бандл, а какие останутся внешними и будут загружаться отдельно. От правильного выбора между bundled и external зависимостями напрямую зависят размер сборки, совместимость пакета, скорость загрузки приложения и удобство распространения библиотек.

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

  • Bundled dependencies — включаются внутрь результирующего файла.
  • External dependencies — исключаются из бандла и остаются внешними.

Рассмотрим каждую категорию подробно.


Bundled зависимости

Bundled-зависимость представляет собой модуль, код которого Rollup включает в итоговую сборку.

Исходный проект:

// main.js
import { sum } from './utils.js';

console.log(sum(5, 10));
// utils.js
export function sum(a, b) {
    return a + b;
}

После сборки Rollup может создать файл:

function sum(a, b) {
    return a + b;
}

console.log(sum(5, 10));

Модуль utils.js больше не существует как отдельный файл. Его содержимое встроено непосредственно в результат.

Именно такой процесс называется bundling.


Что происходит при bundling

Во время сборки Rollup:

  1. Находит точку входа.
  2. Анализирует все импорты.
  3. Строит дерево зависимостей.
  4. Объединяет используемый код.
  5. Удаляет неиспользуемые экспорты через Tree Shaking.
  6. Генерирует итоговый файл.

Например:

// math.js
export function add(a, b) {
    return a + b;
}

export function subtract(a, b) {
    return a - b;
}
// main.js
import { add } from './math.js';

console.log(add(2, 3));

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

function add(a, b) {
    return a + b;
}

console.log(add(2, 3));

Функция subtract() будет удалена как неиспользуемая.


Внешние зависимости (external)

External-зависимость не включается в результирующий бандл.

Вместо вставки её кода Rollup оставляет импорт или создаёт ссылку на внешний модуль.

Пример:

import lodash from 'lodash';

console.log(lodash.random(1, 10));

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

export default {
    input: 'src/main.js',
    external: ['lodash']
};

Результат:

import lodash from 'lodash';

console.log(lodash.random(1, 10));

Код библиотеки lodash в сборку не попадёт.


Зачем делать зависимости external

Причин может быть несколько.

Уменьшение размера сборки

Если библиотека уже присутствует в окружении выполнения, нет смысла включать её повторно.

Например:

external: ['react']

В этом случае React не будет встроен в бандл.

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


Избежание дублирования

Предположим, приложение уже использует React:

import React from 'react';

Создаётся собственная библиотека компонентов:

import React from 'react';

Если React встроить в библиотеку, получится две копии React:

  • одна внутри приложения;
  • другая внутри библиотеки.

Это может привести к ошибкам:

Invalid Hook Call
Hooks can only be called inside the body of a function component

Поэтому React практически всегда объявляется внешней зависимостью.


Снижение времени сборки

Чем меньше модулей обрабатывает Rollup, тем быстрее выполняется сборка.

Крупные зависимости могут значительно замедлять процесс бандлинга.


Гибкость обновления

Если зависимость остаётся внешней, пользователь библиотеки может самостоятельно выбирать её версию.

Например:

{
  "peerDependencies": {
    "react": "^18.0.0"
  }
}

Разработчик приложения сможет использовать собственную версию React.


Настройка external

Самый простой вариант:

export default {
    input: 'src/main.js',
    external: ['react']
};

Можно указать несколько пакетов:

external: [
    'react',
    'react-dom',
    'lodash'
]

Использование функции external

Для сложных сценариев применяется функция.

export default {
    external(id) {
        return id === 'react';
    }
};

Если функция возвращает true, модуль считается внешним.


Проверка по префиксу

Часто требуется исключить целую группу модулей.

export default {
    external(id) {
        return id.startsWith('@company/');
    }
};

Будут исключены:

@company/core
@company/ui
@company/utils

Исключение всех пакетов из node_modules

Иногда библиотека должна содержать только собственный код.

export default {
    external(id) {
        return !id.startsWith('.') &&
               !id.startsWith('/');
    }
};

Любой пакет из npm станет внешним.

Примеры:

react
lodash
rxjs
axios

Все они останутся external.


Bundled зависимости в приложениях

Для конечных приложений обычно используется противоположный подход.

Пример:

import axios from 'axios';
import dayjs from 'dayjs';

Пользователь сайта не обязан отдельно устанавливать эти пакеты.

Поэтому Rollup включает их в сборку.

Структура:

src/
 ├─ main.js
 ├─ api.js
 └─ components/

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

dist/
 └─ bundle.js

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


Bundled зависимости в библиотеках

Для библиотек ситуация сложнее.

Рассмотрим пакет:

import React from 'react';
import clsx from 'clsx';

Здесь обычно выбирается следующий подход:

external: ['react']

а clsx включается в бандл.

Причина проста:

  • React является частью окружения потребителя библиотеки.
  • clsx представляет собой небольшую вспомогательную утилиту.

Итог:

React      → external
clsx       → bundled

Peer Dependencies и external

Очень часто external используется вместе с peerDependencies.

Файл package.json:

{
  "peerDependencies": {
    "react": "^18.0.0"
  }
}

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

export default {
    external: ['react']
};

Такой подход считается стандартным для библиотек компонентов.


Разница между dependencies и peerDependencies

Рассмотрим пакет:

{
  "dependencies": {
    "lodash": "^4.17.21"
  }
}

Зависимость устанавливается автоматически.

Другой вариант:

{
  "peerDependencies": {
    "react": "^18.0.0"
  }
}

React должен присутствовать у потребителя библиотеки.

Поэтому большинство peerDependencies одновременно становятся external.


Формат UMD и external зависимости

Особое внимание требуется при сборке в формат UMD.

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

export default {
    input: 'src/index.js',
    external: ['react'],
    output: {
        format: 'umd',
        name: 'MyLibrary',
        globals: {
            react: 'React'
        }
    }
};

Здесь необходимо указать объект globals.

Без него Rollup не будет знать, какая глобальная переменная соответствует внешнему модулю.


Результат UMD

Получится конструкция вида:

(factory(global.React));

В браузере ожидается наличие:

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

которая создаёт объект:

window.React

Автоматическое определение внешних зависимостей

Во многих проектах external формируется автоматически.

Пример:

import pkg from './package.json';

export default {
    external: [
        ...Object.keys(pkg.dependencies || {}),
        ...Object.keys(pkg.peerDependencies || {})
    ]
};

Rollup будет автоматически исключать зависимости из package.json.

Это особенно удобно для библиотек с большим количеством пакетов.


Когда зависимость лучше включать в бандл

Включение зависимости обычно оправдано в следующих случаях:

  • зависимость маленькая;
  • она используется только внутри библиотеки;
  • отсутствует риск дублирования;
  • пакет редко обновляется;
  • необходимо упростить установку.

Пример:

import nanoid from 'nanoid';

Небольшие утилиты часто удобнее встроить в итоговую сборку.


Когда зависимость лучше делать external

External чаще используется, если:

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

Типичные примеры:

react
react-dom
vue
angular
svelte
rxjs

Проверка состава бандла

Иногда разработчик предполагает, что модуль является external, но на практике он всё равно попадает в сборку.

Проверить это можно несколькими способами:

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

Если внутри dist обнаруживается код сторонней библиотеки, значит она была включена в процесс bundling.


Практическая стратегия для приложений

Для приложений обычно применяется следующая модель:

Собственный код     → bundled
npm-зависимости     → bundled
Общие чанки         → bundled

Пользователь получает полностью готовую сборку.


Практическая стратегия для библиотек

Для библиотек чаще используется другая модель:

Собственный код     → bundled
Мелкие утилиты      → bundled
React/Vue и аналоги → external
peerDependencies    → external

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


Сравнение bundled и external зависимостей

Характеристика Bundled External
Попадает в итоговый файл Да Нет
Увеличивает размер бандла Да Нет
Требует отдельной установки Нет Обычно да
Участвует в Tree Shaking Да Нет
Контролируется Rollup Полностью Частично
Подходит для приложений Да Иногда
Подходит для библиотек Избирательно Очень часто
Исключает дублирование крупных фреймворков Нет Да

Понимание различий между bundled и external зависимостями является одной из ключевых частей проектирования сборки в Rollup. Именно на этом уровне принимаются решения о размере итогового пакета, совместимости библиотек и способе распространения JavaScript-кода.