Bundle Size Audit: Giảm 60% JavaScript với tree-shaking đúng cách
Hướng dẫn thực tế cách audit bundle size và áp dụng tree-shaking hiệu quả để giảm đến 60% lượng JavaScript gửi đến browser.
Tuần trước mình audit lại bundle của một dự án Next.js — kết quả: giảm từ 340KB xuống 128KB gzipped. Không phải magic, chỉ là tree-shaking đúng cách và loại bỏ những thứ không cần thiết.
Bước 1: Xem bundle đang chứa gì
Trước khi optimize, phải biết vấn đề ở đâu. Dùng @next/bundle-analyzer hoặc source-map-explorer:
# Cài bundle analyzer
npm install -D @next/bundle-analyzer
# Chạy analyze
ANALYZE=true npm run build
Với Vite projects, dùng rollup-plugin-visualizer. Output là một treemap cho thấy từng dependency chiếm bao nhiêu.
Bước 2: Những thủ phạm phổ biến
Sau khi audit nhiều dự án, mình thấy pattern lặp lại:
- lodash: Import cả library thay vì từng function → +70KB
- moment.js: Đã deprecated, chuyển sang
date-fnshoặcdayjs - Icon libraries: Import toàn bộ icon set thay vì individual icons
- Polyfills: Vẫn ship polyfills cho features đã support 95%+ browsers
Bước 3: Tree-shaking thực sự hoạt động thế nào
Tree-shaking chỉ hoạt động khi module dùng ES modules (import/export), không phải CommonJS (require). Bundler sẽ phân tích static imports và loại bỏ code không được reference.
Nhưng có nhiều trường hợp tree-shaking thất bại:
// ❌ Tree-shaking KHÔNG hoạt động
import _ from "lodash";
_.debounce(fn, 300);
// ❌ Named import nhưng lodash main entry dùng CJS
import { debounce } from "lodash";
// ✅ Import trực tiếp từ module path
import debounce from "lodash/debounce";
// ✅ Hoặc dùng lodash-es (ES module version)
import { debounce } from "lodash-es";
// ✅ Tốt nhất: tự viết, debounce chỉ có 10 dòng
function debounce<T extends (...args: unknown[]) => void>(
fn: T, ms: number
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout>;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), ms);
};
}
Bước 4: Kiểm tra sideEffects
Trong package.json, field sideEffects rất quan trọng. Nếu một package không khai báo "sideEffects": false, bundler sẽ giữ lại tất cả code vì sợ có side effects (CSS imports, global polyfills...).
Với code của bạn, thêm vào package.json:
{
"sideEffects": ["**/*.css", "**/*.scss"]
}
Điều này nói với bundler: "Chỉ có CSS files có side effects, còn lại cứ tree-shake thoải mái."
Bước 5: Dynamic imports cho heavy components
Không phải mọi code đều cần load ngay. Chart library, rich text editor, date picker — những thứ này nên lazy load:
// Next.js dynamic import
import dynamic from "next/dynamic";
const RichEditor = dynamic(
() => import("@/components/editor/tiptap-editor"),
{
ssr: false,
loading: () => <div className="h-64 animate-pulse bg-muted rounded" />
}
);
Checklist nhanh
- Chạy bundle analyzer, xác định top 5 dependencies lớn nhất
- Replace barrel imports bằng direct imports
- Kiểm tra tất cả dependencies có hỗ trợ ES modules không
- Thêm
sideEffectsfield vào package.json - Dynamic import cho components > 50KB
- Set browserslist hợp lý để bỏ polyfills không cần
Bundle size nhỏ = load nhanh = UX tốt = SEO tốt. Đầu tư 2-3 giờ audit sẽ cải thiện performance đáng kể cho toàn bộ user base.
Admin
Bình luận (0)
Đăng nhập để bình luận
Chưa có bình luận nào. Hãy là người đầu tiên!