Làm chủ Vite: Kỹ thuật Bundling hiệu năng cao và phát triển Plugin tùy chỉnh

Programming tutorial - IT technology blog
Programming tutorial - IT technology blog

Khoảng nghỉ cà phê 30 giây không ai mong muốn

Vài năm trước, tôi đã tham gia một dự án React mà mỗi lần lưu file là phải đợi tới 20 giây để rebuild. Nó vừa đủ lâu để tôi đi pha một tách cà phê, nhưng cũng đủ ngắn để phá vỡ hoàn toàn sự tập trung của mình. Khi đó chúng tôi sử dụng cấu hình Webpack tiêu chuẩn, và khi dự án chạm mốc 500 component, nó đơn giản là không thể theo kịp nữa. Vòng lặp phản hồi (feedback loop) đã hoàn toàn bị phá vỡ.

Sự chậm trễ này không chỉ là một sự khó chịu nhỏ; nó là kẻ thù của năng suất. Khi bạn đang trong trạng thái tập trung cao độ (flow state), việc chờ đợi 10 giây cho Hot Module Replacement (HMR) cảm giác như dài vô tận. Bạn sẽ theo bản năng cầm điện thoại lên hoặc mở một tab mới, và thế là đà làm việc biến mất. Nhiều đội ngũ chấp nhận sự chậm chạp này như một khoản “thuế dự án lớn” tất yếu, nhưng đó là một quan niệm sai lầm mà chúng ta cần thay đổi.

Tại sao các Bundler đời cũ lại chạm ngưỡng giới hạn

Để khắc phục sự chậm chạp của dev server, chúng ta phải hiểu rõ nút thắt cổ chai nằm ở đâu. Các công cụ như Webpack hay Rollup được thiết kế trước khi trình duyệt hỗ trợ module một cách tự nhiên (native). Chúng hoạt động giống như một thủ thư tỉ mỉ, người phải lập chỉ mục lại toàn bộ thư viện mỗi khi có một cuốn sách được trả lại. Chúng quét toàn bộ đồ thị phụ thuộc (dependency graph), xử lý mọi file Sass và hợp nhất hàng nghìn module thành một bundle khổng lồ trước khi server kịp khởi động.

Độ phức tạp tăng theo cấp số nhân khi dự án mở rộng từ 100 lên 5.000 file. Ngay cả với cơ chế build tăng trưởng (incremental builds), khối lượng JavaScript khổng lồ mà trình duyệt phải phân tích mỗi khi reload vẫn tạo ra một rào cản lớn. Chi phí để liên kết các file lại với nhau trở thành một cuộc chiến không hồi kết với sự phát triển của dự án.

Sự chuyển dịch: Webpack vs. Vite

Webpack đã khẳng định vị thế tiêu chuẩn trong ngành nhờ tính linh hoạt tuyệt đối. Tuy nhiên, sự linh hoạt đó thường đi kèm với “gánh nặng cấu hình”, yêu cầu hàng giờ đồng hồ để tinh chỉnh các loader. Vite chọn một con đường hoàn toàn khác bằng cách tận dụng khả năng sẵn có của trình duyệt hiện đại.

Vite bỏ qua hoàn toàn bước đóng gói (bundling) trong quá trình phát triển. Nó cung cấp mã nguồn của bạn thông qua native ES Modules (ESM). Khi trình duyệt gặp một câu lệnh import, nó sẽ yêu cầu file cụ thể đó từ server Vite theo nhu cầu (on demand). Cách tiếp cận này giúp thời gian khởi động server gần như tức thì, bất kể kích thước dự án. Đối với môi trường production, Vite chuyển sang sử dụng Rollup, vốn đã được tinh chỉnh để tạo ra các asset tinh gọn và hiệu suất cao.

Tối ưu hóa Vite thực tế cho Production

Các thiết lập mặc định hiếm khi đủ cho các ứng dụng có lưu lượng truy cập cao. Nếu bạn chỉ chạy npm run build mà không điều chỉnh gì, bạn có nguy cơ xuất bản một file “vendor blob” cồng kềnh, gây ảnh hưởng xấu đến các chỉ số Core Web Vitals. Theo kinh nghiệm của tôi, một vài tinh chỉnh chiến lược thường có thể giảm thời gian tải ban đầu từ 40% trở lên.

