惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
B
Blog RSS Feed
宝玉的分享
宝玉的分享
腾讯CDC
博客园_首页
T
Tailwind CSS Blog
月光博客
月光博客
博客园 - 司徒正美
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
M
MIT News - Artificial intelligence
A
About on SuperTechFans
云风的 BLOG
云风的 BLOG
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
有赞技术团队
有赞技术团队
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
大猫的无限游戏
大猫的无限游戏
MongoDB | Blog
MongoDB | Blog
博客园 - 聂微东
V
Visual Studio Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
SecWiki News
SecWiki News
美团技术团队
P
Privacy International News Feed
H
Help Net Security
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Microsoft Security Blog
Microsoft Security Blog
Know Your Adversary
Know Your Adversary
Y
Y Combinator Blog
D
DataBreaches.Net
Project Zero
Project Zero
T
The Blog of Author Tim Ferriss
Cyberwarzone
Cyberwarzone
C
Cybersecurity and Infrastructure Security Agency CISA
C
Cisco Blogs
S
Schneier on Security
G
GRAHAM CLULEY
博客园 - 三生石上(FineUI控件)
Cisco Talos Blog
Cisco Talos Blog
小众软件
小众软件
Forbes - Security
Forbes - Security
D
Docker
T
Tenable Blog
S
Secure Thoughts
雷峰网
雷峰网
S
Security @ Cisco Blogs
T
The Exploit Database - CXSecurity.com
The Cloudflare Blog
博客园 - 【当耐特】
Spread Privacy
Spread Privacy
阮一峰的网络日志
阮一峰的网络日志

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Почему ваш бандл тяжелее чем должен быть — тестирую tree shaking на 7 бандлерах
SlimDaddy228 · 2026-04-17 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение9 мин

Охват и читатели12K

Обзор

Как всё началось

В августе 2024 я наткнулся на проблему в рабочем проекте на Next.js. Несколько страниц импортировали константы из общего файла через barrel (index.ts с реэкспортами). Каждая страница использовала 2-3 значения, но в бандл попадало всё — десятки неиспользуемых экспортов. Разница оказалась колоссальной: когда я добавил sideEffects: false и перешёл на direct export — бандл уменьшился в два раза.

Я завёл issue на GitHub, покопался, нашёл обходной путь и закрыл. Но вопрос не отпускал: это Next.js виноват? Webpack? Или barrel файлы сами по себе проблема? Спустя полтора года решил разобраться основательно — собрал исследование, прогнал одни и те же тесты на 7 бандлерах и посмотрел что реально попадает в выходные файлы.

Сразу оговорюсь: я не претендую на истину. Это личное исследование, которое я провёл чтобы разобраться в теме для себя. Делюсь результатами в надежде узнать что-то новое от людей, которые работают с бандлерами глубже. Если где-то ошибся или упустил важное — буду рад, если поправите в комментариях.

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

Мёртвый код и tree shaking

Мёртвый код — код, который есть в исходниках, но никогда не используется. Tree shaking — когда бандлер вырезает такой код из финальной сборки. Пример:

// utils.ts — 4 функции
export function formatDate() { /* ... */ }
export function formatCurrency() { /* ... */ }
export function parseQuery() { /* ... */ }
export function debounce() { /* ... */ }
// app.ts — используем только одну
import { formatDate } from './utils'

С tree shaking в бандл попадёт только formatDate. Без него — все четыре функции, даже если они нигде больше не вызываются.

То же самое в масштабе: импортируешь одну иконку из @mui/icons-material (3000+ экспортов) или pick из lodash-es (300+ функций) — tree shaking должен оставить только то, что используется, а остальное выкинуть.

Что такое barrel file и почему с ним проблемы

Barrel file — это index.ts, который реэкспортирует всё из нескольких файлов в одном месте:

// shared/constants/index.ts — barrel file
export { CONSTANT_A, CONFIG_A } from './a'
export { CONSTANT_B, CONFIG_B } from './b'
export { CONSTANT_C, CONFIG_C } from './c'

Удобно — вместо трёх импортов пишешь один:

import { CONSTANT_A, CONFIG_A } from '../shared/constants'

Проблема в том, что бандлер видит импорт из index.ts и может затянуть весь граф зависимостей — включая b.ts и c.ts, которые этой странице не нужны. Неиспользуемый код, который бандлер должен был вырезать (это и есть tree shaking — удаление мёртвого кода при сборке), остаётся в финальном файле и едет в продакшен.

Исследование: 7 бандлеров, 3 кейса

