Skip to content
PПромтбук
RUEN
04Производительность

Анализ 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-analyzerTreemap чанков, что вошло в каждыйwebpack/Next.js
rollup-plugin-visualizerТо же для Rollup/ViteVite/Rollup
source-map-explorerЛюбой бандл с sourcemaps — пакет → размерУниверсально
Lighthouse TreemapВнутри отчёта Lighthouse, по реальной страницеПрод
BundlephobiaРазмер npm-пакета до установкиПеред npm i
import-cost (VSCode)Размер импорта прямо в редактореДев-цикл
next build --profilePer-route JS для Next.jsNext.js

Запусти минимум два: analyzer покажет структуру чанков, source-map-explorer — реальный вклад каждого модуля.

3. Top-N кандидатов на вылет

Отсортируй по size in initial bundle desc. Для каждого из топ-15 ответь:

ПолеЧто писать
Modulenode_modules/moment/locale/*
Size (gzip)42 kb
Used infeatures/dashboard/Chart.tsx:14
Why initialИмпортирован из root layout
StrategyLazy / replace / drop / tree-shake / locale-prune
EffortS / 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-js versions конфликтуют → дубликаты
  • В 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. План — что отдать

  1. Текущее состояние: initial JS gzip / parsed / LCP / TBT
  2. Бюджет: {{budget_kb}}, gap к нему
  3. Top-15 кандидатов с таблицей (см. п.3)
  4. Список замен библиотек (moment → date-fns и т.п.) с -KB прогнозом
  5. Список dynamic import переходов (где конкретно)
  6. CI guard конфиг
  7. Ре-замер после фиксов (сделай до и после одинаковым способом)

Анти-паттерны

  • ❌ Один analyzer — видишь чанки, но не реальный вклад модулей
  • import _ from 'lodash' ради одной функции — тащит 70kb
  • moment в новых проектах — давно есть native Intl / date-fns / dayjs
  • ❌ Lazy всё подряд — десятки крошечных чанков, RTT-ад
  • ❌ Замер на dev-сборке — minification и tree-shake выключены, цифры врут
  • ❌ Замер на localhost без throttling — мобильная реальность другая
  • ❌ Один pnpm dedupe без проверки lockfile — дубль может вернуться через peer
  • ❌ Polyfill в global scope, когда нужен только на одной странице
  • ❌ Cut без CI-guard — через месяц всё назад
  • ❌ «Tree-shake сам уберёт» для CJS-пакета — не уберёт
Похожие промты