Оптимизация JavaScript-бандла
Тяжёлый JS — главная причина плохого INP и медленного FCP для SPA. Браузер должен загрузить, распарсить и выполнить весь JS до рендеринга. Оптимизация бандла — разбивка на части, удаление неиспользуемого кода, отложенная загрузка.
Анализ бандла
# Vite — визуализация через rollup-plugin-visualizer
npm install -D rollup-plugin-visualizer
// vite.config.ts
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true,
})
]
});
После сборки открывается интерактивная карта бандла. Ищем:
- Крупные библиотеки (moment.js, lodash — часто заменимы)
- Дублирование зависимостей
- Библиотеки импортированные целиком вместо нужной функции
Code Splitting — разбивка по маршрутам
// React Router v6 — lazy loading страниц
import { lazy, Suspense } from 'react';
import { createBrowserRouter, RouterProvider } from 'react-router-dom';
const ProductCatalog = lazy(() => import('./pages/ProductCatalog'));
const ProductDetail = lazy(() => import('./pages/ProductDetail'));
const Cart = lazy(() => import('./pages/Cart'));
const Checkout = lazy(() => import('./pages/Checkout'));
const router = createBrowserRouter([
{ path: '/catalog', element: <Suspense fallback={<PageSkeleton />}><ProductCatalog /></Suspense> },
{ path: '/products/:slug', element: <Suspense fallback={<PageSkeleton />}><ProductDetail /></Suspense> },
{ path: '/cart', element: <Suspense fallback={<PageSkeleton />}><Cart /></Suspense> },
{ path: '/checkout', element: <Suspense fallback={<PageSkeleton />}><Checkout /></Suspense> },
]);
Динамический импорт тяжёлых компонентов
// Редактор, графики, карты — загружать только при необходимости
const RichTextEditor = lazy(() => import('./components/RichTextEditor'));
const Chart = lazy(() => import('./components/Chart'));
const YandexMap = lazy(() => import('./components/YandexMap'));
function ProductForm() {
const [showEditor, setShowEditor] = useState(false);
return (
<>
<button onClick={() => setShowEditor(true)}>
Добавить описание
</button>
{showEditor && (
<Suspense fallback={<div>Загрузка редактора...</div>}>
<RichTextEditor />
</Suspense>
)}
</>
);
}
Tree shaking — удаление неиспользуемого кода
// Плохо: импорт всего lodash (~70кБ gzip)
import _ from 'lodash';
const sorted = _.sortBy(products, 'price');
// Хорошо: импорт только нужной функции
import sortBy from 'lodash/sortBy';
const sorted = sortBy(products, 'price');
// Ещё лучше: нативный JS
const sorted = [...products].sort((a, b) => a.price - b.price);
// date-fns вместо moment.js
import { format, addDays } from 'date-fns'; // tree-shakeable
import { ru } from 'date-fns/locale';
Замена тяжёлых библиотек
| Библиотека | Замена | Экономия |
|---|---|---|
| moment.js (72кБ) | date-fns (только нужные функции) | ~60кБ |
| lodash (70кБ) | lodash-es + tree-shaking | ~50кБ |
| axios (13кБ) | native fetch | 13кБ |
| jquery (87кБ) | Нативный JS | 87кБ |
| react-icons (все иконки) | Только нужные из @heroicons | 100–500кБ |
Vite: ручное разбиение чанков
// vite.config.ts
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// Vendor chunk — редко меняется, долго кешируется
'vendor-react': ['react', 'react-dom', 'react-router-dom'],
'vendor-ui': ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
'vendor-query': ['@tanstack/react-query'],
'vendor-forms': ['react-hook-form', 'zod', '@hookform/resolvers'],
'vendor-charts': ['recharts'],
},
}
},
chunkSizeWarningLimit: 500,
}
});
Preload критичных чанков
// Prefetch следующей страницы при hover на ссылку
function PrefetchLink({ to, children }) {
const prefetch = () => {
import(`./pages/${to}`).catch(() => {});
};
return (
<Link to={to} onMouseEnter={prefetch} onFocus={prefetch}>
{children}
</Link>
);
}
Метрики размера бандла
Целевые значения для интернет-магазина:
| Чанк | Цель (gzip) |
|---|---|
| Первоначальный JS (critical path) | < 50 кБ |
| React + React DOM | ~42 кБ (фиксировано) |
| Страница каталога | < 30 кБ |
| Карточка товара | < 20 кБ |
| Корзина/Оформление | < 40 кБ |
Мониторинг размера в CI
# .github/workflows/bundle-size.yml
- name: Check bundle size
run: |
npm run build
MAIN_JS=$(ls dist/assets/index-*.js | xargs stat -c%s | head -1)
if [ "$MAIN_JS" -gt 200000 ]; then
echo "Bundle too large: ${MAIN_JS} bytes"
exit 1
fi
Срок оптимизации: 2–4 дня: анализ, code splitting, замена библиотек.