1. Chiến lược Manual Chunking

Theo mặc định, Vite có thể gộp tất cả các dependency của bạn vào một file vendor.js duy nhất. Điều này phá hủy hiệu quả của cơ chế lưu bộ nhớ đệm (caching). Nếu bạn chỉ cập nhật một thư viện tiện ích nhỏ, người dùng vẫn bị buộc phải tải lại toàn bộ bundle vendor nặng 800KB. Chúng ta có thể giải quyết vấn đề này bằng cách tách các thư viện ổn định thành các chunk riêng biệt trong vite.config.ts.

import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks(id) {
          if (id.includes('node_modules')) {
            if (id.includes('react')) return 'vendor-react';
            if (id.includes('lodash') || id.includes('axios')) return 'vendor-utils';
            return 'vendor'; 
          }
        },
      },
    },
  },
});

2. Kích hoạt nén nâng cao

Các trình duyệt hiện đại xử lý nén Brotli hiệu quả hơn nhiều so với Gzip tiêu chuẩn. Nó thường có thể thu nhỏ một file CSS từ 150KB xuống chỉ còn 25KB. Vì Vite không tự động nén file, bạn nên tích hợp vite-plugin-compression vào quy trình của mình.

bash
npm install vite-plugin-compression --save-dev

Cập nhật cấu hình của bạn để ưu tiên Brotli nhằm đạt kết quả tốt nhất:

import viteCompression from 'vite-plugin-compression';

export default defineConfig({
  plugins: [viteCompression({ algorithm: 'brotliCompress', ext: '.br' })],
});

Viết Custom Plugin cho các nhu cầu đặc thù

Vite sử dụng giao diện plugin của Rollup, giúp việc mở rộng trở nên dễ dàng đến ngạc nhiên. Bạn không cần phải chờ đợi một plugin từ cộng đồng nếu bạn có một yêu cầu đặc thù. Ví dụ, hãy xây dựng một plugin chèn timestamp của bản build vào HTML để giúp xác thực việc triển khai trong môi trường staging.

// vite-plugin-timestamp.ts
export default function timestampPlugin() {
  return {
    name: 'timestamp-plugin',
    transformIndexHtml(html) {
      const now = new Date().toISOString();
      return html.replace(
        '<head>',
        `<head><meta name="build-timestamp" content="${now}">`
      );
    },
  };
}

Hệ thống dựa trên hook này cho phép bạn can thiệp vào quá trình build ở nhiều giai đoạn khác nhau. Bạn có thể biến đổi mã nguồn, giải quyết các đường dẫn tùy chỉnh hoặc sửa đổi kết quả đầu ra cuối cùng chỉ với vài dòng TypeScript.

Kiểm tra hiệu quả

Tối ưu hóa sẽ chỉ là phỏng đoán nếu thiếu dữ liệu trực quan. Tôi luôn đưa rollup-plugin-visualizer vào các dự án của mình để phát hiện tình trạng “phình to dependency”. Nó tạo ra một sơ đồ cây (treemap) tương tác, hiển thị chính xác thư viện nào đang chiếm nhiều diện tích nhất.

import { visualizer } from 'rollup-plugin-visualizer';

export default defineConfig({
  plugins: [
    visualizer({
      open: true,
      filename: 'stats.html',
      gzipSize: true,
    }),
  ],
});

Khi bạn chạy lệnh build, một tab trình duyệt sẽ mở ra hiển thị cấu trúc bundle của bạn. Nếu bạn thấy một khối khổng lồ cho một thư viện chỉ được sử dụng trong một component ít dùng, đó là lúc bạn nên áp dụng lazy loading hoặc tìm một lựa chọn thay thế nhẹ hơn.

Chuyển sang Vite giống như việc đổi một động cơ diesel nặng nề lấy một động cơ điện—nó êm hơn, nhanh hơn và hiệu quả hơn. Bằng cách làm chủ manual chunking và custom plugin, bạn đảm bảo môi trường phát triển của mình luôn nhanh chóng trong khi ứng dụng production vẫn luôn gọn nhẹ. Đừng mất thời gian vật lộn với các công cụ build nữa, hãy để chúng thúc đẩy quy trình làm việc của bạn.

Share: