Khi một trang web gói toàn bộ JavaScript vào một file duy nhất, trình duyệt buộc phải tải xuống, phân tích (parse) và biên dịch (compile) hết khối mã đó trước khi trang phản hồi mượt. Phần lớn công việc này diễn ra trên main thread (luồng chính — luồng duy nhất xử lý cả giao diện lẫn tương tác), nên trong lúc đó người dùng bấm vào đâu cũng thấy khựng. Đây chính là vấn đề mà việc chia nhỏ mã giải quyết.
Vì sao một bundle lớn lại làm trang chậm
Theo tài liệu của web.dev, một long task (tác vụ dài) là bất kỳ tác vụ JavaScript nào chạy quá 50 mili-giây trên main thread; trong suốt thời gian đó trình duyệt không xử lý được thao tác người dùng. Một file JS khổng lồ thường tạo ra một tác vụ dài duy nhất, làm tăng TBT (Total Blocking Time — tổng thời gian chặn) và kéo theo INP (Interaction to Next Paint — độ trễ tới khung hình kế tiếp sau khi người dùng tương tác) kém.
INP hiện là chỉ số chính thức thay cho FID từ tháng 3/2024, với ngưỡng “Good” là dưới 200ms (đo ở phân vị 75 — p75). Hai chỉ số còn lại trong bộ Core Web Vitals giữ nguyên ngưỡng năm 2026: LCP (Largest Contentful Paint) ≤ 2.5s và CLS (Cumulative Layout Shift) ≤ 0.1. Trong ba chỉ số, INP là cái mà nhiều site trượt nhất, và nó nhạy trực tiếp với khối lượng JavaScript phải xử lý ngay khi tải trang.
web.dev mô tả rõ logic của code splitting: thay vì một tác vụ lớn có thể chặn main thread, bạn có nhiều tác vụ nhỏ hơn — chúng chặn ít hơn, hoặc không chặn. Đó là lý do chia nhỏ mã giúp cải thiện độ phản hồi khi tải trang.

Code splitting khác tree-shaking thế nào
Hai khái niệm này hay bị lẫn nhưng giải quyết hai bài toán khác nhau:
| Kỹ thuật | Làm gì | Kết quả |
|---|---|---|
| Tree-shaking (rung cây) | Loại bỏ mã chết — phần code import vào nhưng không bao giờ dùng tới | Bundle nhỏ lại vì bớt code thừa |
| Code splitting (chia nhỏ mã) | Cắt code đang dùng thành nhiều mảnh, tải lười phần chưa cần ngay | JS tải ban đầu nhẹ đi, phần còn lại tải sau |
Nói gọn: tree-shaking loại code không bao giờ chạy, còn code splitting hoãn lại code có chạy nhưng chưa cần ngay từ giây đầu. Hai kỹ thuật bổ trợ nhau, nên dùng cả hai.

Ba kiểu chia nhỏ mã thường gặp
1. Dynamic import — chia ở mức module
Cú pháp import() trả về một Promise và chỉ tải module đúng lúc gọi. Đây là nền tảng của mọi kiểu code splitting. Ví dụ: thư viện vẽ biểu đồ nặng chỉ nên tải khi người dùng thật sự mở phần biểu đồ.
// Tải tĩnh: gói vào bundle ban đầu (nặng ngay từ đầu)
// import { renderChart } from './chart';
// Tải lười bằng dynamic import: chỉ tải khi cần
button.addEventListener('click', async () => {
const { renderChart } = await import('./chart.js');
renderChart(data);
});Trình đóng gói (bundler) như Vite, webpack hay Rollup tự cắt ./chart.js ra một chunk riêng và chỉ tải khi dòng import() chạy.
2. Chia theo route (route-based)
Mỗi trang/đường dẫn là một chunk riêng. Người dùng vào trang chủ thì chỉ tải JS của trang chủ, chưa tải JS của trang giỏ hàng hay trang quản trị. Với React thường dùng React.lazy kết hợp Suspense:
import { lazy, Suspense } from 'react';
const CheckoutPage = lazy(() => import('./pages/Checkout.jsx'));
<Suspense fallback={<Spinner />}>
<CheckoutPage />
</Suspense>Một số framework làm sẵn việc này: Next.js App Router tự chia mã theo route, mỗi trang là một bundle riêng, nên bạn chỉ cần lazy thủ công cho những component nặng không phải lúc nào cũng hiển thị.
3. Lazy component — chia theo thành phần giao diện
Những khối chỉ xuất hiện khi người dùng tương tác (modal, tab ẩn, trình soạn thảo, bản đồ) nên tách thành chunk riêng và chỉ tải khi mở. Cách này tránh nhồi mã của tính năng “thi thoảng mới dùng” vào lần tải đầu tiên.
Cẩn thận khi chia nhỏ mã
- Đừng chia quá vụn. Mỗi chunk là một request mạng; cắt thành hàng trăm mảnh tí hon có thể làm chậm vì chi phí request và làm waterfall (chuỗi tải nối đuôi) dài ra.
- Phân tách rõ “tải lười” với “thứ tự thực thi”. Code splitting quyết định khi nào tải một mảnh; còn việc script không chặn render thì là chuyện của cách nạp script defer hay async. Hai khâu này độc lập, làm cả hai mới gọn.
- Preload phần chắc chắn cần. Với chunk người dùng gần như sẽ chạm tới (route kế tiếp), có thể báo trình duyệt tải sớm để tránh khựng khi chuyển trang.
- Có fallback khi tải lỗi. Chunk tải lười có thể trượt do mạng; cần xử lý trạng thái lỗi để giao diện không treo.
Một góc thực chiến
Web22 vận hành web22.dev trên LiteSpeed + Cloudflare và đo Core Web Vitals bằng Lighthouse/PageSpeed thật. Kinh nghiệm rút ra khi tối ưu: con số “điểm” trong phòng lab có thể nhảy, nhưng nguyên tắc thì ổn định — phần JavaScript nào chưa cần ngay trong màn hình đầu thì đừng bắt trình duyệt tải và biên dịch ngay. Chia nhỏ mã đúng chỗ thường giảm rõ khối lượng main thread phải nuốt lúc khởi động, nhờ đó TBT trong đo lab dịu lại và trải nghiệm bấm/cuộn mượt hơn.
Câu hỏi thường gặp
Code splitting có làm trang phức tạp hơn không?
Có thêm một chút logic tải lười và trạng thái chờ. Nhưng với framework hiện đại, phần chia theo route gần như tự động, bạn chỉ cần để tâm tới vài component nặng.
Chia nhỏ mã có cải thiện SEO không?
Gián tiếp. Nó giúp giảm JS chặn main thread, kéo theo INP và TBT tốt hơn — mà Core Web Vitals là yếu tố trải nghiệm Google quan tâm.
Nên chia theo route hay theo component trước?
Bắt đầu từ route: đây là lằn ranh tự nhiên và cho hiệu quả lớn nhất. Sau đó mới tách thêm những component nặng ít khi hiển thị.
Nếu trang của bạn nặng JS và điểm phản hồi kém, dịch vụ tối ưu hiệu năng web của Web22 có thể rà bundle và chỉ ra chỗ nên chia nhỏ.
