Явный code splitting через manualChunks

Code splitting в Rollup представляет собой механизм разбиения исходного графа модулей на несколько отдельных бандлов. В отличие от Webpack, где код-сплиттинг часто включается автоматически через динамические импорты, Rollup делает упор на предсказуемость и явную конфигурацию. Одним из ключевых инструментов управления разбиением кода является параметр manualChunks, позволяющий разработчику напрямую управлять тем, какие модули попадут в отдельные чанки.

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

Именно в этом месте появляется необходимость ручного управления разбиением:

  • предотвращение дублирования общих модулей
  • выделение библиотек в отдельные vendor-чанки
  • контроль структуры выходных файлов
  • оптимизация кеширования на уровне браузера

Параметр manualChunks: общая концепция

Опция output.manualChunks позволяет явно указать Rollup, как распределять модули по чанкам. Она может быть задана в двух формах:

  • объект
  • функция

Объектный формат

Объектный вариант используется, когда структура чанков заранее известна.

export default {
  input: 'src/main.js',
  output: {
    dir: 'dist',
    format: 'es',
    manualChunks: {
      vendor: ['lodash', 'axios'],
      utils: ['src/utils/math.js', 'src/utils/date.js']
    }
  }
};

В этом случае Rollup принудительно создаст:

  • chunk vendor с библиотеками lodash и axios
  • chunk utils с указанными локальными модулями

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

Функциональный формат manualChunks

Наиболее гибкий способ управления код-сплиттингом — функция:

manualChunks(id) {
  if (id.includes('node_modules')) {
    return 'vendor';
  }
}

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

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

Разделение vendor-кода

Наиболее распространённый сценарий — выделение зависимостей из node_modules в отдельный чанк:

manualChunks(id) {
  if (id.includes('node_modules')) {
    return 'vendor';
  }
}

Такой подход даёт несколько преимуществ:

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

В реальных проектах часто требуется более тонкая сегментация:

manualChunks(id) {
  if (id.includes('node_modules')) {
    const parts = id.split('node_modules/')[1].split('/');
    const pkgName = parts[0].startsWith('@')
      ? parts.slice(0, 2).join('/')
      : parts[0];

    return `vendor-${pkgName}`;
  }
}

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

Группировка по функциональным областям

В приложениях с выраженной архитектурой (например, feature-based structure) manualChunks используется для разбиения по доменам:

manualChunks(id) {
  if (id.includes('/src/features/auth/')) {
    return 'auth';
  }

  if (id.includes('/src/features/dashboard/')) {
    return 'dashboard';
  }
}

Такой подход позволяет:

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

Алгоритмическое формирование чанков

Функция manualChunks не ограничена простыми условиями. Она может реализовывать сложную логику на основе анализа путей:

manualChunks(id) {
  const match = id.match(/src\/modules\/([^/]+)\//);

  if (match) {
    return match[1];
  }
}

В этом случае каждый модуль внутри src/modules/* превращается в отдельный чанк, имя которого соответствует названию директории.

Такой подход особенно полезен в системах с plugin-архитектурой.

Влияние manualChunks на граф зависимостей

Rollup строит граф модулей до этапа генерации выходных файлов. manualChunks вмешивается на стадии partitioning, когда:

  • все зависимости уже известны
  • tree-shaking завершён
  • оптимизация графа выполнена

Это означает, что manualChunks не влияет на tree-shaking напрямую, но может косвенно изменить итоговый результат, если один модуль включается в разные чанки.

Дедупликация и повторное использование модулей

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

При использовании manualChunks важно учитывать:

  • слишком мелкие чанки увеличивают накладные расходы загрузки
  • слишком крупные чанки ухудшают кеширование
  • дублирование логики часто указывает на неправильную сегментацию

Совместимость с динамическими import()

manualChunks работает совместно с динамическими импортами. При наличии конструкции:

import('./feature.js');

Rollup создаёт отдельный чанк автоматически. Однако manualChunks может переопределять его структуру:

manualChunks(id) {
  if (id.includes('feature')) {
    return 'feature-bundle';
  }
}

Это позволяет объединять несколько динамических импортов в один общий chunk.

Типичные ошибки при использовании manualChunks

Слишком агрессивное дробление

Создание чанка на каждый файл приводит к избыточному числу HTTP-запросов и ухудшению производительности загрузки.

Нестабильные имена чанков

Использование случайных или изменяющихся значений приводит к потере кеширования:

manualChunks(id) {
  return `chunk-${Date.now()}`;
}

Такой подход полностью разрушает механизм долгосрочного кеша.

Игнорирование структуры зависимостей

Разделение без учёта реального графа приводит к дублированию кода и увеличению общего размера бандла.

Стратегии проектирования чанков

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

Vendor + App split

  • vendor — все зависимости node_modules
  • app — основная логика

Domain split

  • auth
  • dashboard
  • settings
  • shared

Library-based split

Каждая крупная библиотека получает отдельный chunk

Hybrid approach

Комбинация доменного и библиотечного разделения:

manualChunks(id) {
  if (id.includes('node_modules')) {
    return 'vendor';
  }

  if (id.includes('/features/auth/')) {
    return 'auth';
  }

  if (id.includes('/features/dashboard/')) {
    return 'dashboard';
  }
}

Влияние на производительность загрузки

Правильно настроенный manualChunks напрямую влияет на:

  • скорость первого рендера
  • эффективность HTTP/2 multiplexing
  • повторное использование кеша
  • размер критического пути загрузки

Особенно важно учитывать баланс между количеством запросов и размером каждого чанка. Rollup не оптимизирует это автоматически — ответственность полностью лежит на конфигурации.

Связь с output.manualChunks и file naming

manualChunks влияет только на структуру модулей, но не управляет именами файлов напрямую. Имена формируются через:

  • output.entryFileNames
  • output.chunkFileNames
  • output.assetFileNames

Типичная конфигурация:

output: {
  dir: 'dist',
  format: 'es',
  entryFileNames: '[name].js',
  chunkFileNames: '[name]-[hash].js'
}

Сочетание стабильных chunk names и хеширования позволяет добиться эффективного кеширования.

Поведение при конфликтующих правилах

Если модуль подходит под несколько условий manualChunks, приоритет определяется порядком выполнения логики функции. Например:

manualChunks(id) {
  if (id.includes('node_modules/react')) {
    return 'react';
  }

  if (id.includes('node_modules')) {
    return 'vendor';
  }
}

В этом случае React всегда попадёт в отдельный чанк, несмотря на общее правило для node_modules.

Ограничения механизма manualChunks

  • не влияет на runtime execution order напрямую
  • не заменяет dynamic import semantics
  • не выполняет рефакторинг кода, только перераспределение модулей
  • требует аккуратного управления именами и зависимостями

Механизм остаётся исключительно инструментом оптимизации сборки, а не архитектурной абстракцией приложения.