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';
Практика
- Настрой route-based code splitting с React.lazy
- Добавь webpack-bundle-analyzer и найди самые тяжёлые зависимости
- Вынеси vendor-зависимости в отдельный chunk через splitChunks
- Используй dynamic import для загрузки тяжёлой библиотеки (moment, chart.js) по требованию
- Добавь
webpackPrefetchдля вероятных следующих маршрутов