Tree Shaking là cơ chế loại bỏ mã không sử dụng (dead code elimination) trong giai đoạn xây dựng ứng dụng. Tree Shaking phân tích cấu trúc tĩnh của các module ES và loại trừ các hàm, lớp và biến được xuất khẩu không được import ở bất kỳ đâu. Theo Webpack Documentation, cấu hình Tree Shaking đúng cách có thể giảm kích thước bundle xuống 30–60% mà không thay đổi chức năng ứng dụng.
Những điểm chính
Tree Shaking là kỹ thuật tối ưu mã loại trừ các module và hàm không sử dụng khỏi bundle cuối cùng. Thuật ngữ này được nhóm Rollup giới thiệu vào năm 2015 và mô tả quá trình một cách ẩn dụ: cây phụ thuộc bị rũ (shake) và các nhánh không sử dụng rụng xuống. Không giống như tối ưu thủ công, Tree Shaking được thực hiện tự động trong giai đoạn xây dựng.
Tree Shaking chỉ hoạt động với module ES (ECMAScript Modules), nơi các phụ thuộc được xác định tĩnh thông qua import và export. CommonJS (require/module.exports) không hỗ trợ Tree Shaking vì require được thực thi động — trình bundle không thể xác định trước hàm nào thực sự được sử dụng. Các thư viện hiện đại (Lodash, Moment.js, RxJS) phát hành phiên bản ES để hỗ trợ Tree Shaking.
Rollup là trình bundle đầu tiên triển khai Tree Shaking vào năm 2015. Không giống Webpack, Rollup được thiết kế ngay từ đầu cho module ES và thực hiện loại bỏ mã chết mạnh mẽ hơn. Rollup phân tích không chỉ các export riêng lẻ mà cả toàn bộ module: nếu một module không có tác dụng phụ và không có export nào được sử dụng, Rollup loại trừ toàn bộ module khỏi bundle.
Rollup đặc biệt hiệu quả cho các thư viện và SDK, nơi mỗi kilobyte đều quan trọng. Framework Vue.js sử dụng Rollup để xây dựng phiên bản production. React đã chuyển sang Rollup vào năm 2020. Đối với các ứng dụng, Webpack được sử dụng phổ biến hơn do hệ sinh thái plugin phong phú hơn (Hot Module Replacement, code splitting, CSS modules), nhưng để đạt Tree Shaking tối đa khi xây dựng thư viện, Rollup vẫn là tiêu chuẩn ngành.
Tiết kiệm từ Tree Shaking phụ thuộc nhiều vào kiến trúc dự án. Trong ứng dụng React sử dụng thư viện Ant Design, Tree Shaking có thể loại bỏ tới 70% mã thành phần UI. Trong dự án nơi tất cả import đều cụ thể và mục tiêu, tiết kiệm sẽ là 5–15%. Theo nghiên cứu của Webpack, tiết kiệm trung bình là 30–40% kích thước bundle.
| Loại mã chết | Ví dụ | Phát hiện Tree Shaking |
|---|---|---|
| Export không sử dụng | export function unusedHelper() | Có |
| Import không sử dụng | import { unused } from "lib" | Có |
| Nhánh điều kiện chết | if (false) { ... } | Không (bị minifier loại bỏ) |
| Hàm không được gọi sau DCE | function a(){} a() trong đó a không được gọi | Một phần |
Cơ chế Tree Shaking dựa trên đồ thị phụ thuộc (dependency graph) mà trình bundle xây dựng từ tất cả các câu lệnh import/export trong dự án. Ở giai đoạn đầu, trình bundle duyệt tất cả các tệp từ điểm vào (entry point) và thu thập cây module. Ở giai đoạn thứ hai, nó phân tích export nào từ mỗi module thực sự được import trong các module khác.
Cho mỗi module, Webpack hoặc Rollup đánh dấu các export là đã sử dụng hoặc không sử dụng. Các export không sử dụng bị loại khỏi bundle. Tuy nhiên, bản thân module vẫn ở lại trong bundle nếu ít nhất một export của nó được sử dụng. Một module chỉ có thể bị loại hoàn toàn thông qua cờ sideEffects hoặc nếu module không chứa bất kỳ tác dụng phụ nào.
// utils.js — module với các hàm
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 — điểm vào
import { formatDate } from "./utils";
const today = formatDate(new Date());
console.log(today);// Sau Tree Shaking — trong bundle chỉ có formatDate
function formatDate(date) {
return date.toISOString().slice(0, 10);
}
const today = formatDate(new Date());
console.log(today);Tree Shaking đã loại formatCurrency và slugify khỏi bundle cuối cùng vì chúng không được import trong app.js. Kích thước module utils.js giảm từ 3 hàm xuống còn 1. Nếu utils.js chứa tác dụng phụ (ví dụ, khởi tạo toàn cục), Tree Shaking không thể loại bỏ ngay cả các export không sử dụng.
Webpack bao gồm hỗ trợ Tree Shaking tích hợp thông qua TerserPlugin trong chế độ production. Để bật Tree Shaking, hai điều kiện là đủ: mode được đặt thành production (mode: "production") và các module sử dụng cú pháp ES (import/export). Webpack tự động đánh dấu các export không sử dụng và chuyển chúng cho Terser để loại bỏ.
Cấu hình bổ sung usedExports: true trong optimization.webpack.config.js cho phép phân tích chi tiết việc sử dụng export trong một module. Tùy chọn này xác định export nào thực sự được sử dụng và export nào chỉ được cung cấp (provided). Sự kết hợp của usedExports và Terser mang lại hiệu quả loại bỏ mã chết tối đa.
// webpack.config.js — cấu hình 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 }],
],
},
},
},
],
},
};Tham số chính là modules: false trong @babel/preset-env. Theo mặc định, Babel chuyển đổi module ES thành CommonJS, điều này giết chết Tree Shaking. modules: false ngăn Babel chuyển đổi import/export, giữ nguyên cú pháp ES cho Webpack. concatenateModules thêm vào đó hợp nhất các module vào một phạm vi dùng chung, giảm số lượng IIFE và giảm kích thước bundle.
Side effects (tác dụng phụ) là các hành động mà module thực hiện khi được import không liên quan đến các giá trị được xuất khẩu: kiểu toàn cục (import "./styles.css"), polyfill (import "core-js/stable"), khởi tạo biến toàn cục hoặc đăng ký Service Worker. Nếu một module chứa tác dụng phụ, trình bundle không thể loại bỏ nó khỏi bundle một cách an toàn, ngay cả khi không có export nào được sử dụng.
Cờ sideEffects trong package.json cho trình bundle biết module nào trong gói không có tác dụng phụ. Đối với gói mà tất cả module đều thuần khiết (chỉ export hàm), bạn nên chỉ định "sideEffects": false. Đối với gói có CSS hoặc polyfill — một mảng các đường dẫn đến các tệp có tác dụng phụ: "sideEffects": ["*.css"]. Nếu không có cờ này, Tree Shaking sẽ không loại bỏ ngay cả các hàm không sử dụng.
Để kiểm tra xem một module có tác dụng phụ hay không, hãy tự hỏi: import này có thực hiện hành động nào không liên quan đến việc xuất khẩu giá trị không? import "./styles.css" thêm CSS vào DOM — đó là tác dụng phụ. import { throttle } from "lodash-es" không có tác dụng phụ — nó chỉ làm cho hàm throttle có sẵn. Các polyfill (import "core-js/stable") có tác dụng phụ — chúng sửa đổi các nguyên mẫu toàn cục.
Đối với các module của riêng bạn, khuyên rút: trích xuất style và polyfill vào các điểm vào riêng biệt, tách các tiện ích thuần khiết (hàm không có tác dụng phụ) khỏi các module có tác dụng phụ (khởi tạo, ghi log, đăng ký Service Worker). Trong package.json của dự án cấp cao nhất, chỉ định "sideEffects": false nếu tất cả module đều thuần khiết. Nếu có style, chỉ định "sideEffects": ["*.css"] một cách chính xác.
{
"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"] có nghĩa: tất cả tệp CSS có tác dụng phụ (không thể loại bỏ) và polyfills.js cũng vậy. Tất cả tệp JS khác trong gói là thuần khiết — chúng có thể được shake một cách an toàn. Trường module chỉ định đường dẫn đến phiên bản ES của gói mà trình bundle nên sử dụng thay vì phiên bản CommonJS (main) cho Tree Shaking.
React Native với Metro Bundler hỗ trợ một phiên bản giới hạn của Tree Shaking. Metro không thực hiện phân tích tĩnh đầy đủ về các export được sử dụng (usedExports) như Webpack. Thay vào đó, Metro dựa vào Terser để loại bỏ các phần không sử dụng của module trong quá trình minification. Hiệu quả của cách tiếp cận này thấp hơn so với Tree Shaking đầy đủ trong Webpack.
Để tối ưu tối đa các dự án React Native, khuyên nên: sử dụng thư viện có module ES (trường module trong package.json), thêm babel-plugin-transform-remove-console để loại bỏ mã debug, và cấu hình Metro transformer.minifierConfig cho Terser. Ngoài ra, Ram Bundle (chia bundle thành các module) giảm tải các màn hình không sử dụng.
Câu hỏi thường gặp
CommonJS (require/module.exports) không hỗ trợ phân tích tĩnh — require có thể được gọi động trong các điều kiện và hàm. Trình bundle không thể xác định phần nào của module thực sự được sử dụng. Chỉ các module ES với import/export tĩnh mới cho phép Tree Shaking.
TypeScript hoàn toàn tương thích với Tree Shaking với điều kiện tsconfig.json được cấu hình cho module ES: "module": "esnext". Trình biên dịch TypeScript phải giữ nguyên import/export mà không chuyển đổi chúng thành CommonJS. Babel với @babel/preset-typescript và modules: false cũng truyền module ES một cách chính xác cho Webpack.
Webpack Bundle Analyzer là một plugin trực quan hóa thành phần bundle dưới dạng sơ đồ tương tác. Nếu một thư viện có mặt trong bundle nhưng các hàm của nó không được sử dụng, Tree Shaking đã không hoạt động. Bạn cũng có thể phân tích tệp đầu ra: tìm các export không sử dụng trong văn bản bundle qua grep.
Lodash v4 được phân phối dưới dạng gói CommonJS. Để Tree Shaking, bạn cần sử dụng lodash-es — phiên bản ES của thư viện. Thay thế import throttle from "lodash/throttle" bằng import { throttle } from "lodash-es" và cấu hình resolve.alias trong Webpack để thay thế lodash bằng lodash-es.
Tree Shaking làm tăng nhẹ thời gian xây dựng (5–15%) vì nó thêm giai đoạn phân tích đồ thị phụ thuộc và đánh dấu export đã sử dụng. Trong chế độ development, Tree Shaking thường bị tắt để tăng tốc. Trong production, thời gian bổ sung được bù đắp bởi việc giảm đáng kể kích thước bundle.
Tổng kết
Chúng tôi sẽ phát triển ứng dụng di động chìa khóa trao tay
IT Sectr tạo các ứng dụng iOS và Android cho các công ty khởi nghiệp và doanh nghiệp từ năm 2017. Chúng tôi sẽ tư vấn và đề xuất giải pháp tốt nhất cho bạn.
Đọc thêm