Code Splitting

Code splitting — разделение JavaScript-бандла на несколько файлов (chunks), которые загружаются по требованию. Уменьшает начальный размер и ускоряет загрузку.

Зачем нужно

Без code splitting весь JavaScript приложения загружается одним файлом. Для SPA это может быть 500KB-2MB. Пользователь на главной странице загружает код для настроек, дашборда, админки — того, что ему сейчас не нужно. Code splitting загружает только то, что нужно.

Где используется

SPA (React, Vue, Angular), библиотеки с опциональными модулями, приложения с тяжёлыми зависимостями (графики, редакторы, карты).

Предпосылки

Lazy loading, webpack/Vite, модульная система (import/export)

Типы Code Splitting

1. Entry Points — разделение по входным точкам

// webpack.config.js
module.exports = {
  entry: {
    main: './src/index.js',
    admin: './src/admin.js',
    vendor: './src/vendor.js',
  },
  output: {
    filename: '[name].[contenthash].js',
  },
};

2. Dynamic Imports — разделение по требованию

// Webpack автоматически создаёт отдельный chunk
async function loadEditor() {
  const { Editor } = await import(
    /* webpackChunkName: "editor" */
    './components/Editor'
  );
  return new Editor();
}

// Именование чанков (webpack magic comments)
const Chart = () => import(
  /* webpackChunkName: "charts" */
  /* webpackPrefetch: true */
  './components/Chart'
);

3. Route-based Splitting — разделение по маршрутам

// React Router + lazy
import { lazy, Suspense } from 'react';

const routes = [
  { path: '/',         component: lazy( => import('./pages/Home')) },
  { path: '/products', component: lazy( => import('./pages/Products')) },
  { path: '/cart',     component: lazy( => import('./pages/Cart')) },
  { path: '/admin',    component: lazy( => import('./pages/Admin')) },
];

// Каждая страница — отдельный chunk:
// home.chunk.js      ~50KB
// products.chunk.js  ~80KB
// cart.chunk.js      ~30KB
// admin.chunk.js     ~120KB
// Вместо одного main.js ~280KB

Webpack — настройка splitChunks

// webpack.config.js
module.exports = {
  optimization: {
    splitChunks: {
      chunks: 'all', // 'all' | 'async' | 'initial'
      minSize: 20000,           // Минимум 20KB для создания chunk
      maxSize: 244000,          // Разбивать большие chunks
      minChunks: 1,             // Минимум 1 импорт
      maxAsyncRequests: 30,
      maxInitialRequests: 30,

      cacheGroups: {
        // Вендорные зависимости — отдельный chunk
        vendor: {
          test: /[\\/]node_modules[\\/]/,
          name: 'vendors',
          chunks: 'all',
          priority: 10,
        },

        // React отдельно (часто обновляется разработчиком, реже — библиотека)
        react: {
          test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
          name: 'react',
          chunks: 'all',
          priority: 20,
        },

        // Общий код (используется в 2+ местах)
        common: {
          minChunks: 2,
          name: 'common',
          chunks: 'all',
          priority: 5,
          reuseExistingChunk: true,
        },
      },
    },

    // Runtime — отдельный файл
    runtimeChunk: 'single',
  },
};

Vite — автоматический code splitting

// vite.config.js — Vite разделяет автоматически при dynamic import
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          // Ручная группировка
          vendor: ['react', 'react-dom'],
          charts: ['chart.js', 'd3'],
          ui: ['@radix-ui/react-dialog', '@radix-ui/react-popover'],
        },
        // Или функция:
        // manualChunks(id) {
        //   if (id.includes('node_modules')) {
        //     return 'vendor';
        //   }
        // },
      },
    },
    chunkSizeWarningLimit: 500, // Предупреждение при chunk > 500KB
  },
});

Анализ бандла

webpack-bundle-analyzer

npm install -D webpack-bundle-analyzer
// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');

module.exports = {
  plugins: [
    new BundleAnalyzerPlugin({
      analyzerMode: 'static',
      openAnalyzer: false,
      reportFilename: 'bundle-report.html',
    }),
  ],
};

Vite — rollup-plugin-visualizer

npm install -D rollup-plugin-visualizer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    visualizer({
      filename: 'bundle-report.html',
      open: true,
    }),
  ],
});

source-map-explorer

npx source-map-explorer build/static/js/*.js

Стратегии разделения

По маршрутам (основная стратегия)

main.js          — общий код, layout, навигация (~50KB)
home.chunk.js    — главная страница
about.chunk.js   — страница о нас
dashboard.chunk.js — дашборд (тяжёлый, графики)

По компонентам (для тяжёлых)

// Модальное окно — загружаем при открытии
const Modal = lazy( => import('./Modal'));

// Редактор — загружаем при фокусе
const RichEditor = lazy( => import('./RichEditor'));

// Карта — загружаем при скролле до секции
const Map = lazy( => import('./Map'));

Vendor splitting

vendors.js       — React, React DOM (~40KB gzipped)
charts.js        — Chart.js, D3 (~80KB gzipped)
main.js          — ваш код (~30KB gzipped)

Преимущество: vendors.js кэшируется надолго (редко меняется), main.js — часто обновляется.

Prefetch и Preload для chunks

// Webpack magic comments
const Dashboard = lazy( => import(
  /* webpackPrefetch: true */    // Загрузить в idle time
  './pages/Dashboard'
));

const Editor = lazy( => import(
  /* webpackPreload: true */     // Загрузить параллельно с parent
  './components/Editor'
));
<!-- Результат в HTML: -->
<link rel="prefetch" href="/dashboard.chunk.js">
<link rel="preload" href="/editor.chunk.js" as="script">

Частые ошибки

1. Дублирование зависимостей в chunks

// Проблема: lodash попал и в main.js, и в dashboard.chunk.js
// Решение: splitChunks.cacheGroups для общих зависимостей

2. Слишком много мелких chunks

100 чанков по 1-2KB = 100 HTTP-запросов
Лучше 10 чанков по 20KB

// Настройка: minSize: 20000 (20KB минимум)

3. Нет содержимого [contenthash] в имени файла

// ПЛОХО: кэш не инвалидируется
output: { filename: 'bundle.js' }

// ХОРОШО: хеш меняется при изменении кода
output: { filename: '[name].[contenthash].js' }

4. Code splitting для крошечных модулей

// ПЛОХО: отдельный chunk для 500 байт
const add = lazy( => import('./utils/add'));

// ХОРОШО: включить в основной бандл
import { add } from './utils/add';

Практика

  1. Настрой route-based code splitting с React.lazy
  2. Добавь webpack-bundle-analyzer и найди самые тяжёлые зависимости
  3. Вынеси vendor-зависимости в отдельный chunk через splitChunks
  4. Используй dynamic import для загрузки тяжёлой библиотеки (moment, chart.js) по требованию
  5. Добавь webpackPrefetch для вероятных следующих маршрутов

Связанные темы

Ресурсы