awesome-everything EN
↑ Обратно к восхождению

Производительность

Tree shaking и compression: удаляем то, что не используем

Суть Tree shaking убирает unused exports при сборке — но четыре распространённые ловушки незаметно его побеждают. Brotli и gzip сокращают wire-байты без влияния на parse-стоимость.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Разработчик импортирует одну функцию из lodash: import { debounce } from 'lodash'. Bundle analyzer показывает 70 КБ lodash в выводе. Ожидалось 2 КБ. Импорт выглядел правильно — но формат модуля был неверным.

Как работает tree shaking

Tree shaking — способность bundler’а обнаруживать и удалять экспорты, которые никогда не импортируются. Если модуль экспортирует 200 функций, а app импортирует только одну, остальные 199 — «мёртвый код» — их не должно быть в bundle.

Это работает, потому что ES module синтаксис (import / export) поддаётся статическому анализу: bundler может трассировать import-цепочки при сборке без выполнения кода. CommonJS (require()) вычисляет экспорты во время выполнения; bundler не может статически знать, какие используются, и включает всё.

// Tree-shakeable (ESM) — bundler видит при сборке, какие экспорты используются
import { debounce } from 'lodash-es';  // Только debounce в bundle

// НЕ tree-shakeable (CJS) — bundler включает все экспорты
import { debounce } from 'lodash';      // Все 70 КБ lodash в bundle

Для включения tree shaking: используй ESM-сборку библиотеки (часто указана как module в package.json), настрой bundler на её использование (resolve.mainFields: ['module', 'main'] в Webpack, автоматически в Vite), и убедись, что твой код использует named exports.

Четыре ловушки, побеждающие tree shaking

Ловушка 1 — side effects на уровне модуля. Модуль, устанавливающий window.foo = bar или регистрирующий глобал при импорте, не может быть tree-shaken — bundler должен сохранить side effect, даже если ни один экспорт не используется. Fix: пометь пакет как "sideEffects": false в package.json, если у него нет module-scope side effects, или используй /* #__PURE__ */ аннотации на конкретных вызовах.

Ловушка 2 — CJS re-export. Даже если твой код ESM, библиотека, компилирующаяся в CJS, не может быть tree-shaken. Bundler оборачивает весь модуль. Проверь package.json библиотеки: если нет поля "module" или "exports", указывающего на ESM-сборку, — весь модуль едет. Переключись на ESM-first альтернативу или используй cherry-picked imports (lodash/debounce вместо lodash).

Ловушка 3 — export * re-exports. export * from './module' ре-экспортирует всё. Если даже один потребитель импортирует один экспорт из barrel-файла, весь модуль сохраняется. Fix: явные named re-exports в barrel-файлах.

Ловушка 4 — dynamic import с переменной. await import('./module') — tree-shakeable; await import(name), где name — runtime-переменная, — нет: bundler не знает, какой модуль будет запрошен при сборке.

ЛовушкаПочему побеждает tree shakingFix
Side effects на уровне модуляBundler должен сохранить global mutationsideEffects: false или #PURE аннотация
CJS-библиотекаЭкспорты вычисляются во время выполненияESM-сборка или cherry-pick imports
export * barrel filesОдин потребитель держит весь модульЯвные named re-exports
Dynamic import с переменнойМодуль неизвестен при сборкеString literals в import()

Compression: gzip vs brotli

Compression уменьшает байты, передаваемые по сети. Text-based ассеты (JS, CSS, JSON, SVG) сжимаются на 60-80% с gzip и 70-90% с brotli. Те же 500 КБ uncompressed JS становятся примерно 200 КБ gzip’d или 160 КБ brotli’d.

Важное ограничение: compression уменьшает только download-стоимость. Parse, compile и execute стоимость определяется несжатым размером кода. Переход с gzip на brotli экономит ~10-25% сверх на wire — стоит делать, но не адресует CPU-узкое место.

Static vs dynamic compression: Static compression генерирует сжатый файл при сборке и отдаёт его напрямую с Content-Encoding: br. Стоимость при request time: ноль. Dynamic compression сжимает on the fly per request: ~5-30 мс для brotli, ~1-5 мс для gzip. Senior pattern: static brotli для всех статических ассетов (JS, CSS); dynamic gzip для HTML responses, varying per user.

# Проверить, что brotli отдаётся
curl -sI -H "Accept-Encoding: br" https://your-app.com/app.js | grep content-encoding
# Ожидается: content-encoding: br
Почему это работает

Почему несжатый размер определяет parse-стоимость? Браузер распаковывает файл перед парсингом. JS-движок получает и парсит исходные байты, не сжатую форму. Gzip и brotli — transport-layer оптимизации — экономят сетевые байты, но CPU-работа та же.

Викторина

Библиотека поставляет только CJS-сборку. Команда импортирует одну utility-функцию. Что происходит в bundle?

Викторина

Команда переключается с gzip на static brotli для JS-ассетов. Transfer size падает на 15%. LCP улучшается на 40 мс на mobile. Затронута ли parse-стоимость?

Викторина

Модуль имеет `window.analytics = createAnalytics()` на уровне модуля и помечен sideEffects: true. App импортирует только одну функцию из него. Что происходит?

Вспомните перед уходом
  1. 01
    Почему импорт из CJS-библиотеки шлёт всю библиотеку, даже если используешь только одну функцию?
  2. 02
    Библиотека помечена sideEffects: false, но всё равно полностью появляется в bundle. Назови две вероятных причины.
  3. 03
    В чём разница эффекта gzip и code splitting на parse-стоимость?
Итог

Tree shaking опирается на ESM статический анализ для удаления unused exports. Четыре ловушки — CJS-модули, module-scope side effects, barrel export*, dynamic variable imports — незаметно его побеждают и стоят проверки при каждой сборке. Compression (brotli предпочтительнее gzip) уменьшает wire-байты, но не трогает parse-стоимость. Вместе tree shaking и compression дополняют code splitting: splitting уменьшает то, что route скачивает; tree shaking убирает мёртвый код внутри каждого chunk; compression сжимает transfer. Следующий урок рассматривает third-party scripts — категорию, чаще всего взрывающую тщательно выставленные бюджеты.

Связанные уроки
встречается в159
Продолжить восхождение ↑Third-party scripts: тихий убийца бюджета
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources2
expand
  1. 01
  2. 02

Trademarks belong to their respective owners. Editorial reference only.