Библиотеки с плохой поддержкой tree shaking создают одну из наиболее
частых причин раздувания бандла в Webpack-проектах. Даже при корректно
настроенной сборке, использовании ES Modules и включённой оптимизации
usedExports, значительная часть стороннего кода может
попадать в итоговый бандл целиком. Причина почти всегда одна: библиотека
распространяется в формате CommonJS или содержит побочные эффекты,
которые невозможно безопасно удалить статическим анализом.
Основная проблема заключается в формате публикации кода. Tree shaking в Webpack эффективно работает только в условиях, когда:
import / export);Многие популярные библиотеки исторически писались под CommonJS. В
таком формате отсутствует статическая структура экспорта:
require() может вызываться динамически, а
module.exports перезаписываться в любой момент. В
результате Webpack вынужден считать такой модуль «непрозрачным» и
включать его целиком.
Дополнительный фактор — побочные эффекты. Даже если библиотека использует ESM, но внутри модулей выполняется код на верхнем уровне (например, регистрация глобальных объектов, полифиллы, изменение прототипов), tree shaking отключается для безопасности.
Типичный класс проблемных библиотек:
Паттерн, который ломает tree shaking:
// CommonJS стиль
const utils = require('utils-lib');
utils.a();
utils.b();
Webpack не может определить, используется ли utils.c,
utils.d, и вынужден включить весь модуль.
Даже в ESM-обёртке проблема сохраняется:
import * as utils from 'utils-lib';
Конструкция * as часто приводит к включению всего
пространства имён, так как Webpack теряет возможность точно отследить
используемые экспорты.
Базовые настройки оптимизации:
module.exports = {
mode: 'production',
optimization: {
usedExports: true,
sideEffects: true,
concatenateModules: true
}
};
Однако эти настройки не решают проблему библиотек, собранных в CommonJS. Они лишь усиливают анализ внутри уже ESM-совместимого кода.
Ключевую роль играет поле sideEffects в
package.json зависимостей.
Если библиотека корректно описывает отсутствие побочных эффектов:
{
"sideEffects": false
}
Webpack получает возможность безопасно удалять неиспользуемые экспорты.
Если библиотека частично «грязная»:
{
"sideEffects": [
"*.css",
"*.scss"
]
}
Это позволяет оставить стили, но вырезать JS-части.
Проблема возникает, когда поле отсутствует. Тогда Webpack вынужден считать, что любой импорт может иметь побочные эффекты, и tree shaking становится ограниченным.
Один из самых практичных способов борьбы с «толстыми» библиотеками — импорт не всего пакета, а конкретного модуля.
Плохой вариант:
import { debounce } from 'lodash';
Хороший вариант:
import debounce from 'lodash/debounce';
Во втором случае Webpack подключает только один файл, а не весь набор утилит.
Аналогично для крупных библиотек:
import format from 'date-fns/format';
вместо:
import { format } from 'date-fns';
Такой подход работает только если библиотека действительно публикует модули по отдельным путям.
Некоторые библиотеки имеют альтернативные сборки:
Пример:
import { debounce } from 'lodash-es';
В этом случае tree shaking работает значительно эффективнее, так как каждый модуль представлен отдельно и не содержит CommonJS-обёртки.
Webpack позволяет подменять проблемные зависимости:
resolve: {
alias: {
lodash: 'lodash-es'
}
}
Это особенно полезно в больших проектах, где миграция кода невозможна сразу.
Также используется более точечная подмена:
resolve: {
alias: {
'moment': 'dayjs'
}
}
Иногда помогает ограничение обработки CommonJS через loader:
module: {
rules: [
{
test: /\.js$/,
include: /node_modules/,
type: 'javascript/auto'
}
]
}
Но в реальности это редко улучшает tree shaking и чаще используется для контроля совместимости.
Для библиотек вроде lodash существует специализированная оптимизация:
babel-plugin-lodash
Он трансформирует:
import { debounce, throttle } from 'lodash';
в:
import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';
Это позволяет автоматически получать эффект deep imports без ручного переписывания кода.
Tree shaking в Webpack опирается на статический анализ AST. Он не способен:
анализировать динамические require:
const mod = require(name);предсказывать side effects внутри модулей
безопасно удалять код, если есть вероятность глобального воздействия
Поэтому любые библиотеки, использующие динамическую загрузку или глобальные мутации, автоматически ухудшают результат оптимизации.
На практике используется комбинация подходов:
sideEffects: false в собственных пакетахimport * asПример итоговой структуры импорта:
import debounce from 'lodash-es/debounce';
import format from 'date-fns/format';
import dayjs from 'dayjs';
В такой конфигурации Webpack способен исключить большую часть неиспользуемого кода и сократить размер финального бандла до минимально необходимого.
Когда в одном проекте смешиваются ESM и CommonJS зависимости, Webpack строит граф модулей с пониженной точностью. Узлы CommonJS помечаются как «opaque», и дальнейшее удаление веток становится невозможным.
В таких случаях даже частично используемые библиотеки могут подтягивать десятки килобайт лишнего кода, особенно если они экспортируют единый объект с множеством методов.
Для выявления проблемных зависимостей используется анализ итогового чанка:
Они позволяют определить, какие библиотеки игнорируют tree shaking и включаются полностью.
Типичная ситуация:
Разница показывает эффективность перехода на модульный импорт.
Если библиотека не поддерживает tree shaking и её нельзя заменить, применяются компромиссные решения:
изоляция в отдельный чанк (code splitting)
динамический импорт:
import('legacy-lib').then(mod => mod.doSomething());загрузка только в нужных маршрутах приложения
Такой подход не уменьшает общий объём библиотеки, но снижает влияние на initial bundle и улучшает метрики загрузки.