Перейти к содержимому

Bundling / Code-splitting / Tree-shaking

Современные бандлеры (Webpack, Rollup, Vite, esbuild, Turbopack) решают задачи объединения модулей, удаления мёртвого кода и разбиения на чанки; TechLead должен объяснить, почему tree-shaking не работает с CommonJS и как правильно настроить code-splitting для снижения TTI.

// Tree-shaking: работает ТОЛЬКО с ES Modules (статический анализ импортов)
// ✅ tree-shakeable
export function add(a, b) { return a + b; }
export function subtract(a, b) { return a - b; }
import { add } from './math.js'; // subtract не попадёт в bundle
// ❌ НЕ tree-shakeable (CommonJS — динамический require)
// module.exports = { add, subtract };
// const { add } = require('./math'); // бандлер не знает что нужно
// package.json: sideEffects для агрессивного tree-shaking
// {
// "sideEffects": false, // все модули — pure, можно удалять неиспользуемое
// "sideEffects": ["*.css", "*.svg"] // только эти файлы имеют side effects
// }
// Анализ бандла — обязательный инструмент TechLead
// npx webpack-bundle-analyzer webpack-stats.json
// npx vite-bundle-visualizer
// npx source-map-explorer dist/app.js
// → находим дубли, неожиданно большие зависимости, неправильный tree-shaking
// Code Splitting: динамический import() → отдельный чанк, загружается по требованию
// Route-based splitting (React)
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
const Settings = React.lazy(() => import('./pages/Settings'));
function App() {
return (
<Suspense fallback={<PageSpinner />}>
<Routes>
<Route path="/dashboard" element={<Dashboard />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
);
}
// Prefetch: загрузить чанк заранее (до перехода на маршрут)
// Webpack: import(/* webpackPrefetch: true, webpackChunkName: "dashboard" */ './pages/Dashboard')
// Vite: автоматически добавляет <link rel="modulepreload"> в HTML
// Component-level splitting: тяжёлые зависимости (редакторы, чарты, PDF-viewer)
const PdfViewer = React.lazy(() => import('./PdfViewer'));
// PdfViewer + его зависимости → отдельный чанк, загружается только при открытии PDF
vite.config.ts
// Vite / Rollup: стратегия чанков для оптимального кэширования
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// vendor: редко меняется → долгий Cache-Control: immutable + content hash
vendor: ['react', 'react-dom'],
ui: ['@radix-ui/react-dialog', '@radix-ui/react-dropdown-menu'],
charts: ['recharts', 'd3-scale'],
// app-чанки: часто меняются → короткий кэш или без кэша
},
},
},
chunkSizeWarningLimit: 500, // KB — порог предупреждения
},
});
// Цели для First Load JS (gzip):
// Critical path (main + vendor chunks): < 200 КБ
// Lazy-loaded chunks: < 50 КБ каждый
// Метрика: @next/bundle-analyzer (Next.js) или vite-plugin-inspect
// HTTP/2 + многочисленные мелкие чанки vs HTTP/1.1 + monolith:
// HTTP/2 мультиплексирует → больше мелких чанков — ОК
// HTTP/1.1 (6 соединений) → оптимальнее несколько крупных чанков

Итог: Tree-shaking требует ES Modules и корректного sideEffects; code-splitting делит bundle на чанки для быстрого TTI; vendor-чанки обеспечивают долгое кэширование.