Задумка простая — взять одни и те же данные, одну и ту же структуру импортов, и посмотреть как каждый бандлер справляется с удалением неиспользуемого кода. Без фреймворков, без сложной логики, минимальный воспроизводимый пример.

6 экспортов — 3 простые строки и 3 объекта конфигурации. Три страницы, каждая использует только 2 из 6. Если tree shaking работает — в финальном файле страницы будут только её 2 значения. Если нет — потащит все 6.

Одни и те же данные, три варианта импорта:

Single file — все 6 экспортов в одном файле:

// shared/constants-single-file.ts
export const CONSTANT_A = 'value_a_from_single_file'
export const CONFIG_A = { name: 'config_a', value: 1, nested: { deep: true } }
// ... и ещё B, C

Barrel — экспорты разнесены по файлам, импорт через index.ts:

// shared/constants-separate/index.ts
export { CONSTANT_A, CONFIG_A } from './a'
export { CONSTANT_B, CONFIG_B } from './b'
export { CONSTANT_C, CONFIG_C } from './c'

Direct — те же отдельные файлы, но импорт напрямую, без barrel:

import { CONSTANT_A, CONFIG_A } from '../shared/constants-separate/a'

Прогнал на:

  • webpack 5 — scope hoisting + terser

  • rspack 1 — webpack на Rust, тот же API

  • rollup 4 — заточен под ES-модули и tree shaking

  • vite 8 — rolldown под капотом для production-сборки

  • esbuild 0.28 — написан на Go, самый быстрый

  • Next.js 15 (webpack) — Next.js с webpack под капотом

  • Next.js 16 (Turbopack) — Next.js с Turbopack по умолчанию

Всё автоматизировано — node analyze.js ставит зависимости, собирает каждый бандлер и анализирует выходные файлы, проверяя какие маркеры попали в бандл.

Репозиторий с исследованием: github.com/lykianovsky/tree-shaking-barrel-test

Результаты

Кейс

webpack

rspack

rollup

vite

esbuild

next-webpack

next-turbopack

Single file

Barrel

Direct

Главный вывод: direct import (import { X } from './constants/a') — единственный способ, который гарантирует tree shaking у всех 7 бандлеров. Только webpack и rspack справляются со всеми тремя кейсами. Остальные ломаются на single file, barrel, или на обоих.

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


Как webpack делает tree shaking — и почему это два шага, а не один

Webpack не вырезает мёртвый код напрямую. Он работает в два этапа:

webpack:  [модули] → scope hoisting (concat) → всё в одном скоупе → terser → мёртвый код удалён ✅
rollup:   [модули] → анализ графа → включает только используемое → минификация (опционально) ✅
Next.js:  [модули] → shared chunks (multi-entry) → concat невозможен → terser не видит мёртвое ❌

Шаг 1 — scope hoisting. ModuleConcatenationPlugin объединяет все модули в один скоуп. Вместо того чтобы оборачивать каждый файл в отдельную функцию, webpack складывает весь код в одно место. На этом этапе мёртвый код ещё никуда не делся — все 6 экспортов на месте:

// webpack dist/separate/page1.js — minimize: false
(() => {
  "use strict";

  // ../shared/constants-separate/a.ts
  const CONSTANT_A = 'value_a_from_separate_file';
  const CONFIG_A = { name: 'config_a', value: 1, nested: { deep: true } };

  // ../shared/constants-separate/b.ts        ← мёртвый код
  const CONSTANT_B = 'value_b_from_separate_file';
  const CONFIG_B = { name: 'config_b', value: 2, nested: { deep: false } };

  // ../shared/constants-separate/c.ts        ← мёртвый код
  const CONSTANT_C = 'value_c_from_separate_file';
  const CONFIG_C = { name: 'config_c', value: 3, nested: { deep: true } };

  // ./src/separate-files/page1.ts
  console.log('Page 1:', CONSTANT_A, CONFIG_A);
})();

Шаг 2 — terser. Минификатор видит что CONSTANT_B, CONFIG_B, CONSTANT_C, CONFIG_C нигде не используются — и вырезает:

// webpack dist/separate/page1.js — minimize: true (по умолчанию)
(()=>{"use strict";console.log("Page 1:","value_a_from_separate_file",{name:"config_a",value:1,nested:{deep:!0}})})();

Чисто. Только нужные данные.

Важный момент: отключи minimize: false — и tree shaking ломается. Terser не запускается, все 6 экспортов остаются. Scope hoisting сам по себе не удаляет код — он только создаёт условия, чтобы terser мог увидеть неиспользуемые переменные. Без минификации webpack не лучше остальных.

