Tree shaking
Tree shaking це процес аналізу імпортів та експортів модулів і видалення тих, які не використовуються у фінальному коді. Простіше кажучи: якщо функцію, клас чи змінну ніколи не викликають і не імпортують, збирач просто викидає їх із підсумкового бандла.
Теорія
TL;DR
- Назва метафорична: дерево це граф залежностей проєкту, а shaking це «струшування» невикористаних гілок.
- Збирач бере entry points, будує граф імпортів і видаляє все, що з цих коренів недосяжне.
- Працює лише з ES Modules, бо
import/exportстатичні; CommonJS і динамічнийrequire()ламають аналіз. - Код із побічними ефектами не видаляється, навіть якщо його експорти ніде не потрібні.
- Поле
sideEffectsуpackage.jsonпідказує збирачу, що модулі можна прибирати агресивніше. - Інструменти: Rollup, Webpack (версія 2 і новіші, у режимі
production), ESBuild, Vite.
Швидкий приклад
// 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 ніде не імпортується, отже його можна безпечно видалити. У результуючому бандлі залишиться тільки:
function add(a,b){return a+b}
console.log(add(2,3))Як збирач розуміє, що можна видалити
Tree shaking це аналіз графа залежностей у три кроки:
- Визначаються «корені», тобто модулі, які реально використовує застосунок (entry points).
- Будується граф імпортів.
- Видаляється все, що з цих коренів недосяжне.
Варто розрізняти два суміжні поняття. Tree shaking це логічний аналіз імпортів та експортів на рівні модулів. Dead code elimination (DCE) це подальший аналіз на рівні виразів, який зазвичай робить мініфікатор (Terser, UglifyJS). Ланцюжок виглядає так:
Webpack / Rollup -> позначають невикористані експорти
|
v
Terser -> фізично вирізає кодЧому потрібні саме ES Modules
Tree shaking працює тільки з ES Modules (ESM), бо вони статичні:
// Працює: ESM
import { add } from './math.js';
// Не працює: CommonJS, аналіз на етапі збірки неможливий
const math = require('./math');Збирач має бачити всі імпорти та експорти на етапі компіляції, а не під час виконання. Динамічні require() зі змінною в шляху ламають цю оптимізацію, бо передбачити, що саме буде імпортовано, неможливо.
Побічні ефекти і поле sideEffects
Tree shaking не видаляє код із побічними ефектами, тобто той, який може змінити глобальний стан:
// utils.js
console.log('модуль завантажено'); // побічний ефект
export const a = 1;
export const b = 2;Навіть якщо ви не імпортуєте ні a, ні b, збирач не видалить модуль, бо бачить побічний ефект: виклик console.log, зміну глобальної змінної тощо.
У пакетах можна явно сказати, що код побічних ефектів не має, і тоді tree shaking працює агресивніше:
{
"name": "my-lib",
"sideEffects": false
}або вибірково, залишивши в списку лише те, що справді має ефекти:
{
"sideEffects": [
"*.css",
"./polyfills.js"
]
}Налаштування у збирачах
Webpack. Режим production вмикає tree shaking разом із мініфікацією:
// webpack.config.js
module.exports = {
mode: 'production', // вмикає tree shaking та мініфікацію
optimization: {
usedExports: true, // позначає використані експорти
}
};Якщо подивитися в bundle-analyzer, ви побачите коментарі /* unused harmony export */, це і є tree shaking у дії.
Rollup. Його від початку будували навколо tree shaking, тому він робить це найкраще:
// 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. Перше прибирає невикористаний код, друге ділить використаний код на окремі чанки; це різні оптимізації.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.