Оптимизация бандла: code splitting, tree shaking
Зачем оптимизировать
Проблема:
- Bundle 5 MB — долго скачивается.
- Парсинг JS — сотни мс.
- LCP страдает.
- Мобильные — 3G.
Что даёт:
- Быстрее загрузка.
- Меньше трафика.
- Лучше CWV.
- Дешевле CDN.
Правило: бандл — первый bottleneck фронта.
Что внутри бандла
Что типично:
- React / Vue / Next — ~50 KB gzip.
- Router — 10–20 KB.
- UI-lib — 30–100 KB.
- Твой код — 30–300 KB.
- Полифиллы — 20 KB.
Что должно быть:
- < 200 KB gzip для initial.
- < 500 KB — предел.
- > 1 MB — проблема.
Правило: initial bundle — < 200 KB gzip.
Bundle Analyzer
Vite:
npm install -D rollup-plugin-visualizer
// vite.config.js
import { visualizer } from 'rollup-plugin-visualizer';
export default {
plugins: [visualizer({ open: true, gzipSize: true })],
};
Next.js:
npm install -D @next/bundle-analyzer
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
});
module.exports = withBundleAnalyzer({});
ANALYZE=true npm run build
Правило: без analyzer — не видно, что внутри.
Что искать в analyzer
- Крупные пакеты.
- Дубли (moment + date-fns).
- Полные lodash.
- Момент (тяжёлый).
- Polyfills без нужды.
- Всё в initial chunk.
Правило: топ-5 крупных — план оптимизации.
Tree shaking
Что это: удаление неиспользуемого кода.
Что требует:
- ESM (import/export).
- Side-effect free код.
sideEffects: falseв package.json.
Что даёт: размер ↓ 20–50%.
CommonJS vs ESM
Плохо (CJS):
const _ = require('lodash');
Хорошо (ESM):
import debounce from 'lodash/debounce';
// или
import { debounce } from 'lodash-es';
Правило: ESM + named imports — tree-shakable.
sideEffects
package.json проекта:
{
"sideEffects": false
}
⚠️ Только если нет side-effects (CSS-импортов, инициализаций).
Или явно:
{
"sideEffects": ["*.css", "./src/setup.js"]
}
Правило: sideEffects — осторожно.
Импорты по частям
Плохо:
import _ from 'lodash';
_.debounce(...);
Хорошо:
import debounce from 'lodash/debounce';
Что даёт: в 10× меньше lodash.
Библиотеки
Замена тяжёлых:
- moment → date-fns / dayjs.
- lodash → es-toolkit / radash.
- axios → fetch.
- jQuery → vanilla.
Правило: каждая зависимость — осознанно.
Code splitting
Что это: разделение бандла на chunks.
Что даёт:
- Меньше initial.
- Загрузка по требованию.
- Кэш между релизами.
Правило: code splitting — обязательно в SPA.
Dynamic import
Базовое:
button.addEventListener('click', async () => {
const { default: Chart } = await import('chart.js');
new Chart(...);
});
Что даёт: chart.js загрузится по клику.
React lazy
import { lazy, Suspense } from 'react';
const Dashboard = lazy(() => import('./Dashboard'));
<Suspense fallback={<Spinner />}>
<Dashboard />
</Suspense>
Что даёт: отдельный chunk для Dashboard.
React Router lazy
const routes = [
{
path: '/dashboard',
lazy: () => import('./pages/Dashboard'),
},
];
Что даёт: chunk на каждый роут.
Правило: роуты — всегда lazy.
Next.js dynamic
import dynamic from 'next/dynamic';
const Chart = dynamic(() => import('./Chart'), {
ssr: false,
loading: () => <Spinner />,
});
Что даёт: chart только на клиенте.
Next.js App Router
Автоматически:
- Каждая страница — свой chunk.
loading.tsx— стриминг.Suspense— по требованию.
Правило: в App Router — ничего вручную.
Route-based
Плохо: всё в main.js.
Хорошо:
main.js — shell
catalog.js — каталог
product.js — товар
account.js — кабинет
admin.js — админка
Что даёт: админка не грузится для гостя.
Component-based
Для тяжёлых:
- Chart.js.
- Editor.
- Maps.
- Rich text.
- PDF viewer.
Правило: > 50 KB компонент — dynamic.
Prefetch
Что даёт: заранее загрузить.
React Router:
<Link to="/dashboard" prefetch="intent">Dashboard</Link>
Next.js Link:
<Link href="/dashboard" prefetch>Dashboard</Link>
Правило: prefetch — на hover.
Vendor chunk
Что даёт: кэш библиотек отдельно.
Vite:
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['react', 'react-dom'],
router: ['react-router-dom'],
},
},
},
},
Правило: vendor — меняется редко → кэш.
Minify
Что делает: удаляет пробелы, сокращает.
Vite: автоматически (esbuild).
Webpack: terser.
Правило: prod — всегда minify.
Compression
gzip / brotli:
gzip on;
gzip_types application/javascript text/css application/json;
brotli on;
brotli_types application/javascript text/css application/json;
Что даёт: gzip ↓ 70%, brotli ↓ 75%.
Правило: brotli — если поддерживает Nginx.
Preload / Prefetch
Preload критичного:
<link rel="preload" as="script" href="/main.js" crossorigin>
Prefetch будущего:
<link rel="prefetch" href="/dashboard.js">
Правило: preload — только критичное.
Что важно в 2026
- Vite — стандарт для SPA.
- Turbopack — для Next.js.
- esbuild — быстрая минификация.
- Brotli — дефолт.
- HTTP/3 — быстрее.
- RSC — Server Components.
Практика
1. Прогоните analyzer.
2. Найдите топ-5 крупных.
3. Замените moment на dayjs.
4. lodash — на es-toolkit.
5. Добавьте lazy для роутов.
6. Разделите vendor.
7. Включите brotli.
8. Замерьте размер.
9. Сравните initial bundle.
10. Проверьте Lighthouse.
Частые ошибки
1. import _ from 'lodash':
Весь lodash
Решение: named / path.
2. Без analyzer:
Не видно что внутри
Решение: включить.
3. Всё в initial:
5 MB бандл
Решение: lazy.
4. Без minify:
+30%
Решение: prod build.
5. Без gzip/brotli:
Больше трафик
Решение: Nginx.
6. Polyfills без нужды:
+20 KB
Решение: по browserslist.
7. Preload всего:
Конкуренция
Решение: только критичное.
8. Один vendor:
Нет кэша
Решение: split.
Итог
- < 200 KB gzip.
- Analyzer — обязательно.
- Tree shaking — ESM.
- Named imports — правило.
- Замена moment / lodash.
- Lazy — для роутов.
- Vendor — отдельно.
- Brotli — дефолт.
- Preload — критичное.
- Замеры до/после.
Дальше — lazy-loading компонентов и изображений.