Анализ JS-бандла и реальные сокращения
Найти топ-кандидатов на вылет из бандла: webpack-bundle-analyzer / source-map-explorer / Lighthouse, dynamic import strategy, бюджет.
Проведи аудит JS-бандла {{app}} и подготовь план сокращения до бюджета {{budget_kb}} (gzip, initial).
Базовая аксиома: оптимизируешь не «весь бандл», а initial JS на критичной странице. Что грузится лениво — отдельная история. Без измерения не режь — выкинешь нужное.
1. Снять baseline
Зафиксируй три числа на главной странице (cold cache, throttled 4G):
- Initial JS transferred (gzip/brotli) — из DevTools Network → JS filter → sum
- Initial JS parsed (uncompressed) — для CPU-budget на мобиле
- LCP / TBT / INP из Lighthouse
Также: общий размер из stats.json (webpack --json) или vite build --report.
2. Инструменты — каждый показывает своё
| Инструмент | Что показывает | Когда |
|---|---|---|
| webpack-bundle-analyzer | Treemap чанков, что вошло в каждый | webpack/Next.js |
| rollup-plugin-visualizer | То же для Rollup/Vite | Vite/Rollup |
| source-map-explorer | Любой бандл с sourcemaps — пакет → размер | Универсально |
| Lighthouse Treemap | Внутри отчёта Lighthouse, по реальной странице | Прод |
| Bundlephobia | Размер npm-пакета до установки | Перед npm i |
| import-cost (VSCode) | Размер импорта прямо в редакторе | Дев-цикл |
| next build --profile | Per-route JS для Next.js | Next.js |
Запусти минимум два: analyzer покажет структуру чанков, source-map-explorer — реальный вклад каждого модуля.
3. Top-N кандидатов на вылет
Отсортируй по size in initial bundle desc. Для каждого из топ-15 ответь:
| Поле | Что писать |
|---|---|
| Module | node_modules/moment/locale/* |
| Size (gzip) | 42 kb |
| Used in | features/dashboard/Chart.tsx:14 |
| Why initial | Импортирован из root layout |
| Strategy | Lazy / replace / drop / tree-shake / locale-prune |
| Effort | S / M / L |
| Impact | -42kb initial |
4. Главные виновники (бьём по очереди)
A. Moment / Lodash целиком
// до — 280kb
import _ from 'lodash';
import moment from 'moment';
// после — 8kb
import debounce from 'lodash/debounce';
import { format } from 'date-fns'; // или dayjs / native Intl
B. Polyfills для уже-поддерживаемых браузеров
core-jsversions конфликтуют → дубликаты- В
browserslistуказан старый таргет, который вам не нужен @babel/preset-envбезuseBuiltIns: 'usage'
C. Дубликаты одной библиотеки разных версий
npm ls react/pnpm why react- Один пакет тянет
lodash@3, другойlodash@4— оба в бандле
D. CSS-in-JS runtime на странице, где CSS статичен
Замена на zero-runtime (vanilla-extract, Linaria) или Tailwind.
E. Иконки целым пакетом
// до — 600kb
import { FiHome } from 'react-icons/fi';
// после — 2kb
import FiHome from 'react-icons/fi/FiHome';
// ещё лучше — SVG-спрайт / inline SVG
F. Локализации
moment тянет все локали по умолчанию. Webpack-плагин IgnorePlugin / переход на date-fns с per-locale import.
G. Tree-shake не работает
Признаки: пакет помечен "sideEffects": true или CJS. Решения: ESM-форк, ручной импорт submodule, замена.
5. Стратегия dynamic import
Что выкидываем из initial → lazy:
- Маршруты не первой страницы → route-based split (Next/React Router делают сами, проверь, что не порвано)
- Тяжёлые виджеты ниже фолда →
dynamic(() => import('./Chart'), { ssr: false }) - Модалки / drawer / тултипы на ховер → lazy при первом открытии
- Admin-only код → отдельный чанк за гейтом роли
- Редкие форматы (PDF-вьюер, markdown-редактор, codemirror) → on-demand
// React example
const Chart = lazy(() => import('./Chart'));
// с предзагрузкой по hover
<button onMouseEnter={() => import('./Chart')} onClick={openChart}>
Анти-паттерн: lazy всё подряд. Each chunk = round-trip. Цель — initial < бюджета, но без 30 чанков по 2kb.
6. Tree-shaking diagnostics
# Webpack
npx webpack --json > stats.json
# открыть в webpack-bundle-analyzer
# или https://chrisbateman.github.io/webpack-visualizer/
# Vite
npm run build -- --mode production
# vite-bundle-visualizer auto-open
В отчёте проверь:
- Названия модулей в чанках — есть ли неожиданное
concatenated modules— хорошо (scope hoisting)- Дубли путей (
lodash/debounce.js× 3) — плохо
7. CI guard на регрессии
Добавь budget в CI — без него всё откатится через спринт:
// next.config.js / webpack performance
performance: { maxAssetSize: 250000, maxEntrypointSize: 250000, hints: 'error' }
Или size-limit:
"size-limit": [{ "path": ".next/static/chunks/main-*.js", "limit": "170 KB" }]
Прогон в PR → fail если бюджет превышен.
8. План — что отдать
- Текущее состояние: initial JS gzip / parsed / LCP / TBT
- Бюджет: {{budget_kb}}, gap к нему
- Top-15 кандидатов с таблицей (см. п.3)
- Список замен библиотек (moment → date-fns и т.п.) с -KB прогнозом
- Список dynamic import переходов (где конкретно)
- CI guard конфиг
- Ре-замер после фиксов (сделай до и после одинаковым способом)
Анти-паттерны
- ❌ Один analyzer — видишь чанки, но не реальный вклад модулей
- ❌
import _ from 'lodash'ради одной функции — тащит 70kb - ❌
momentв новых проектах — давно есть nativeIntl/date-fns/dayjs - ❌ Lazy всё подряд — десятки крошечных чанков, RTT-ад
- ❌ Замер на dev-сборке — minification и tree-shake выключены, цифры врут
- ❌ Замер на localhost без throttling — мобильная реальность другая
- ❌ Один
pnpm dedupeбез проверки lockfile — дубль может вернуться через peer - ❌ Polyfill в global scope, когда нужен только на одной странице
- ❌ Cut без CI-guard — через месяц всё назад
- ❌ «Tree-shake сам уберёт» для CJS-пакета — не уберёт
Аудит производительности (Core Web Vitals)
Глубокая проверка LCP, INP, CLS с привязкой к коду и приоритизированным планом исправлений.
Мастер-аудит сайта: 6 измерений за один проход
Orchestrator-аудит по 6 направлениям: UX, accessibility, performance, SEO, brand consistency, security. Quick scan + deep dive + приоритизированный план + композитная оценка + roadmap.
Performance budget по типам страниц
Бюджеты JS/CSS/images для разных типов страниц, целевые Web Vitals, enforcement в CI с конкретными порогами.