Олег Марков
Бандл анализ - bundle analysis во фронтенде
Введение
Бандл-анализ (bundle analysis) — это процесс изучения итоговой сборки фронтенд-приложения, чтобы понять:
- какие модули попадают в бандл;
- сколько они весят;
- почему размер вырос после очередного коммита;
- что можно безопасно удалить или разделить.
Смотрите, здесь важно не только «сжать всё, что можно», а научиться осознанно управлять размером и структурой бандла. Без анализа сборка часто обрастает:
- дублирующимися зависимостями;
- неиспользуемыми компонентами;
- тяжёлыми библиотеками ради одной маленькой функции;
- кодом, который мог быть загружен позже (lazy loading).
Давайте разберёмся, как устроен бандл-анализ, какие есть инструменты, как встроить его в ваш процесс разработки и что именно смотреть в отчётах, чтобы реально уменьшить размер фронтенда, а не просто «покликать красивый граф».
Что такое бандл и зачем его анализировать
Что такое бандл
Бандл — это файл (или набор файлов), в который сборщик (Webpack, Vite, Rollup, esbuild, Parcel и другие) объединяет ваш код, зависимости и ассеты:
- JS/TS-код приложения;
- код библиотек (React, lodash, Moment, date-fns и т.д.);
- стили (если используются CSS-in-JS или импорты CSS);
- иногда — изображения, шрифты, SVG-иконки.
Бандлы могут быть:
- основные (main / app) — загружаются при первом открытии страницы;
- чанки (chunks) — куски кода, которые подгружаются по мере необходимости (code splitting, lazy loading);
- vendor-бандлы — отдельные файлы с внешними зависимостями (например,
vendors~main.js); - runtime/manifest — служебный код сборщика.
Зачем анализировать бандл
Бандл-анализ помогает ответить на конкретные вопросы:
- Откуда взялись лишние 300 КБ в продакшене после «безобидного» PR?
- Почему приложение грузится 4 секунды на мобильном интернете?
- Почему в бандл попал
momentс кучей локалей, если используется один формат даты? - Почему один и тот же модуль дублируется в нескольких чанках?
- Какие зависимости самые тяжёлые и стоит ли их менять?
Анализ даёт вам:
- прозрачность — вы видите реальную «карту» кода;
- контроль — вы можете ставить бюджеты размера;
- основания для решений — вы выбираете библиотеку, понимая её вес, а не только удобство API;
- обратную связь — каждый PR можно проверять на влияние на размер.
Основные подходы к бандл-анализу
Статический и динамический анализ
Разделим анализ условно на два типа.
1. Статический анализ бандла
Это когда вы:
- собираете проект;
- получаете готовые файлы (
dist,build,outи т.д.); - запускаете анализатор, который разбирает бандлы как файлы, не исполняя их в браузере.
Результат — отчёты:
- дерево модулей;
- граф зависимостей;
- размеры до и после сжатия (gzip, brotli);
- информация по чанкам, точкам входа.
Инструменты: Webpack Bundle Analyzer, source-map-explorer, Rollup Visualizer, встроенные репортеры Vite.
2. Динамический анализ (в браузере / профилирование)
Здесь вы:
- открываете приложение в браузере;
- включаете DevTools;
- смотрите вкладки Network, Performance, Coverage.
Вы видите:
- фактическое время загрузки и выполнения;
- сколько кода реально используется на странице;
- какие скрипты блокируют рендер.
Оба подхода дополняют друг друга: статический анализ показывает структуру, динамический — реальное поведение.
Ключевые метрики бандл-анализа
Основные размеры
Когда вы анализируете отчёт, чаще всего видите несколько колонок:
- Parsed size (uncompressed / raw) — размер кода после сборки, но до gzip/brotli. Полезно, чтобы понимать, сколько кода придётся парсить движку JS.
- Gzip size — примерный вес, который реально передаётся по сети, если включён gzip.
- Brotli size — ещё более сжатый вариант (часто на продакшене используется brotli, если сервер настроен соответствующе).
Смотрите, я покажу вам, как обычно это выглядит в таблице отчёта:
main.js— 500 KB parsed, 120 KB gzip, 100 KB brotli;vendors~main.js— 800 KB parsed, 200 KB gzip, 170 KB brotli.
Обычно ориентируются на gzip/brotli, но не забывайте, что время парсинга и выполнения зависит именно от parsed size.
Code splitting и чанки
Важно не только «общий размер», но и как он разбит на чанки:
- если у вас один огромный
main.jsна 2 МБ — это плохо; - если код разделён по маршрутам и фичам — пользователь загружает меньшие части по мере навигации.
При анализе обратите внимание:
- сколько чанков тянет первая страница;
- нет ли большого чанка, который загружается везде, хотя нужен только на одной странице;
- не дублируются ли одни и те же зависимости в нескольких чанках.
Точки входа и динамические импорты
Многие анализаторы показывают:
- какие модули входят в
entrypoint(например,index.tsx); - какие модули загружаются через
import()(динамический импорт).
Это помогает понять, почему, например, код страницы профиля попадает в бандл главной страницы.
Инструменты для бандл-анализа
Теперь давайте перейдём к практическим инструментам, с которыми вы скорее всего столкнётесь.
Webpack Bundle Analyzer
Это один из самых популярных инструментов. Он отображает бандл в виде интерактивной treemap-диаграммы.
Установка
Если у вас есть проект на Webpack:
npm install --save-dev webpack-bundle-analyzer
# или
yarn add --dev webpack-bundle-analyzer
Подключение к Webpack
Покажу вам пример конфигурации для Webpack 5:
// webpack.config.js
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
// ... остальная конфигурация
plugins: [
// Здесь вы подключаете плагин только для production-сборки
process.env.ANALYZE === 'true' &&
new BundleAnalyzerPlugin({
analyzerMode: 'server', // режим - откроется веб-сервер с отчетом
analyzerHost: '127.0.0.1', // хост для сервера
analyzerPort: 8888, // порт для сервера
openAnalyzer: true, // автоматически открыть отчет в браузере
defaultSizes: 'gzip', // показывать размеры с учетом gzip-сжатия
generateStatsFile: true, // сгенерировать дополнительный JSON-файл со статистикой
statsFilename: 'stats.json', // имя файла со статистикой
}),
].filter(Boolean), // Здесь мы удаляем false если ANALYZE не включен
};
Комментарии:
process.env.ANALYZE === 'true'— вы включаете анализ только при необходимости, чтобы не замедлять обычную CI-сборку.defaultSizes: 'gzip'— удобнее сразу видеть более «реалистичный» размер.
Запуск анализа
Вы можете добавить скрипт в package.json:
// package.json
{
"scripts": {
// Обычная production-сборка
"build": "webpack --mode production",
// Сборка с анализом бандлов
"build:analyze": "ANALYZE=true webpack --mode production"
}
}
Теперь вы можете выполнить:
npm run build:analyze
После сборки откроется окно браузера с интерактивным отчётом.
Что смотреть в отчёте
Когда вы увидите «карту» бандла, обратите внимание:
- Самые большие блоки — обычно это тяжелые библиотеки или «толстые» фичи.
- Дубли библиотек — например,
lodashиlodash-esодновременно. - Целые папки, попавшие в бандл — например, все иконки вместо нужных трёх.
Если вы кликните на модуль, увидите:
- в какие чанки он входит;
- откуда он импортируется;
- его вклад в общий размер.
source-map-explorer
source-map-explorer — это утилита, которая анализирует бандлы на основе sourcemap-файлов. Полезно, когда:
- сборка уже настроена (например, CRA, Next.js);
- вам нужно быстро понять, что внутри конкретного файла (например,
main-XXXXX.js).
Установка
npm install --save-dev source-map-explorer
# или
yarn add --dev source-map-explorer
Базовое использование
Предположим, у вас есть:
build/static/js/main.123456.jsbuild/static/js/main.123456.js.map
Теперь вы можете выполнить:
npx source-map-explorer build/static/js/main.123456.js
Откроется браузер с диаграммой. Там вы увидите разбиение бандла на модули, которые восстановлены по sourcemap.
Пример с CRA (Create React App)
Для Create React App можно добавить отдельный скрипт.
// package.json
{
"scripts": {
"build": "react-scripts build",
"analyze": "npm run build && source-map-explorer 'build/static/js/*.js'"
}
}
Комментарии:
- Сначала выполняется обычная production-сборка.
- Затем
source-map-explorerанализирует все JS-файлы в папке.
Анализ в Vite, Rollup и других сборщиках
Если вы используете Vite или Rollup, логика похожая, но инструменты немного другие.
Vite
В Vite есть встроенная команда build --report и build --reportCompressed (зависит от версии).
Пример:
npm run build -- --report
После сборки вы получите HTML-отчёт с таблицей.
Также часто используют Rollup-плагин:
npm install --save-dev rollup-plugin-visualizer
и подключают его в vite.config.ts:
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
react(),
// Здесь подключаем визуализатор только при наличии переменной окружения
process.env.ANALYZE === 'true' &&
visualizer({
filename: 'stats.html', // Здесь задаем имя выходного html-отчета
gzipSize: true, // Считать и показывать размер с учетом gzip
brotliSize: true, // Считать и показывать размер с учетом brotli
open: true, // Автоматически открыть отчет в браузере
}),
].filter(Boolean),
});
Теперь вы можете запустить:
ANALYZE=true npm run build
И получите stats.html в корне проекта или в dist (зависит от настроек).
Как читать отчёты и что оптимизировать
Теперь давайте посмотрим, как интерпретировать данные и что именно менять в коде.
Шаг 1. Находим тяжелые зависимости
Обычно 70–80% веса бандла — это внешние библиотеки. Вам важно увидеть:
- какие пакеты самые большие;
- нет ли лишних (например,
moment,lodash,axios,chart.jsи т.д.); - не дублируются ли похожие библиотеки (например, два разных HTTP-клиента).
Типичный пример с Moment.js
Представьте, вы видите, что moment занимает 200 КБ gzip. Часто это потому, что в бандл попали все локали.
Решение может быть таким:
// dateUtils.js
// Здесь мы импортируем только нужные функции из date-fns
import { format } from 'date-fns';
// Здесь мы создаем функцию-обертку для форматирования дат
export function formatDate(date) {
return format(date, 'dd.MM.yyyy'); // Форматируем дату по нужному шаблону
}
Здесь я показываю пример перехода на более лёгкую библиотеку date-fns, которая позволяет импортировать только необходимые функции.
Шаг 2. Проверяем импорты: default vs named
Большие библиотеки, такие как lodash, часто попадают в бандл полностью из-за импорта по умолчанию.
Плохой вариант:
// badExample.js
// Здесь мы импортируем всю библиотеку lodash целиком
import _ from 'lodash';
// Здесь мы используем только одну функцию debounce
const handleChange = _.debounce((value) => {
// Обработка изменения
}, 300);
Лучший вариант:
// goodExample.js
// Здесь мы импортируем только одну функцию debounce из lodash
import debounce from 'lodash/debounce';
const handleChange = debounce((value) => {
// Обработка изменения
}, 300);
Или, если вы используете lodash-es и tree shaking настроен:
// betterExample.js
// Здесь мы импортируем debounce как именованный импорт из lodash-es
import { debounce } from 'lodash-es';
const handleChange = debounce((value) => {
// Обработка изменения
}, 300);
Обратите внимание, как изменение способа импорта может существенно сократить размер бандла.
Шаг 3. Code splitting и lazy loading
Теперь давайте разберёмся на примере, как убрать лишний код из начального бандла с помощью динамического импорта.
Плохой сценарий:
// App.tsx
// Здесь мы импортируем тяжелый компонент синхронно
import HeavyChart from './components/HeavyChart';
function App() {
return (
<div>
{/* Здесь график отрисовывается на главной странице сразу */}
<HeavyChart />
</div>
);
}
export default App;
Этот HeavyChart попадает в первый бандл, даже если пользователь может никогда не открыть раздел с графиком.
Лучший сценарий с React.lazy:
// App.tsx
import React, { Suspense } from 'react';
// Здесь мы используем динамический импорт для тяжелого компонента
const HeavyChart = React.lazy(() => import('./components/HeavyChart'));
function App() {
return (
<div>
{/* Здесь мы показываем заглушку пока график подгружается */}
<Suspense fallback={<div>Загрузка графика...</div>}>
<HeavyChart />
</Suspense>
</div>
);
}
export default App;
Теперь ваш сборщик создаст отдельный чанк под HeavyChart, который загрузится только тогда, когда компонент реально понадобится.
Анализ дубликатов и общих зависимостей
Почему появляются дубликаты
Дубликаты часто возникают, когда:
- используются разные версии одной и той же библиотеки;
- один пакет тянет
lodash@4.17.15, другой —lodash@4.17.21; - часть кода собирается как
commonjs, а часть какesm.
В отчётах вы можете увидеть:
- два модуля
lodashс разными путями; - несколько копий
core-js,regenerator-runtimeи т.п.
Поиск дубликатов в Webpack
В Webpack можно использовать stats.json и, например, анализировать его с помощью сторонних утилит, но начните с простого: посмотрите на treemap и найдите одинаковые названия.
Если вы находите дубликаты:
- Проверьте
package.json— можно ли выровнять версии зависимостей. - Используйте
resolutionsвpackage.json(для Yarn / npm 8+ с overrides), чтобы зафиксировать одну версию.
Пример для Yarn:
// package.json
{
"resolutions": {
// Здесь мы принудительно выставляем одну версию lodash
"lodash": "4.17.21"
}
}
После этого выполните:
yarn install
И пересоберите проект, чтобы убедиться, что лишние копии исчезли.
Интеграция бандл-анализа в процесс разработки
Один раз открыть красивый отчёт — полезно, но гораздо важнее сделать так, чтобы размер бандла контролировался постоянно.
Size budgets (бюджеты размера)
Идея проста: вы задаёте ограничения, например:
- основной бандл не должен превышать 150 КБ gzip;
- общий размер всех чанк-файлов для первой страницы не должен превышать 300 КБ.
Если сборка превышает лимит — build падает, и разработчик видит ошибку.
Пример через webpack-bundle-analyzer и CI
Один из подходов:
- В CI вы запускаете сборку с генерацией
stats.json. - Отдельный скрипт анализирует
stats.jsonи сравнивает размеры с эталонными. - Если превышение — PR блокируется.
Псевдокод простого скрипта на Node.js:
// checkBundleSize.js
// Здесь мы подключаем модуль fs для чтения файлов
const fs = require('fs');
// Здесь мы задаем максимальный допустимый размер в байтах (например 150 КБ)
const MAX_MAIN_GZIP_SIZE = 150 * 1024;
// Здесь мы читаем и парсим файл статистики Webpack
const stats = JSON.parse(fs.readFileSync('./dist/stats.json', 'utf-8'));
// Здесь мы ищем ассет с именем main.js
const mainAsset = stats.assets.find((asset) => asset.name.includes('main.js'));
if (!mainAsset) {
// Если основной бандл не найден - завершаем с ошибкой
console.error('Не найден main.js в статистике бандла');
process.exit(1);
}
// Здесь мы получаем размер файла в байтах
const size = mainAsset.size;
// Здесь мы сравниваем размер с допустимым лимитом
if (size > MAX_MAIN_GZIP_SIZE) {
console.error(
`Размер main.js слишком большой - ${size} байт - лимит ${MAX_MAIN_GZIP_SIZE} байт`
);
process.exit(1); // Завершаем процесс с ошибкой - CI пометит сборку как неуспешную
} else {
console.log(`Размер main.js в пределах лимита - ${size} байт`);
}
Далее в CI вы делаете:
npm run build:stats # сборка + генерация stats.json
node checkBundleSize.js
Здесь я показываю упрощённый пример. В реальных проектах обычно анализируется сразу несколько файлов и используются именно gzip/brotli размеры.
Сравнение размеров между ветками / PR
Полезно автоматически показывать, как изменился размер бандла в PR.
Подход:
- В
main-ветке храните эталонныйstats.json(или его ключевые данные). В PR:
- собираете проект;
- делаете
stats.json; - сравниваете с эталоном;
- публикуете комментарий в PR с результатом.
Существуют готовые решения:
bundlewatch;size-limit;- GitHub Apps, которые подгружают такие отчёты автоматически.
Практические сценарии оптимизации по результатам анализа
Теперь давайте посмотрим, какие типичные проблемы чаще всего всплывают при bundle-analysis и как их решать.
Сценарий 1. Тяжёлая библиотека ради одной функции
Например, вы использовали moment или lodash целиком ради нескольких операций.
Решения:
- заменить на более лёгкую библиотеку (
date-fns,dayjs, нативныеIntlAPI); - использовать точечные импорты (как мы разбирали выше);
- вынести операции в утилитный модуль и следить за импортами только оттуда.
Сценарий 2. Все страницы в одном бандле
Вы запускаете анализ и видите, что ваш main.js содержит код всех страниц, даже если они доступны только по разным маршрутам.
Решения:
- использовать динамический импорт по роутам (React.lazy, Vue async components);
- организовать код так, чтобы каждая крупная фича имела свой entrypoint;
- избегать «центральных» файлов, которые импортируют всё подряд.
Пример с React Router:
// routes.tsx
import React, { Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';
// Здесь мы лениво импортируем страницу профиля
const ProfilePage = React.lazy(() => import('./pages/ProfilePage'));
// Здесь мы лениво импортируем страницу настроек
const SettingsPage = React.lazy(() => import('./pages/SettingsPage'));
function AppRoutes() {
return (
<BrowserRouter>
{/* Здесь оборачиваем роуты в Suspense чтобы показывать fallback при загрузке */}
<Suspense fallback={<div>Загрузка страницы...</div>}>
<Routes>
<Route path="/profile" element={<ProfilePage />} />
<Route path="/settings" element={<SettingsPage />} />
</Routes>
</Suspense>
</BrowserRouter>
);
}
export default AppRoutes;
Теперь код страниц будет вынесен в отдельные чанки.
Сценарий 3. Дублирующиеся polyfill-ы и runtime-код
Часто при анализе вы видите, что:
core-jsподключён несколько раз;regenerator-runtimeвходит в разные чанки;- Babel добавляет вспомогательный код в каждый файл.
Решения:
- использовать
@babel/plugin-transform-runtime, чтобы вынести helpers в отдельный модуль; - настроить
useBuiltIns: 'usage'в@babel/preset-env, чтобы подключать только нужные polyfill-ы; - проверить, не тянет ли каждая библиотека свой собственный polyfill.
Анализ использования кода (Coverage) в браузере
Для дополнения статического анализа полезно посмотреть, какой код реально используется на конкретной странице.
Coverage в Chrome DevTools
Теперь давайте перейдём к шагам в браузере:
- Откройте приложение в Chrome.
- Откройте DevTools, вкладка Coverage (может быть в разделе More tools).
- Нажмите кнопку Start instrumenting coverage and reload page.
- Перезагрузите страницу.
Вы увидите список файлов и процент используемого кода.
Что это даёт:
- вы видите, что, например, из 500 КБ JS используется только 30%;
- можно понять, какие чанки логично разделить или отложить.
Например, если модуль chart.js используется только после клика на вкладку «Аналитика», но загружен сразу, вы точно знаете, что его нужно вынести в lazy component.
Что включать в регулярный процесс бандл-анализа
Чтобы bundle-analysis стал частью нормальной жизни проекта, а не разовой акцией «перед запуском», стоит выстроить несколько практик.
1. Периодический ручной анализ
- Запускайте Webpack Bundle Analyzer или аналог после крупных изменений.
- Фиксируйте где‑нибудь (в Wiki / документации) основные тяжёлые зависимости и принятые решения.
2. Автоматический контроль в CI
- Настройте проверку размеров ключевых бандлов.
- Добавьте уведомление в PR, если размер значительно вырос.
- Сохраняйте
stats.jsonкак артефакт сборки, чтобы при необходимости быстро его открыть.
3. Правила код-ревью
- При добавлении новой библиотеки спрашивайте: «Сколько она весит?»
- Смотрите на импорт: default vs named,
* asvs конкретные функции. - Обращайте внимание на большие компоненты, которые попадают в начальный бандл.
Заключение
Бандл-анализ — это не отдельная «магическая оптимизация», а набор вполне конкретных практик:
- измерять размеры бандлов регулярными инструментами;
- читать отчёты и понимать, откуда взялся каждый крупный кусок кода;
- видеть дубликаты и лишние зависимости;
- применять code splitting и lazy loading там, где это действительно снижает начальный вес страницы;
- интегрировать проверки размеров в CI и процесс ревью.
Если вы выстроите минимальную дисциплину вокруг bundle-analysis, ваш фронтенд будет расти контролируемо: новые фичи не будут внезапно удваивать размер бандла, а пользователи — ждать лишние секунды при загрузке.
Частозадаваемые технические вопросы и ответы
Как сделать бандл-анализ если sourcemap выключены в продакшене
Сначала включите генерацию sourcemap хотя бы для отдельной анализной сборки. В Webpack для продакшен-конфига добавьте devtool: 'source-map' и сделайте отдельный скрипт build:analyze, который не используется в обычном деплое. Затем запускайте source-map-explorer или webpack-bundle-analyzer именно на этой сборке. В продакшен-конфиге по умолчанию sourcemap можно оставить отключенными.
Как смотреть размеры с учетом gzip если анализатор показывает только «сырые» размеры
Если инструмент не считает gzip сам, вы можете отдельно прогнать файлы через gzip-size или brotli-size. Установите npm install --save-dev gzip-size, напишите скрипт на Node.js который читает бандл из dist, передаёт его в gzipSize.sync(buffer) и выводит результат. Так вы получите приближенный к реальности вес.
Как понять какие чанки загружаются на конкретной странице
Откройте браузер, включите DevTools, перейдите на вкладку Network и отфильтруйте по типу JS. Перезагрузите страницу и посмотрите список загруженных файлов. Сопоставьте имена файлов с чанками из отчёта bundle-анализатора. Так вы точно увидите, какие чанки участвуют в initial load и стоит ли что-то вынести в lazy loading.
Что делать если билд-система скрыта внутри фреймворка и нет прямого доступа к Webpack или Rollup
Многие фреймворки (Next.js, CRA, Nuxt) уже имеют встроенные опции анализа. Например, в Next.js можно включить ANALYZE=true и использовать рекомендуемый плагин. Если встроенного режима нет — чаще всего можно всё равно использовать source-map-explorer, запустив обычный production build и анализируя конечные JS-файлы в папке сборки.
Как анализировать CSS-бандлы а не только JS
Многие анализаторы (например, Rollup Visualizer, часть режимов Webpack Bundle Analyzer) показывают и CSS-ассеты. Если ваш инструмент этого не делает, можно использовать отдельные плагины, которые считают веса CSS, либо обработать CSS-файлы в dist через тот же gzip-size. Дополнительно вы можете включить Coverage в DevTools и отфильтровать по CSS чтобы увидеть процент реально используемых стилей.
Постройте личный план изучения Vue до уровня Middle — бесплатно!
Vue — часть карты развития Frontend
100+ шагов развития
30 бесплатных лекций
300 бонусных рублей на счет
Бесплатные лекции
Все гайды по Vue
Лучшие курсы по теме

Vue 3 и Pinia
Антон Ларичев
TypeScript с нуля
Антон Ларичев