Skip to main content

Tree shaking

Tree shaking це процес аналізу імпортів та експортів модулів і видалення тих, які не використовуються у фінальному коді. Простіше кажучи: якщо функцію, клас чи змінну ніколи не викликають і не імпортують, збирач просто викидає їх із підсумкового бандла.

Теорія

TL;DR

  • Назва метафорична: дерево це граф залежностей проєкту, а shaking це «струшування» невикористаних гілок.
  • Збирач бере entry points, будує граф імпортів і видаляє все, що з цих коренів недосяжне.
  • Працює лише з ES Modules, бо import/export статичні; CommonJS і динамічний require() ламають аналіз.
  • Код із побічними ефектами не видаляється, навіть якщо його експорти ніде не потрібні.
  • Поле sideEffects у package.json підказує збирачу, що модулі можна прибирати агресивніше.
  • Інструменти: Rollup, Webpack (версія 2 і новіші, у режимі production), ESBuild, Vite.

Швидкий приклад

javascript
// math.js export function add(a, b) { return a + b; } export function multiply(a, b) { return a * b; } // app.js import { add } from './math.js'; console.log(add(2, 3));

Без tree shaking у бандл потрапить і add, і multiply, хоча multiply не використовується.

Сучасний збирач (Rollup, Webpack 5 і новіші, ESBuild, Vite) проаналізує, що multiply ніде не імпортується, отже його можна безпечно видалити. У результуючому бандлі залишиться тільки:

javascript
function add(a,b){return a+b} console.log(add(2,3))

Як збирач розуміє, що можна видалити

Tree shaking це аналіз графа залежностей у три кроки:

  1. Визначаються «корені», тобто модулі, які реально використовує застосунок (entry points).
  2. Будується граф імпортів.
  3. Видаляється все, що з цих коренів недосяжне.

Варто розрізняти два суміжні поняття. Tree shaking це логічний аналіз імпортів та експортів на рівні модулів. Dead code elimination (DCE) це подальший аналіз на рівні виразів, який зазвичай робить мініфікатор (Terser, UglifyJS). Ланцюжок виглядає так:

text
Webpack / Rollup -> позначають невикористані експорти | v Terser -> фізично вирізає код

Чому потрібні саме ES Modules

Tree shaking працює тільки з ES Modules (ESM), бо вони статичні:

javascript
// Працює: ESM import { add } from './math.js'; // Не працює: CommonJS, аналіз на етапі збірки неможливий const math = require('./math');

Збирач має бачити всі імпорти та експорти на етапі компіляції, а не під час виконання. Динамічні require() зі змінною в шляху ламають цю оптимізацію, бо передбачити, що саме буде імпортовано, неможливо.

Побічні ефекти і поле sideEffects

Tree shaking не видаляє код із побічними ефектами, тобто той, який може змінити глобальний стан:

javascript
// utils.js console.log('модуль завантажено'); // побічний ефект export const a = 1; export const b = 2;

Навіть якщо ви не імпортуєте ні a, ні b, збирач не видалить модуль, бо бачить побічний ефект: виклик console.log, зміну глобальної змінної тощо.

У пакетах можна явно сказати, що код побічних ефектів не має, і тоді tree shaking працює агресивніше:

json
{ "name": "my-lib", "sideEffects": false }

або вибірково, залишивши в списку лише те, що справді має ефекти:

json
{ "sideEffects": [ "*.css", "./polyfills.js" ] }

Налаштування у збирачах

Webpack. Режим production вмикає tree shaking разом із мініфікацією:

javascript
// webpack.config.js module.exports = { mode: 'production', // вмикає tree shaking та мініфікацію optimization: { usedExports: true, // позначає використані експорти } };

Якщо подивитися в bundle-analyzer, ви побачите коментарі /* unused harmony export */, це і є tree shaking у дії.

Rollup. Його від початку будували навколо tree shaking, тому він робить це найкраще:

javascript
// rollup.config.js export default { input: 'src/index.js', output: { file: 'dist/bundle.js', format: 'esm' } };

Rollup видаляє не лише невикористані експорти, а й цілі ланцюжки функцій, якщо їх ніхто не викликає.

Vite та ESBuild. Вони завжди працюють з ESM і підтримують tree shaking вбудовано, налаштовувати нічого не треба. Головне не використовувати CommonJS і не мати прихованих побічних ефектів.

Підсумок:

ЩоОпис
Tree shakingВидалення невикористаних імпортів та експортів
Працює лише зES Modules (import / export)
Не видаляєКод із побічними ефектами
НалаштуванняsideEffects: false у package.json, mode: production
ІнструментиWebpack ≥ 2, Rollup, ESBuild, Vite
МетаЗменшити розмір бандла без ручного видалення коду

Головна ідея: збирач будує «дерево залежностей» і струшує з нього все, що не використовується, звідси й назва tree shaking.

Типові помилки

Найчастіші причини, через які tree shaking не спрацьовує:

ПричинаЧому
Використовується CommonJS (require)немає статичного аналізу залежностей
Імпорт усього модуля (import * as m) або require(variable)збирач не може передбачити, що саме імпортується
Побічні ефекти в модуляхїх не можна безпечно видалити
Плагіни або транспіляція ламають ESMнаприклад, Babel перетворює import/export на require
Не увімкнено mode: 'production' у Webpackу dev-режимі оптимізації вимкнені

Окремо варто пам'ятати:

  • Чекати, що tree shaking сам зменшить бандл у dev-збірці. Вимірювати розмір треба лише на production-збірці.
  • Ставити "sideEffects": false наосліп. Якщо у пакеті є CSS-імпорти, поліфіли чи реєстрація глобальних хелперів, збирач їх викине, і застосунок зламається у production, хоча локально все працювало.
  • Плутати tree shaking із code splitting. Перше прибирає невикористаний код, друге ділить використаний код на окремі чанки; це різні оптимізації.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.