Tree Shaking — механізм видалення невикористовуваного коду (dead code elimination) на етапі збірки застосунку. Tree Shaking аналізує статичну структуру ES-модулів та виключає експортовані функції, класи та змінні, які ніде не імпортуються. Згідно з Webpack Documentation, правильне налаштування Tree Shaking може зменшити розмір бандла на 30–60% без зміни функціональності застосунку.
Головне
Tree Shaking — техніка оптимізації коду, при якій з фінального бандла виключаються модулі та функції, що не використовуються в застосунку. Термін введений командою Rollup у 2015 році та метафорично описує процес: дерево залежностей струшується, і невикористовувані гілки відпадають. На відміну від ручної оптимізації, Tree Shaking виконується автоматично на етапі збірки.
Tree Shaking працює тільки з ES-модулями (ECMAScript Modules), де залежності визначаються статично через import та export. CommonJS (require/module.exports) не підтримує Tree Shaking, тому що require виконується динамічно — збірник не може заздалегідь визначити, які функції реально використовуються. Сучасні бібліотеки (Lodash, Moment.js, RxJS) випускають ES-версії для підтримки Tree Shaking.
Rollup — перший збірник, що впровадив Tree Shaking у 2015 році. На відміну від Webpack, Rollup спочатку проєктувався для ES-модулів і виконує більш агресивне видалення мертвого коду. Rollup аналізує не тільки окремі експорти, але й цілі модулі: якщо модуль не має побічних ефектів і жоден експорт не використовується, Rollup виключає весь модуль з бандла.
Rollup особливо ефективний для бібліотек та SDK, де кожен кілобайт має значення. Фреймворк Vue.js використовує Rollup для збірки production-версії. React перейшов на Rollup у 2020 році. Для застосунків частіше використовується Webpack через багатшу екосистему плагінів (Hot Module Replacement, code splitting, CSS modules), але для максимального Tree Shaking при збірці бібліотек Rollup залишається стандартом індустрії.
Економія від Tree Shaking сильно залежить від архітектури проєкту. У застосунку на React з бібліотекою Ant Design Tree Shaking може видалити до 70% коду UI-компонентів. У проєкті, де всі імпорти специфічні та точкові, економія складе 5–15%. Середня економія за дослідженнями Webpack становить 30–40% від розміру бандла.
| Тип мертвого коду | Приклад | Виявлення Tree Shaking |
|---|---|---|
| Невикористовуваний експорт | export function unusedHelper() | Так |
| Невикористовуваний import | import { unused } from "lib" | Так |
| Мертва гілка умови | if (false) { ... } | Ні (видаляється мініфікатором) |
| Невикликана функція після DCE | function a(){} a() де a не викликана | Частково |
Механізм Tree Shaking заснований на графі залежностей (dependency graph), який збірник будує з усіх import/export у проєкті. На першому етапі збірник обходить усі файли від точки входу (entry point) і збирає дерево модулів. На другому етапі аналізується, які експорти з кожного модуля реально імпортуються в інших модулях.
Для кожного модуля Webpack або Rollup позначає експорти як використані або невикористані. Невикористані експорти виключаються з бандла. Однак сам модуль залишається в бандлі, якщо хоча б один його експорт використовується. Повністю виключити модуль можна тільки через прапорець sideEffects або якщо модуль не містить жодних побічних ефектів.
// utils.js — модуль з функціями
export function formatDate(date) {
return date.toISOString().slice(0, 10);
}
export function formatCurrency(amount) {
return "$" + amount.toFixed(2);
}
export function slugify(text) {
return text.toLowerCase().replace(/\s+/g, "-");
}// app.js — точка входу
import { formatDate } from "./utils";
const today = formatDate(new Date());
console.log(today);// Після Tree Shaking — у бандлі тільки formatDate
function formatDate(date) {
return date.toISOString().slice(0, 10);
}
const today = formatDate(new Date());
console.log(today);Tree Shaking виключив formatCurrency та slugify з фінального бандла, оскільки вони не імпортуються в app.js. Розмір модуля utils.js зменшився з 3 функцій до 1. Якщо utils.js містить побічні ефекти (наприклад, глобальну ініціалізацію), Tree Shaking не зможе видалити навіть невикористовувані експорти.
Webpack включає вбудовану підтримку Tree Shaking через плагін TerserPlugin у production-режимі. Для включення Tree Shaking достатньо двох умов: mode встановлено в production (mode: "production") і модулі використовують ES-синтаксис (import/export). Webpack автоматично позначає невикористовувані експорти та передає їх Terser для видалення.
Додаткове налаштування usedExports: true в optimization.webpack.config.js включає детальний аналіз використання експортів всередині модуля. Ця опція визначає, які експорти дійсно використовуються (used), а які є тільки експортованими (provided). Комбінація usedExports та Terser дає максимальну ефективність видалення мертвого коду.
// webpack.config.js — налаштування Tree Shaking
module.exports = {
mode: "production",
entry: "./src/app.js",
output: {
filename: "bundle.js",
},
optimization: {
usedExports: true,
minimize: true,
concatenateModules: true,
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules\/(?!(my-lib)\/).*/,
use: {
loader: "babel-loader",
options: {
presets: [
["@babel/preset-env", { modules: false }],
],
},
},
},
],
},
};Ключовий параметр — modules: false у @babel/preset-env. Babel за замовчуванням перетворює ES-модулі в CommonJS, що вбиває Tree Shaking. modules: false забороняє Babel перетворювати import/export, зберігаючи ES-синтаксис для Webpack. concatenateModules додатково об'єднує модулі в загальну область видимості, зменшуючи кількість IIFE та знижуючи розмір бандла.
Side effects (побічні ефекти) — дії модуля при його імпорті, не пов'язані з експортованими значеннями: глобальні стилі (import "./styles.css"), поліфіли (import "core-js/stable"), ініціалізація глобальних змінних або реєстрація Service Worker. Якщо модуль містить побічні ефекти, збірник не може безпечно видалити його з бандла, навіть якщо жоден експорт не використовується.
Прапорець sideEffects у package.json повідомляє збірнику, які модулі в пакеті не мають побічних ефектів. Для пакета, де всі модулі чисті (тільки експорт функцій), слід вказати "sideEffects": false. Для пакетів з CSS або поліфілами — масив шляхів до файлів з побічними ефектами: "sideEffects": ["*.css"]. Без цього прапорця Tree Shaking не видалить навіть невикористовувані функції.
Щоб перевірити, чи є в модулі побічні ефекти, поставте запитання: чи виконає цей import які-небудь дії, не пов'язані з експортом значень? import "./styles.css" додає CSS в DOM — це побічний ефект. import { throttle } from "lodash-es" не має побічних ефектів — він тільки робить функцію throttle доступною. Поліфіли (import "core-js/stable") мають побічні ефекти — вони модифікують глобальні прототипи.
Для власних модулів рекомендується: виносити стилі та поліфіли в окремі entry-точки, розділяти чисті утиліти (функції без побічних ефектів) та модулі з побічними ефектами (ініціалізація, логування, реєстрація Service Worker). У package.json проєкту верхнього рівня вказувати "sideEffects": false тільки якщо всі модулі чисті. Якщо є стилі — вказувати "sideEffects": ["*.css"] точно.
{
"name": "my-ui-lib",
"version": "2.1.0",
"sideEffects": [
"*.css",
"polyfills.js"
],
"module": "dist/index.esm.js",
"main": "dist/index.cjs.js"
}"sideEffects": ["*.css", "polyfills.js"] означає: всі CSS-файли мають побічні ефекти (їх не можна видаляти) і polyfills.js теж. Всі інші JS-файли в пакеті чисті — їх можна безпечно shake-ити. Поле module вказує шлях до ES-версії пакета, яку збірник повинен використовувати замість CommonJS-версії (main) для Tree Shaking.
React Native з Metro Bundler підтримує обмежену версію Tree Shaking. Metro не виконує повноцінний статичний аналіз використаних експортів (usedExports), як Webpack. Натомість Metro покладається на Terser для видалення невикористовуваної частини модулів на етапі мініфікації. Ефективність такого підходу нижча, ніж повноцінний Tree Shaking у Webpack.
Для максимальної оптимізації React Native проєктів рекомендується: використовувати бібліотеки з ES-модулями (поле module в package.json), підключати плагіни babel-plugin-transform-remove-console для видалення налагоджувального коду та налаштовувати Metro transformer.minifierConfig для Terser. Додатково Ram Bundle (розділення бандла на модулі) зменшує завантаження невикористовуваних екранів.
Часті запитання
CommonJS (require/module.exports) не підтримує статичний аналіз — require може викликатися динамічно всередині умов і функцій. Збірник не може визначити, які частини модуля реально використовуються. Тільки ES-модулі зі статичними import/export дозволяють Tree Shaking.
TypeScript повністю сумісний з Tree Shaking за умови, що tsconfig.json налаштовано на ES-модулі: "module": "esnext". TypeScript-компілятор повинен зберігати import/export без перетворення в CommonJS. Babel з @babel/preset-typescript та modules: false також коректно передає ES-модулі в Webpack.
Webpack Bundle Analyzer — плагін, що візуалізує склад бандла у вигляді інтерактивної діаграми. Якщо бібліотека присутня в бандлі, але її функції не використовуються, Tree Shaking не спрацював. Також можна проаналізувати output-файл: знайти невикористовуваний експорт у тексті бандла через grep.
Lodash v4 поширюється як CommonJS-пакет. Для Tree Shaking потрібно використовувати lodash-es — ES-версію бібліотеки. Замініть import throttle from "lodash/throttle" на import { throttle } from "lodash-es" та налаштуйте resolve.alias у Webpack для підміни lodash на lodash-es.
Tree Shaking незначно збільшує час збірки (на 5–15%), оскільки додає етап аналізу графа залежностей та позначення використаних експортів. У режимі development Tree Shaking зазвичай вимкнений для швидкості. У production додатковий час виправданий значним зменшенням розміру бандла.
Підсумки
Ми розробимо мобільний застосунок під ключ
IT Sectr створює застосунки для iOS та Android для стартапів і бізнесу з 2017 року. Ми проконсультуємо вас і запропонуємо найкраще рішення.
Читайте також