0

Cách tôi giảm 67% Initial Bundle cho dự án Vue + Vite

Nếu bạn đang làm việc với một dự án Vue + Vite có nhiều module, có thể bạn đang gặp cùng một vấn đề mà tôi từng gặp: trang web tải chậm không phải vì code nhiều, mà vì code load sai thời điểm.

Bài viết này ghi lại những gì tôi đã làm — không có gì fancy, chỉ là thay đổi cách import — nhưng kết quả thì khá ấn tượng.

Chuyện gì đang xảy ra?

Dự án của tôi là một SPA khá lớn: hơn 10 module, hàng trăm component. Mọi thứ chạy ổn cho đến khi tôi để ý rằng lần đầu truy cập, trình duyệt phải tải một file JS nặng gần 1.5 MB.

Vấn đề nằm ở đây: Vite bắt đầu từ main.ts, đi theo tất cả các import statement, và nhồi mọi thứ vào một chunk duy nhất. Nếu bạn import tĩnh — Vite hiểu rằng "à, cái này cần ngay" — và đóng gói nó vào initial bundle.

main.ts
  ├── router/index.ts
  │     ├── import LayoutA             ← load ngay
  │     ├── import LayoutB             ← load ngay
  │     └── ...modules/*/routes.ts
  │           ├── import MainLayout     ← load ngay
  │           ├── import PageDashboard  ← load ngay
  │           └── ... (15+ components)
  ├── MainLayout.vue
  │     ├── import ModalSettings       ← load ngay
  │     ├── import SidebarChat         ← load ngay
  │     └── ... (9 components)
  └── HeavyChartPlugin (global plugin) ← load cho MỌI trang

User chỉ vào trang chủ thôi, nhưng phải tải code của cả trang Chat, trang Dashboard, trang Quản lý... Nghe vô lý đúng không?

Và đây là kết quả sau khi sửa

Trước Sau Thay đổi
Entry chunk 1,487.79 KB 479.95 KB -67.7%
Entry chunk (gzip) 412.34 KB 158.58 KB -61.5%
CSS preload 25 files 18 files -28%
Tổng JS 11.03 MB 11.06 MB ~0%

Dòng cuối là điểm quan trọng nhất: tổng dung lượng không đổi. Tôi không xoá code nào cả — chỉ thay đổi thời điểm nó được load.

# Entry chunk trước tối ưu
- index-[hash].js    1,487.79 kB │ gzip: 412.34 kB

# Entry chunk sau tối ưu
+ index-[hash].js      479.95 kB │ gzip: 158.58 kB

Tôi đã làm gì?

1. Bỏ component ra khỏi global

Đây là sai lầm "kinh điển" mà ai cũng có thể mắc: đăng ký một plugin global trong main.ts dù nó chỉ dùng ở 3 component.

Câu hỏi tôi tự đặt ra: "Plugin này có dùng ở hơn một nửa số trang không?" — Nếu không, nó không xứng đáng là global.


2. Lazy load route components — thay đổi lớn nhất

Đây là nơi mang lại kết quả rõ rệt nhất. Hầu hết route files đều import tĩnh component ở đầu file:

Tôi áp dụng cách này lên ~15 components trên 10 route files.


3. defineAsyncComponent cho những component lớn & không hiện ngay

Tối ưu này tôi áp dụng cho MainLayout — layout chính mà tất cả các trang đều dùng. Mọi thứ import tĩnh trong MainLayout sẽ nằm trong initial bundle của toàn bộ ứng dụng, nên mỗi KB tiết kiệm ở đây đều có tác động lên mọi route.

Giả sử trong layout của bạn có một component rất lớn (ví dụ: HeavyComponent), nhưng nó chỉ hiển thị khi người dùng bấm nút mở (v-if). Nếu import tĩnh theo thói quen thông thường, component này sẽ bị nhét vào initial bundle và khiến trang load chậm một cách vô ích.

4. ESLint rule để team không "undo" tối ưu

Tối ưu xong, lo nhất là teammate commit thêm route với static import. Nên tôi thêm luôn ESLint rule:

// eslint.config.mjs
{
  files: ['src/**/routes.ts'],
  rules: {
    'no-restricted-syntax': ['warn', {
      selector: 'Property[key.name="component"][value.type="Identifier"]',
      message: 'Route component should use dynamic import: component: () => import("...")',
    }],
  },
},

Rule này dùng AST selector: nếu component trỏ đến một biến (Identifier) thay vì arrow function → cảnh báo ngay trong editor. Không cần code review mới phát hiện.


Những gì tôi rút ra

  1. Mặc định dùng () => import() cho route — Không có lý do gì để static import component trong route config.
  2. Global plugin phải thật sự global — Nếu chỉ dùng ở vài trang, hãy import local.
  3. v-if component = defineAsyncComponent — Đặc biệt với modal, drawer, notification.
  4. Encode rule vào ESLint — Tối ưu thủ công sẽ bị regression, tooling thì không.

Tất cả những thay đổi trên đều đơn giản — không cần refactor kiến trúc, không cần thêm library, chỉ cần hiểu Vite bundle code như thế nào và thay đổi cách import cho phù hợp.



All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí