Trong các ứng dụng Single Page Application (SPA) truyền thống, toàn bộ mã nguồn JavaScript được đóng gói thành một file bundle.js khổng lồ. Điều này dẫn đến thời gian tải ban đầu (Initial Load Time) tăng theo tỉ lệ thuận với độ phức tạp của ứng dụng.
Code Splitting giải quyết vấn đề này bằng cách chia nhỏ bundle thành các "chunks" và chỉ tải chúng khi cần thiết (Lazy Loading).
1. Bundle Analysis (Đo lường trước)
Trước khi tối ưu, cần xác định rõ các module nào đang chiếm dung lượng lớn nhất.
Sử dụng công cụ @next/bundle-analyzer để trực quan hóa cấu trúc bundle.
ANALYZE=true npm run buildCác "nạn nhân" thường gặp:
- Moment.js / Lodash: Import toàn bộ thư viện chỉ để dùng 1 hàm.
- Rich Text Editors: TinyMCE, CKEditor.
- Data Visualization: Chart.js, D3.js.
2. Route-based Splitting (Next.js Default)
Next.js tự động thực hiện Code Splitting ở cấp độ Route (Page).
- User truy cập
/dashboard-> Chỉ tải code của Dashboard. - Code của
/settingssẽ không được tải cho đến khi user điều hướng hoặc hover vào link (Prefetching).
Bài học: Giữ cây dependency của mỗi Page càng độc lập càng tốt. Tránh import các module nặng ở layout.tsx gốc nếu chỉ một page con sử dụng nó.
3. Component-based Splitting (Dynamic Import)
Với các thành phần giao diện nặng nhưng không nằm trên viewport ngay lập tức (Modal, Drawer, Chart dưới fold), sử dụng next/dynamic (bản chất là React.lazy + Suspense).
Dynamic Import Pattern
import dynamic from 'next/dynamic';
// HeavyComponent chỉ được tải khi component cha render nó
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <div className="h-64 bg-gray-100 animate-pulse" />,
ssr: false, // Tắt Server-Side Rendering nếu thư viện phụ thuộc window/document
});
export default function Dashboard() {
const [showChart, setShowChart] = useState(false);
return (
<section>
<h1>Analytics</h1>
<button onClick={() => setShowChart(true)}>View Chart</button>
{/* Chunk JS của HeavyChart chỉ được download khi showChart = true */}
{showChart && <HeavyChart />}
</section>
);
}4. Library Optimization Case Study
Vấn đề: Import import { map } from 'lodash' vẫn có thể khiến Webpack bundle cả thư viện Lodash (70KB) thay vì chỉ hàm map (2KB) nếu Tree Shaking không hoạt động hiệu quả.
Giải pháp:
- Selective Import:
import map from 'lodash/map'. - Modern Alternatives: Sử dụng các thư viện hỗ trợ ES Modules tốt hơn (VD:
date-fnsthay vìmoment.js).
5. Streaming & Suspense Architecture (React 18+)
Trong App Router, Code Splitting được nâng cấp với Streaming Server Rendering. Thay vì đợi toàn bộ trang render xong, Server sẽ stream HTML về Browser từng phần.
<Suspense fallback={<LoadingSkeleton />}>
{/* Component này có thể fetching data chậm hoặc bundle JS lớn */}
<Feed />
</Suspense>Cơ chế này giúp giảm TTI (Time to Interactive) và cải thiện First Contentful Paint (FCP) đáng kể, đặc biệt trên mạng chậm.
Kết luận
Mục tiêu tối thượng của Code Splitting là giảm lượng JavaScript không cần thiết (Unused CSS/JS) trong lần tải đầu tiên. "Code nhanh nhất là code không bao giờ được tải".