Библиотеки с плохой поддержкой Tree Shaking: обходные решения

Библиотеки с плохой поддержкой tree shaking создают одну из наиболее частых причин раздувания бандла в Webpack-проектах. Даже при корректно настроенной сборке, использовании ES Modules и включённой оптимизации usedExports, значительная часть стороннего кода может попадать в итоговый бандл целиком. Причина почти всегда одна: библиотека распространяется в формате CommonJS или содержит побочные эффекты, которые невозможно безопасно удалить статическим анализом.

Основная проблема заключается в формате публикации кода. Tree shaking в Webpack эффективно работает только в условиях, когда:

  • используется ESM (import / export);
  • отсутствуют неконтролируемые side effects;
  • структура модулей позволяет статически определить используемые экспорты.

Многие популярные библиотеки исторически писались под CommonJS. В таком формате отсутствует статическая структура экспорта: require() может вызываться динамически, а module.exports перезаписываться в любой момент. В результате Webpack вынужден считать такой модуль «непрозрачным» и включать его целиком.

Дополнительный фактор — побочные эффекты. Даже если библиотека использует ESM, но внутри модулей выполняется код на верхнем уровне (например, регистрация глобальных объектов, полифиллы, изменение прототипов), tree shaking отключается для безопасности.

Примеры проблемных библиотек и типичных паттернов

Типичный класс проблемных библиотек:

  • утилитарные библиотеки старого поколения (например, lodash в CommonJS-версии)
  • большие монолиты (moment.js)
  • UI-фреймворки с глобальными побочными эффектами
  • библиотеки, смешивающие ESM и CommonJS без корректной сборки

Паттерн, который ломает 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 теряет возможность точно отследить используемые экспорты.

Настройка Webpack для частичного восстановления tree shaking

Базовые настройки оптимизации:

module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,
    sideEffects: true,
    concatenateModules: true
  }
};

Однако эти настройки не решают проблему библиотек, собранных в CommonJS. Они лишь усиливают анализ внутри уже ESM-совместимого кода.

Использование поля sideEffects

Ключевую роль играет поле sideEffects в package.json зависимостей.

Если библиотека корректно описывает отсутствие побочных эффектов:

{
  "sideEffects": false
}

Webpack получает возможность безопасно удалять неиспользуемые экспорты.

Если библиотека частично «грязная»:

{
  "sideEffects": [
    "*.css",
    "*.scss"
  ]
}

Это позволяет оставить стили, но вырезать JS-части.

Проблема возникает, когда поле отсутствует. Тогда Webpack вынужден считать, что любой импорт может иметь побочные эффекты, и tree shaking становится ограниченным.

Обход через глубокие импорты (deep imports)

Один из самых практичных способов борьбы с «толстыми» библиотеками — импорт не всего пакета, а конкретного модуля.

Плохой вариант:

import { debounce } from 'lodash';

Хороший вариант:

import debounce from 'lodash/debounce';

Во втором случае Webpack подключает только один файл, а не весь набор утилит.

Аналогично для крупных библиотек:

import format from 'date-fns/format';

вместо:

import { format } from 'date-fns';

Такой подход работает только если библиотека действительно публикует модули по отдельным путям.

Замена проблемных библиотек на ESM-аналоги

Некоторые библиотеки имеют альтернативные сборки:

  • lodash → lodash-es
  • moment → date-fns или dayjs
  • rxjs → современные ESM-сборки rxjs 7+

Пример:

import { debounce } from 'lodash-es';

В этом случае tree shaking работает значительно эффективнее, так как каждый модуль представлен отдельно и не содержит CommonJS-обёртки.

Алиасы и принудительная подмена импортов

Webpack позволяет подменять проблемные зависимости:

resolve: {
  alias: {
    lodash: 'lodash-es'
  }
}

Это особенно полезно в больших проектах, где миграция кода невозможна сразу.

Также используется более точечная подмена:

resolve: {
  alias: {
    'moment': 'dayjs'
  }
}

Исключение CommonJS через оптимизацию сборки

Иногда помогает ограничение обработки CommonJS через loader:

module: {
  rules: [
    {
      test: /\.js$/,
      include: /node_modules/,
      type: 'javascript/auto'
    }
  ]
}

Но в реальности это редко улучшает tree shaking и чаще используется для контроля совместимости.

Использование babel-plugin-lodash и аналогов

Для библиотек вроде lodash существует специализированная оптимизация:

babel-plugin-lodash

Он трансформирует:

import { debounce, throttle } from 'lodash';

в:

import debounce from 'lodash/debounce';
import throttle from 'lodash/throttle';

Это позволяет автоматически получать эффект deep imports без ручного переписывания кода.

Ограничения статического анализа Webpack

Tree shaking в Webpack опирается на статический анализ AST. Он не способен:

  • анализировать динамические require:

    const mod = require(name);
  • предсказывать side effects внутри модулей

  • безопасно удалять код, если есть вероятность глобального воздействия

Поэтому любые библиотеки, использующие динамическую загрузку или глобальные мутации, автоматически ухудшают результат оптимизации.

Практика комбинированной оптимизации

На практике используется комбинация подходов:

  • переход на ESM-версии библиотек
  • использование глубоких импортов
  • замена крупных монолитов на модульные аналоги
  • алиасы на ESM-реализации
  • включение sideEffects: false в собственных пакетах
  • минимизация import * as

Пример итоговой структуры импорта:

import debounce from 'lodash-es/debounce';
import format from 'date-fns/format';
import dayjs from 'dayjs';

В такой конфигурации Webpack способен исключить большую часть неиспользуемого кода и сократить размер финального бандла до минимально необходимого.

Поведение Webpack при смешанных модулях

Когда в одном проекте смешиваются ESM и CommonJS зависимости, Webpack строит граф модулей с пониженной точностью. Узлы CommonJS помечаются как «opaque», и дальнейшее удаление веток становится невозможным.

В таких случаях даже частично используемые библиотеки могут подтягивать десятки килобайт лишнего кода, особенно если они экспортируют единый объект с множеством методов.

Оптимизация через анализ бандла

Для выявления проблемных зависимостей используется анализ итогового чанка:

  • Webpack Bundle Analyzer
  • source-map-explorer

Они позволяют определить, какие библиотеки игнорируют tree shaking и включаются полностью.

Типичная ситуация:

  • одна утилита из lodash → 70–100 KB
  • вся библиотека → 500 KB+

Разница показывает эффективность перехода на модульный импорт.

Практическая стратегия работы с legacy-библиотеками

Если библиотека не поддерживает tree shaking и её нельзя заменить, применяются компромиссные решения:

  • изоляция в отдельный чанк (code splitting)

  • динамический импорт:

    import('legacy-lib').then(mod => mod.doSomething());
  • загрузка только в нужных маршрутах приложения

Такой подход не уменьшает общий объём библиотеки, но снижает влияние на initial bundle и улучшает метрики загрузки.