В rollup и vite такой зависимости нет — tree shaking у них работает на этапе построения графа модулей, ещё до минификации. Rollup анализирует какие экспорты реально используются и просто не включает остальные в выходной файл.

Побочный эффект scope hoisting — дублирование кода

Scope hoisting дублирует модули в каждый entry. Это даёт чистый tree shaking, но один и тот же код физически присутствует в нескольких файлах. Возникает вопрос: а не ломает ли это синглтоны и общее состояние?

Разбор: как webpack сохраняет синглтоны при дублировании

Допустим, три страницы используют общий shared-state.ts:

// shared-state.ts                          — общий модуль
export const testMap = new Map<string, string>()

// page1.ts — пишет в Map
import { testMap } from './shared-state'
testMap.set('page', 'page1')

// page2.ts — читает из Map
import { testMap } from './shared-state'
console.log('Page 2:', testMap.get('page'))  // 'page1' — если синглтон работает

После сборки new Map() появляется внутри каждого чанка — в 503.chunk.js и в 570.chunk.js. Выглядит как два разных экземпляра new Map().

Но webpack при первом вызове require(564) выполняет фабрику модуля и сохраняет результат в __webpack_module_cache__. Когда page2 запрашивает тот же модуль 564 — webpack берёт его из кэша, new Map второй раз не вызывается. Код дублирован физически, но выполняется один раз. Синглтон работает.

Если дублирование не устраивает — splitChunks с minSize: 0 выносит общий код в отдельный чанк, один на всех. Но тогда этот чанк содержит все экспорты и tree shaking на нём не работает — тот же компромисс что у rollup.

Почему rollup и vite ломаются на single file

Если tree shaking у rollup работает на этапе графа — почему single file ломается?

Потому что когда один файл (constants-single-file.ts) импортируется из нескольких entry (page1, page2, page3), rollup выносит его в отдельный shared chunk. Этот shared chunk содержит все 6 экспортов, потому что разные страницы используют разные:

// rollup dist/single/shared-constants-single-file.js — shared chunk
const e = "value_a...", a = { name: "config_a", ... };
const n = "value_b...", o = { name: "config_b", ... };  // page1 это не нужно
const s = "value_c...", t = { name: "config_c", ... };  // и это тоже
export { e as C, a, n as b, o as c, s as d, t as e };   // но экспортируется всё

Shared chunk — это отдельный JS-файл, в который бандлер выносит код, общий для нескольких страниц. Каждая страница подключает его и берёт только свои значения, но сам файл грузится целиком — мёртвый код доставляется в браузер.

Webpack поступает иначе — дублирует код в каждый entry через scope hoisting, а terser вычищает лишнее в каждом отдельно. Каждый entry содержит только нужное.

В barrel-кейсе rollup справляется — трейсит через index.ts до конкретных файлов и включает только нужные. Shared chunk не создаётся, потому что файлы маленькие и разные страницы тянут разные файлы, которые не пересекаются.

Почему esbuild ломает и barrel тоже

esbuild — самый быстрый из всех, но агрессивно создаёт shared chunks. И для single file, и для barrel весь общий код уезжает в один файл:

// esbuild dist/separate/shared-chunk-X3DOSE65.js — ВСЕ 6 экспортов в одном файле
var e="value_a...",_={name:"config_a",...};
var o="value_b...",t={name:"config_b",...};  // ← не нужен page1, но всё равно здесь
var r="value_c...",a={name:"config_c",...};  // ← и это тоже
export{e as a,_ as b,o as c,t as d,r as e,a as f};

С direct-импортами shared chunk не создаётся — каждая страница видит только свой файл.

Почему Next.js (webpack) ломает tree shaking — и это не 'use client'

Это было самое интересное в исследовании. Первая мысль — виноват 'use client', граница клиентского компонента мешает scope hoisting. Но нет — на Pages Router, где никакого 'use client' нет, картина ровно такая же.

Я полез в исходники Next.js и повесил отладочные хуки на webpack-конфиг. Вот что выяснилось:

Next.js вообще не включает ModuleConcatenationPlugin. В файле webpack-config.js нет ни одного упоминания concatenateModules или ModuleConcatenationPlugin. Ноль.

Окей, может достаточно включить руками? Добавил через next.config.js:

webpack(config) {
  config.optimization.concatenateModules = true
  return config
}

Результат — 0 модулей сконкатенировано. Bailout на каждом. Webpack прямо говорит почему:

ModuleConcatenation bailout: Cannot concat with shared/constants-single-file.ts:
  Module is referenced from different chunks by these modules:
  app/single/page2/page.tsx, app/single/page3/page.tsx

