TL;DR
- Bản chất: Kỹ thuật tối ưu hóa mã nguồn trong quá trình đóng gói (Bundling / Compilation) dựa trên phân tích tĩnh (Static Analysis) cấu trúc cây cú pháp (AST) để định vị và loại bỏ hoàn toàn các hàm, biến, hoặc module không bao giờ được tham chiếu (Dead Code Elimination).
- Mục đích: Giảm thiểu tối đa dung lượng file JavaScript phân phối tới client (Client Bundle Size), rút ngắn thời gian tải mạng (Network Transfer) và thời gian phân tích/thực thi mã nguồn của JavaScript Engine (Parse & Compile Time).
- Điểm mấu chốt: Tree Shaking chỉ hoạt động hoàn hảo trên ES Modules (ESM:
import/export) nhờ tính chất tĩnh bất biến (Static Structure); hoàn toàn bất lực trước tính năng nạp động (Dynamic Resolution) của CommonJS (require()).
Core Concept
flowchart LR
subgraph Source_Code [Mã nguồn Utility Library]
F1[export function formatCurrency]
F2[export function calculateVAT]
F3[export function unusedHeavyChart]
end
subgraph Entry_Point [Ứng dụng App.ts]
Import[import { formatCurrency } from './utils']
Call[formatCurrency(1000)]
end
subgraph Bundler [Bundler AST Traversal: Webpack / Rollup / esbuild]
AST[Build Module Dependency Graph]
Shake[Shake Tree: Drop Unreachable Nodes]
end
subgraph Production_Bundle [Output Bundle]
Out1[formatCurrency]
end
F1 --> Import --> AST --> Out1
F2 -.->|Unused| Shake
F3 -.->|Dead Code Dropped| Shake
1. Tại sao CommonJS không thể Tree Shake hiệu quả?
- CommonJS (
require()) mang tính động (Dynamic Evaluation):
Bundler không thể dự đoán được module nào sẽ được nạp khi chỉ phân tích code tĩnh trước thời gian chạy.// CommonJS cho phép nạp module phụ thuộc vào điều kiện runtime: if (isProduction) { const { report } = require("./prod-reporter"); } - ES Modules (
import/export) mang tính tĩnh (Static Declarations):
Câu lệnhimport { formatCurrency } from "./utils";importbắt buộc phải nằm ở top-level, không thể nằm trongif/else, giúp Bundler duyệt đồ thị phụ thuộc (Dependency Graph) và xác định chắc chắn 100% hàm nào có người gọi trước khi chạy code.
2. Yếu tố phá hoại Tree Shaking: Side Effects
Nếu một module thực hiện các tác vụ thay đổi trạng thái toàn cục khi vừa được import (ví dụ: gán biến vào window, can thiệp prototype, hoặc khởi tạo polyfill), Bundler sẽ không dám xóa module đó vì sợ làm hỏng ứng dụng.
- Giải pháp: Khai báo cờ
"sideEffects": falsetrongpackage.jsonđể báo hiệu cho Bundler rằng toàn bộ file trong package thuần khiết (Pure Functions) và an toàn để xóa bỏ nếu không được gọi trực tiếp.
Practical Implementation
1. Cấu hình package.json cho Thư viện (Library Authoring)
{
"name": "@company/shared-utils",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"sideEffects": ["*.css", "./src/polyfills.ts"]
}
2. Tránh Anti-pattern Barrel Files làm hỏng Tree Shaking
// ❌ ANTI-PATTERN: Gộp toàn bộ export vào một index.ts khổng lồ có side-effects
export * from "./auth";
export * from "./heavy-chart"; // Nếu file này import thư viện 5MB, toàn bộ app có thể bị kéo theo
// ✅ BEST PRACTICE: Thiết kế Atomic Modular Exports hoặc Export độc lập
import { formatCurrency } from "@company/shared-utils/formatters";
3. Kiểm định kết quả Tree Shaking qua CLI (esbuild)
# Đóng gói và phân tích bundle để kiểm tra dead code đã bị loại bỏ:
esbuild src/index.ts --bundle --minify --format=esm --analyze