Tree Shaking چگونه کدهای اضافی JavaScript را حذف میکند؟
در پروژههای مدرن JavaScript، حجم کدهای ارسالشده به مرورگر تأثیر مستقیمی بر سرعت بارگذاری، زمان اجرای JavaScript و در نهایت تجربه کاربر دارد. یکی از تکنیکهای مهم برای کاهش حجم فایلهای خروجی، Tree Shaking است؛ تکنیکی که به ابزارهای Build اجازه میدهد بخشهایی از کد که هیچوقت استفاده نمیشوند را شناسایی و از خروجی نهایی حذف کنند.
اما Tree Shaking دقیقاً چگونه کار میکند؟ آیا هر کدی که استفاده نمیشود قابل حذف است؟ نقش import و export در این فرآیند چیست؟ و چرا استفاده از ES Modules به Tree Shaking بهتر کمک میکند؟
در این مقاله قدمبهقدم به این موضوع میپردازیم.
Tree Shaking چیست؟
Tree Shaking فرایندی است که طی آن ابزارهای Build مانند Webpack، Rollup، esbuild و Vite کدهای بلااستفاده را از Bundle نهایی حذف میکنند.
ایده اصلی بسیار ساده است:
اگر بخشی از کد هیچ تأثیری روی برنامه نهایی نداشته باشد، چرا باید آن را برای کاربر ارسال کنیم؟
فرض کنید یک فایل JavaScript شامل ۱۰ تابع است، اما برنامه شما فقط از دو تابع استفاده میکند. یک Bundler هوشمند میتواند تشخیص دهد که هشت تابع دیگر مورد نیاز نیستند و آنها را از خروجی حذف کند.
برای مثال:
export function add(a, b) {
return a + b;
}
export function subtract(a, b) {
return a - b;
}
export function multiply(a, b) {
return a * b;
}
و در فایل دیگری فقط این تابع را استفاده کنیم:
import { add } from './math.js';
console.log(add(2, 3));
در یک Build مناسب، تابعهای subtract و multiply میتوانند از Bundle نهایی حذف شوند.
چرا نام آن Tree Shaking است؟
برای درک نام این تکنیک، یک درخت را تصور کنید.
درختی داریم که ریشه آن برنامه اصلی است و شاخههای آن ماژولها، توابع و وابستگیهای مختلف هستند.
مثلاً:
Application
│
├── Header
│ ├── Logo
│ └── Navigation
│
├── Dashboard
│ ├── Chart
│ └── Table
│
└── Utils
├── formatDate
├── formatCurrency
└── formatNumber
اگر برنامه فقط به formatDate نیاز داشته باشد، شاخههای دیگر برای اجرای این بخش ضروری نیستند.
Tree Shaking با تحلیل وابستگیها، بخشهای غیرضروری درخت را شناسایی کرده و حذف میکند؛ درست مانند تکاندن یک درخت برای ریختن برگها و شاخههای اضافی.
Tree Shaking چگونه کار میکند؟
Tree Shaking معمولاً با ترکیبی از چند مرحله انجام میشود:
تحلیل ساختار ماژولها
شناسایی Exportها و Importهای استفادهشده
ساخت گراف وابستگیها
تشخیص کدهای قابل حذف
حذف کدهای Dead
Minification و بهینهسازی نهایی
بیایید این مراحل را دقیقتر بررسی کنیم.
۱. تحلیل ماژولها
Bundler ابتدا فایلهای پروژه را بررسی میکند و رابطه بین آنها را متوجه میشود.
مثلاً:
// 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(10, 20));
Bundler متوجه میشود که app.js فقط به add وابسته است.
در نتیجه:
app.js
│
└── add()
و تابع multiply() در مسیر اجرای برنامه قرار ندارد.
۲. ساخت Dependency Graph
یکی از مفاهیم مهم در Bundling، Dependency Graph یا گراف وابستگی است.
فرض کنید پروژهای داریم:
main.js
│
├── utils.js
│ ├── formatDate()
│ └── formatCurrency()
│
└── user.js
└── getUser()
اگر main.js فقط از formatDate() استفاده کند، ابزار Build میتواند مسیرهای استفادهنشده را شناسایی کند:
main.js
│
└── formatDate()
در این حالت:
formatCurrency()
getUser()
ممکن است اصلاً وارد Bundle نهایی نشوند.
۳. مفهوم Live Binding در ES Modules
یکی از دلایل اصلی عملکرد خوب Tree Shaking، استفاده از سیستم ES Modules است.
مثلاً:
import { add } from './math.js';
در ES Modules ساختار Import و Export از قبل مشخص و قابل تحلیل است.
در نتیجه Bundler میتواند قبل از اجرای برنامه بفهمد چه چیزی از یک ماژول مورد نیاز است.
این ویژگی را معمولاً Static Analysis مینامیم.
Static Analysis چیست؟
Static Analysis یعنی بررسی کد بدون اجرای واقعی آن.
برای مثال Bundler میتواند این کد را بررسی کند:
import { add } from './math.js';
و متوجه شود که add به صورت مستقیم وارد برنامه شده است.
اما شرایطی مانند این میتواند تحلیل را دشوارتر کند:
const moduleName = getModuleName();
const module = require(moduleName);
در این حالت مشخص نیست در زمان Build دقیقاً چه ماژولی باید وارد برنامه شود.
به همین دلیل Tree Shaking در اکوسیستم ES Modules معمولاً عملکرد بهتری دارد.
ES Modules در برابر CommonJS
یکی از تفاوتهای مهم در بحث Tree Shaking، تفاوت بین ES Modules و CommonJS است.
ES Modules
import { add } from './math.js';
CommonJS
const { add } = require('./math.js');
ES Modules ساختار استاتیکتری دارد و Bundler راحتتر میتواند وابستگیها را تحلیل کند.
در نتیجه کتابخانههایی که با ساختار مدرن ESM ارائه میشوند، معمولاً Tree Shaking بهتری دارند.
البته ابزارهای مدرن در برخی شرایط میتوانند CommonJS را نیز تحلیل و بهینه کنند، اما این فرآیند میتواند پیچیدهتر باشد و همیشه به اندازه ESM قابل پیشبینی نیست.
Dead Code چیست؟
برای فهم Tree Shaking باید مفهوم Dead Code را هم بشناسیم.
Dead Code به کدی گفته میشود که در مسیر اجرای برنامه مورد نیاز نیست یا نتیجه آن هیچ تأثیری روی برنامه ندارد.
مثلاً:
function calculateTax(price) {
return price * 0.2;
}
function calculateDiscount(price) {
return price * 0.1;
}
console.log(calculateTax(100));
اگر calculateDiscount() هیچجا استفاده نشود، ممکن است به عنوان کد قابل حذف شناسایی شود.
البته یک نکته مهم وجود دارد:
«استفاده نشدن» همیشه به معنی «قابل حذف بودن» نیست.
چرا همه کدهای استفادهنشده حذف نمیشوند؟
یکی از مهمترین مفاهیم Tree Shaking، Side Effect است.
فرض کنید چنین کدی داریم:
console.log('Module loaded');
این کد چیزی را Export نمیکند، اما هنگام اجرای ماژول یک اثر جانبی دارد.
بنابراین حذف آن ممکن است رفتار برنامه را تغییر دهد.
مثال دیگر:
document.body.classList.add('dark');
حتی اگر هیچ تابعی از این فایل Import نشده باشد، اجرای آن روی DOM تأثیر میگذارد.
پس Bundler نمیتواند صرفاً بگوید:
«این تابع استفاده نشده، پس حذفش کن.»
باید بررسی کند که حذف آن چه تأثیری روی رفتار برنامه دارد.
Side Effects چیست؟
Side Effect یعنی اجرای یک قطعه کد علاوه بر محاسبه مقدار خودش، تأثیری خارج از Scope داخلی آن ایجاد کند.
مثلاً:
let counter = 0;
export function increment() {
counter++;
}
یا:
window.appInitialized = true;
یا:
document.body.classList.add('loaded');
این کدها میتوانند رفتار یا وضعیت محیط را تغییر دهند.
به همین دلیل تشخیص Dead Code همیشه ساده نیست.
نقش package.json در Tree Shaking
در برخی پکیجهای JavaScript، میتوان مشخص کرد که فایلها Side Effect دارند یا خیر.
مثلاً:
{
"sideEffects": false
}
این تنظیم به Bundler اعلام میکند که فایلهای این Package فاقد Side Effectهای مهم هستند و بنابراین میتوان با اطمینان بیشتری کدهای استفادهنشده را حذف کرد.
البته استفاده نادرست از این گزینه میتواند خطرناک باشد.
اگر واقعاً فایلی Side Effect داشته باشد اما Package آن را بدون Side Effect معرفی کند، ممکن است Bundler آن فایل را حذف کند و رفتار برنامه تغییر کند.
بنابراین:
{
"sideEffects": false
}
یک گزینه قدرتمند است، اما باید آگاهانه استفاده شود.
یک مثال واقعیتر
فرض کنید یک کتابخانه UI داریم:
export function Button() {
// ...
}
export function Modal() {
// ...
}
export function Tooltip() {
// ...
}
export function Dropdown() {
// ...
}
در برنامه:
import { Button } from 'ui-library';
اگر کتابخانه و ابزار Build بهدرستی برای Tree Shaking آماده شده باشند، خروجی نهایی میتواند فقط شامل کد مورد نیاز Button باشد.
در مقابل، اگر کتابخانه به شکلی طراحی شده باشد که تمام کدها را به صورت یکپارچه وارد کند، ممکن است Bundle بسیار بزرگتر شود.
Tree Shaking و Minification یکی نیستند
این دو مفهوم اغلب با یکدیگر اشتباه گرفته میشوند.
Tree Shaking
هدف اصلی:
حذف بخشهای غیرضروری برنامه
مثلاً:
function unusedFunction() {
// ...
}
function usedFunction() {
return 42;
}
ممکن است unusedFunction کاملاً حذف شود.
Minification
هدف:
کوچکتر کردن کد موجود
مثلاً:
function calculatePrice(price) {
return price * 1.2;
}
میتواند به چیزی شبیه این تبدیل شود:
function calculatePrice(a){return a*1.2}
پس:
Tree Shaking کد را حذف میکند؛ Minification کد باقیمانده را فشردهتر میکند.
این دو معمولاً در یک Pipeline بهینهسازی با یکدیگر استفاده میشوند.
Tree Shaking در Webpack
Webpack از Tree Shaking پشتیبانی میکند، بهخصوص زمانی که پروژه از ES Modules استفاده کند.
یک ساختار ساده:
// utils.js
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;
و:
// index.js
import { add } from './utils.js';
console.log(add(2, 3));
در Build Production، Webpack میتواند multiply را از Bundle نهایی حذف کند.
برای فعال شدن بسیاری از بهینهسازیها، معمولاً باید Build را در حالت Production انجام داد:
webpack --mode production
Tree Shaking در Rollup
Rollup از ابتدا با تمرکز ویژه روی Bundling ماژولهای ES طراحی شده است.
مثلاً:
import { add } from './math.js';
console.log(add(1, 2));
اگر math.js توابع دیگری هم داشته باشد، Rollup میتواند فقط بخشهای مورد نیاز را وارد خروجی کند.
به همین دلیل Rollup یکی از ابزارهای محبوب برای ساخت کتابخانههای JavaScript است.
Tree Shaking در Vite
Vite در محیط Production از ابزارهای مدرن Bundling استفاده میکند و میتواند Tree Shaking را در فرایند Build اعمال کند.
یک نکته مهم:
Dev Server و Production Build را نباید از نظر بهینهسازی یکسان در نظر گرفت.
در حالت توسعه، هدف اصلی سرعت Development است؛ اما در Production، کاهش حجم Bundle و حذف کدهای غیرضروری اهمیت بیشتری پیدا میکند.
Tree Shaking در کتابخانههای JavaScript
اگر در حال ساخت یک Library هستید، باید Tree Shaking را از همان ابتدا در معماری پروژه در نظر بگیرید.
ساختار زیر مناسبتر است:
export { Button } from './Button.js';
export { Modal } from './Modal.js';
export { Tooltip } from './Tooltip.js';
مصرفکننده میتواند فقط چیزی را که نیاز دارد Import کند:
import { Button } from 'my-library';
این معماری به Bundler اجازه میدهد بخشهای غیرضروری را راحتتر حذف کند.
چه چیزهایی Tree Shaking را خراب میکنند؟
چند الگوی رایج میتوانند قابلیت Tree Shaking را محدود کنند.
۱. استفاده گسترده از Dynamic Code
مثلاً:
const name = getModuleName();
const module = require(name);
تحلیل چنین کدی برای Bundler دشوارتر است.
۲. Side Effectهای ناشناخته
مثلاً:
import './initialize.js';
حتی اگر چیزی از initialize.js Export نشده باشد، ممکن است اجرای آن ضروری باشد.
۳. کتابخانههای نامناسب
اگر یک کتابخانه فقط یک Bundle بزرگ ارائه کند و ساختار ماژولار مناسبی نداشته باشد، Tree Shaking ممکن است نتواند تمام بخشهای بلااستفاده را حذف کند.
۴. استفاده اشتباه از sideEffects
اگر Package اعلام کند:
{
"sideEffects": false
}
اما واقعاً فایلهایی با Side Effect داشته باشد، امکان حذف اشتباه کد وجود دارد.
Tree Shaking و Dynamic Import
Tree Shaking با Code Splitting و Dynamic Import ارتباط دارد، اما این دو یکسان نیستند.
مثلاً:
const module = await import('./admin.js');
در اینجا به جای اینکه کد admin.js حتماً داخل Bundle اصلی باشد، میتوان آن را به یک Chunk جداگانه منتقل کرد.
در نتیجه کاربر فقط زمانی آن کد را دریافت میکند که واقعاً به آن نیاز باشد.
پس دو تکنیک متفاوت داریم:
Tree Shaking
کد غیرضروری را حذف میکند.
Code Splitting
کد را به بخشهای کوچکتر تقسیم میکند تا همه آنها یکجا دانلود نشوند.
ترکیب این دو تکنیک میتواند تأثیر بسیار زیادی روی Performance داشته باشد.
چگونه بفهمیم Tree Shaking واقعاً انجام شده است؟
صرفاً فعال بودن Webpack یا Vite به این معنی نیست که پروژه شما حتماً به بهترین شکل ممکن Tree Shaking شده است.
یکی از روشهای مناسب، بررسی Bundle نهایی است.
ابزارهایی مانند Bundle Analyzer میتوانند نشان دهند چه ماژولهایی بیشترین حجم را اشغال کردهاند.
برای مثال ممکن است تصور کنید فقط یک قابلیت از یک کتابخانه را استفاده میکنید، اما گزارش Bundle نشان دهد که بخش بزرگی از آن کتابخانه وارد خروجی شده است.
این موضوع میتواند نشانهای باشد که:
کتابخانه Tree-Shakable نیست
Import به شکل نامناسب انجام شده
Side Effect وجود دارد
تنظیمات Build مناسب نیست
یا وابستگی دیگری بخشهای مورد نظر را وارد Bundle کرده است
آیا استفاده از Named Import همیشه حجم Bundle را کم میکند؟
این تصور رایج اما ناقص است.
مثلاً:
import { debounce } from 'library';
بهتنهایی تضمین نمیکند که فقط کد debounce وارد Bundle شود.
نتیجه به نحوه ساخت کتابخانه، خروجیهای ESM، Side Effectها و قابلیتهای Bundler بستگی دارد.
بنابراین مهمتر از شکل Import، ساختار داخلی Package و قابلیت Tree Shaking آن است.
چگونه پروژه را برای Tree Shaking آماده کنیم؟
برای داشتن بهترین نتیجه، چند اصل مهم را رعایت کنید.
۱. از ES Modules استفاده کنید
تا جای ممکن:
import ...
export ...
را به ساختارهای قدیمیتر ترجیح دهید.
۲. کتابخانههای Tree-Shakable انتخاب کنید
هنگام انتخاب Dependency فقط به API و محبوبیت آن نگاه نکنید؛ نحوه انتشار Package و پشتیبانی آن از ESM نیز اهمیت دارد.
۳. Side Effectها را کنترل کنید
فایلهایی که فقط برای Side Effect وارد میشوند را بهصورت آگاهانه مدیریت کنید.
۴. Production Build را بررسی کنید
نتیجه نهایی را در محیط Production ارزیابی کنید، نه صرفاً Development.
۵. Bundle را تحلیل کنید
با ابزارهای تحلیل Bundle بررسی کنید چه چیزی واقعاً وارد فایل نهایی شده است.
۶. از Dependencyهای غیرضروری دوری کنید
گاهی بهترین راه برای کاهش Bundle این نیست که Tree Shaking را بهتر کنیم؛ بلکه باید Dependency غیرضروری را از ابتدا حذف کنیم.
یک اشتباه رایج: «Tree Shaking یعنی حذف تمام کدهای استفادهنشده»
این تعریف دقیق نیست.
Tree Shaking در واقع نوعی Dead Code Elimination مبتنی بر تحلیل ماژولها و وابستگیها است.
ابزار Build باید بتواند با اطمینان تشخیص دهد که یک بخش از کد:
مورد نیاز نیست،
Side Effect مهمی ندارد،
و حذف آن رفتار برنامه را تغییر نمیدهد.
هرچه کد پیچیدهتر و Dynamicتر شود، این تحلیل دشوارتر خواهد شد.
چرا Tree Shaking برای Performance مهم است؟
JavaScript فقط مسئله حجم دانلود نیست.
وقتی مرورگر JavaScript بیشتری دریافت میکند، معمولاً باید آن را:
Download
↓
Parse
↓
Compile
↓
Execute
کند.
بنابراین حذف کدهای غیرضروری میتواند در چند مرحله به Performance کمک کند.
Bundle کوچکتر میتواند به معنی:
انتقال داده کمتر
Parse سریعتر
Compile کمتر
اجرای JavaScript کمتر
مصرف منابع کمتر
باشد.
این موضوع بهخصوص در دستگاههای ضعیفتر یا شبکههای کند اهمیت بیشتری پیدا میکند.
آیا Tree Shaking همیشه مفید است؟
تقریباً همیشه به عنوان یک تکنیک بهینهسازی مفید است، اما نباید آن را راهحل همه مشکلات Performance دانست.
اگر Bundle شما بسیار بزرگ است، ممکن است مشکل اصلی یکی از موارد زیر باشد:
Dependencyهای سنگین
نبود Code Splitting
تصاویر و Assets حجیم
JavaScript غیرضروری
Third-party Scriptها
Polyfillهای اضافی
Import اشتباه کتابخانهها
بنابراین Tree Shaking باید بخشی از یک استراتژی بزرگتر برای Performance باشد.
جمعبندی
Tree Shaking یکی از مهمترین تکنیکهای بهینهسازی در اکوسیستم مدرن JavaScript است که با تحلیل ساختار ماژولها، وابستگیها و Exportهای مورد استفاده، کدهای غیرضروری را از خروجی نهایی حذف میکند.
بهترین شرایط برای Tree Shaking زمانی فراهم میشود که:
پروژه از ES Modules استفاده کند،
کتابخانهها ساختار Tree-Shakable داشته باشند،
Side Effectها بهدرستی مدیریت شوند،
Build در حالت Production انجام شود،
و Bundle نهایی بهصورت واقعی تحلیل شود.
در نهایت باید به خاطر داشت که هدف Tree Shaking فقط «کم کردن چند خط کد» نیست؛ هدف اصلی آن کاهش JavaScript غیرضروریای است که کاربر باید دانلود، Parse و اجرا کند.
اگر Tree Shaking را در کنار Code Splitting، Lazy Loading، Minification و انتخاب صحیح Dependencyها به کار بگیریم، میتوانیم Bundleهای کوچکتر و برنامههای سریعتری بسازیم.
یک اصل ساده را به خاطر بسپارید:
هر کدی که کاربر به آن نیاز ندارد، نباید مجبور باشد آن را دانلود و اجرا کند.
