Lazy loading và code splitting: giảm dung lượng JavaScript tải lần đầu

  • author-image

    Nguyễn Đình Cường

  • blog-tag
  • blog-comment 0 comment
  • created-date 09 Nov, 2026
blog-thumbnail

Các ứng dụng web hiện đại hay "phình" ra: càng thêm tính năng và thư viện, gói JavaScript người dùng phải tải càng lớn. Trong khi đó, ở lần truy cập đầu, họ chỉ cần phần nhỏ của ứng dụng. Code splitting và lazy loading là cách chia nhỏ mã và chỉ tải khi cần.

Vấn đề với một gói lớn duy nhất

Mặc định, công cụ đóng gói (bundler) như Vite, webpack hay Rollup gộp mã thành một hoặc vài file lớn. Người dùng vào trang chủ vẫn phải tải mã của trang quản trị, của biểu đồ, của trình soạn thảo mà họ có thể không bao giờ mở. Hệ quả: tải lâu hơn, trình duyệt phải phân tích và chạy nhiều JavaScript hơn, nhất là khó chịu trên điện thoại cấu hình thấp.

Giải pháp: dynamic import

Hàm import() là tính năng chuẩn của JavaScript, trả về promise và tải module khi gọi. Bundler nhận ra nó và tự tách module đó thành file riêng (chunk):

const nut = document.querySelector('#xem-bieu-do');

nut.addEventListener('click', async () => {
  const { veBieuDo } = await import('./bieu-do.js');
  veBieuDo(document.querySelector('#khung'));
});

Mã vẽ biểu đồ (có thể kèm thư viện nặng) chỉ tải khi người dùng bấm nút. Bạn cũng có thể tải trước lúc rảnh hoặc khi rê chuột vào nút để cảm giác tức thì mà vẫn không tải thừa ở lần đầu.

React: lazy và Suspense

import { lazy, Suspense } from 'react';

const TrangQuanTri = lazy(() => import('./TrangQuanTri'));

function App() {
  return (
    <Suspense fallback={<p>Đang tải...</p>}>
      <TrangQuanTri />
    </Suspense>
  );
}

lazy nhận hàm trả về dynamic import của một component (xuất mặc định), Suspense hiển thị giao diện dự phòng trong lúc chờ tải. Cách phổ biến nhất là tách theo route, mỗi trang là một chunk riêng:

const TrangChu = lazy(() => import('./pages/TrangChu'));
const KhoaHoc = lazy(() => import('./pages/KhoaHoc'));
const CaiDat = lazy(() => import('./pages/CaiDat'));

Vue và các framework khác

Vue Router cho phép khai báo route với import động: component: () => import('./views/CaiDat.vue'). Còn trong Vue có thể dùng defineAsyncComponent cho từng component. Next.js tự động tách mã theo trang và hỗ trợ next/dynamic cho component tải theo nhu cầu. Hãy xem công cụ bạn dùng đã làm sẵn những gì trước khi tự làm.

Nên tách ở đâu?

  • Theo route: hiệu quả và dễ nhất.
  • Thành phần nặng và hiếm dùng: trình soạn thảo văn bản, biểu đồ, bản đồ, bộ chọn tệp, hộp thoại lớn.
  • Phần phía sau điều kiện: tính năng chỉ cho người dùng đã đăng nhập hoặc vai trò đặc biệt.
  • Thư viện lớn dùng ít: ví dụ bộ xử lý ngày tháng đầy đủ hoặc thư viện xuất PDF.

Đừng tách quá vụn. Mỗi chunk là một yêu cầu mạng thêm, và tách mọi thứ có thể tạo ra hiệu ứng "thác nước" làm chậm hơn. Hãy tách những phần đủ lớn để có lợi.

Đo trước khi tối ưu

  1. Xem kích thước gói: dùng công cụ trực quan hóa như rollup-plugin-visualizer (với Vite) hay webpack-bundle-analyzer để thấy thư viện nào chiếm nhiều dung lượng.
  2. Tab Network trong DevTools: xem file nào tải, kích thước và thứ tự.
  3. Tab Coverage: cho biết phần trăm mã JavaScript và CSS thực sự được dùng trên trang.
  4. Lighthouse: gợi ý giảm JavaScript không dùng.

Thường bạn sẽ tìm ra vài "thủ phạm" gây phình gói: thư viện nhập cả bộ khi chỉ dùng một hàm, hoặc hai thư viện cùng làm một việc.

Các biện pháp bổ sung

  • Tree shaking: bundler loại bỏ mã không dùng nếu bạn import đúng cách (import { debounce } from 'lodash-es' thay vì nhập cả thư viện cũ).
  • Chọn thư viện nhẹ khi có lựa chọn tương đương.
  • Preload hoặc prefetch chunk sắp dùng: <link rel='prefetch' href='...'> hoặc gợi ý magic comment của bundler.
  • Nén và cache: bật brotli hoặc gzip, đặt tên file có mã băm để cache lâu dài.
  • Lazy load cả ảnh và iframe dưới màn hình bằng loading='lazy'.

Xử lý trạng thái tải và lỗi

Khi tải theo nhu cầu, mạng có thể chậm hoặc lỗi. Hãy luôn có giao diện chờ gọn gàng (khung xương skeleton thay vì vòng xoay chung chung), và bắt lỗi tải chunk bằng error boundary trong React để người dùng có thể thử lại thay vì thấy màn hình trắng.

Kết luận

Chỉ gửi cho người dùng đúng phần mã họ cần ở thời điểm họ cần. Hãy bắt đầu từ tách theo route và các thành phần nặng, dùng công cụ phân tích để tìm điểm phình, và đo lại sau mỗi thay đổi.

Nếu muốn học cách tổ chức và tối ưu dự án frontend quy mô lớn, bạn có thể xem các khóa học tại codebem.com.

author_photo
Nguyễn Đình Cường

0 comment