Оптимізація 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 load сторінок
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 наступної сторінки при наведенні на посилання
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, заміна бібліотек.