Корневая причина: Next.js создаёт отдельный chunk entry на каждую страницу. В Pages Router это делает next-client-pages-loader, в App Router — next-flight-client-entry-loader. Когда constants-single-file.ts импортируется из page1, page2, page3 — модуль оказывается referenced из нескольких чанков.

ModuleConcatenationPlugin не может заинлайнить модуль, на который ссылаются из разных чанков — ему пришлось бы дублировать код, а он этого не делает. Без конкатенации модули остаются в отдельных обёртках, terser не видит что экспорты не используются, мёртвый код остаётся.

В чистом webpack те же 3 entry, тот же файл. Но там splitChunks.minSize = 20000 (порог по умолчанию) — файл констант маленький, не дотягивает, webpack не выносит его в shared chunk, а дублирует в каждый entry. Дублированный код конкатенируется → terser вычищает. Next.js так не может — его загрузчики сразу создают отдельные chunk entries для каждой страницы, и модули автоматически шарятся между ними.

Turbopack (Next.js 16) частично решает проблему — barrel обрабатывает, трейсит через index.ts до конкретных файлов, как rollup. Но single file всё равно ломает — та же история с shared модулем.

Можно ли починить tree shaking в Next.js?

Теоретически есть три пути:

Дублировать модули в каждый entry — как делает чистый webpack с маленькими файлами. Конкатенация работает, terser вычищает. Но Next.js специально шарит модули между страницами — при навигации page1 → page2 shared chunk уже в кеше браузера. Дублирование означает: каждая страница тяжелее, при навигации тот же код грузится заново.

Tree shaking на уровне графа — резать неиспользуемые экспорты при построении графа, не зависеть от concat + terser. Turbopack так и делает, поэтому barrel у него работает. Но это переписывание core-логики webpack, и single file всё равно не решается.

Per-entry копии модуля — создавать отдельную версию модуля для каждого entry с только нужными экспортами. Фундаментальное изменение, которого в webpack нет и вряд ли появится.

На практике:

  • Большие barrel-ы из npm (@mui/icons-material, lodash-es) — Next.js решает через optimizePackageImports:

// next.config.js
module.exports = {
  experimental: {
    optimizePackageImports: ['@mui/icons-material', 'lodash-es']
  }
}
// Результат: import { Add } from '@mui/icons-material'
//         → import Add from '@mui/icons-material/Add'
  • Свои barrel-ы — direct import или разбивай на мелкие группы по фичам

  • Масштаб проблемы зависит от размера barrel-а. 6 констант — незаметно. Но если в одном файле 100 экспортов с тяжёлыми объектами — каждая страница тащит всё, и это уже ощутимо

Трейдоффы — нет единого лучшего решения

Каждый бандлер делает свой выбор, и у каждого выбора есть цена:

webpack / rspack — дублирует код в каждый entry, concat + terser вычищает мёртвое. Tree shaking работает на всех кейсах. Но есть порог splitChunks.minSize (по умолчанию 20kb) — если общий модуль вырастает больше порога, webpack выносит его в shared chunk и tree shaking на нём ломается. Плюс зависимость от minimize — без минификации ничего не вырезается.

rollup / vite — tree shaking на этапе графа, не зависит от минификации. Но создают shared chunks для файлов, которые импортируются из нескольких entry. Shared chunk содержит все экспорты для всех потребителей — мёртвый код для конкретной страницы.

esbuild — быстрее всех, но агрессивные shared chunks и для single file, и для barrel. Tree shaking работает только с direct imports.

Next.js (webpack) — multi-entry архитектура ради кеширования при навигации между страницами. Shared chunk загрузился один раз — при переходе на другую страницу грузится только новый код. Цена — tree shaking не работает на shared модулях, мёртвый код попадает в бандл.

Вывод

Практический совет: import { X } from './constants/a' вместо import { X } from './constants'. Direct import работает у всех 7 бандлеров — это единственный способ, где tree shaking гарантирован.

Но у меня остался открытый вопрос. Webpack пошёл по пути scope hoisting + terser — дублирует модули, потом вычищает мёртвое. Rollup, vite, esbuild пошли другим путём — tree shaking на уровне графа, shared chunks вместо дублирования. Почему? Есть ли фундаментальная причина, по которой другие бандлеры не пошли по пути webpack? Или это просто разные компромиссы? И можно ли совместить лучшее из обоих подходов — tree shaking на уровне графа без shared chunks с мёртвым кодом?

Если вы работаете над бандлерами или глубоко копали эту тему — буду рад услышать ваше мнение в комментариях.