Áp dụng Bazel để tối ưu hóa quy trình xây dựng Frontend Monorepo

05 tháng 4, 2026·5 phút đọc

Bài viết chia sẻ hành trình tích hợp Bazel vào một JavaScript monorepo nhằm tối ưu hóa hiệu quả xây dựng và kiểm thử. Tác giả đi sâu vào việc thiết lập Bazel cho gói hooks, bao gồm biên dịch TypeScript, chạy Jest test và ESLint thông qua các target của Bazel. Đây là giải pháp hứa hẹn giúp tăng tốc độ CI/CD nhờ khả năng caching thông minh.

Áp dụng Bazel để tối ưu hóa quy trình xây dựng Frontend Monorepo

Tối ưu hóa hiệu suất xây dựng (build) và kiểm thử (test) luôn là mối quan tâm hàng đầu của các đội ngũ phát triển phần mềm, đặc biệt là với các dự án monorepo quy mô lớn. Gần đây, tôi đã có cơ hội thử nghiệm Bazel — một hệ thống build mã nguồn mở của Google — để cải thiện pipeline CI cho monorepo JavaScript của mình. Bài viết này sẽ ghi chép lại quá trình "Bazel hóa" một gói thư viện trong dự án, từ thiết lập môi trường đến biên dịch, chạy unit test và linting code.

Tại sao chọn Bazel?

Tôi hiện đang quản lý một JavaScript monorepo tên là Pedalboard, bao gồm các thư viện React component, hooks, lint plugins và công cụ phát triển. Vấn đề chính là quy trình CI hiện tại: mỗi khi có thay đổi, GitHub Actions sẽ kích hoạt và chạy pnpm installpnpm run build cho toàn bộ dự án, bất kể gói nào thực sự bị ảnh hưởng.

Mặc dù các script test và lint thông minh hơn khi chỉ chạy trên các gói có thay đổi, nhưng việc cài đặt và build lại tất cả mọi thứ là một sự lãng phí lớn. Bazel nổi tiếng với khả năng tăng tốc thời gian build nhờ cơ chế caching thông minh và chỉ rebuild lại những gì thực sự thay đổi. Mục tiêu của tôi là kiểm tra xem Bazel có phù hợp để giải quyết bài toán này hay không.

Kết quả build ESM và CJS với BazelKết quả build ESM và CJS với Bazel

Chuẩn bị dự án cho Bazel

Thay vì cài đặt Bazel trực tiếp, tôi sử dụng Bazelisk — một công cụ quản lý phiên bản Bazel tương tự như nvm cho Node.js. Nó đọc file .bazelversion và tự động tải xuống binary tương ứng, đảm bảo cả đội ngũ dùng cùng một phiên bản.

Cấu hình ban đầu tại thư mục gốc bao gồm file MODULE.bazel để khai báo module và các phụ thuộc như rules_nodejs cùng aspect_rules_js — bộ quy tắc quan trọng để Bazel làm việc với npm và pnpm.

Điểm thú vị là Bazel không dùng thư mục node_modules truyền thống. Nó tải các gói npm vào cache nội bộ của mình. Để các công cụ khác trong dự án vẫn hoạt động, chúng ta cần tạo symlink từ cache của Bazel quay lại node_modules thông qua rules_js.

Thiết lập quy trình Build cho gói Hooks

Tôi chọn gói hooks làm mục tiêu thí điểm. Đây là một gói TypeScript đơn giản, cần biên dịch sang ESM và CJS, đồng thời tạo ra các tệp khai báo kiểu (.d.ts).

Biên dịch TypeScript với SWC

Để biên dịch nhanh, tôi sử dụng aspect_rules_swc (bao bọc compiler SWC). File BUILD.bazel trong gói hooks được cấu hình để tạo ra các target compile_esmcompile_cjs dựa trên cấu hình SWC tương ứng.

Việc chạy lệnh pnpm bazel build :build sẽ tạo ra các artefacts trong thư mục bazel-bin.

Tạo tệp khai báo kiểu

SWC không tạo ra tệp .d.ts, nên tôi cần thêm aspect_rules_ts để sử dụng tsc ở chế độ chỉ khai báo (declaration-only). Điều này yêu cầu một file BUILD.bazel tại thư mục gốc để cấu hình tsconfig cơ bản và link các node_modules.

Cấu trúc file sau khi thêm biên dịch kiểuCấu trúc file sau khi thêm biên dịch kiểu

Copy kết quả về thư mục dist

Theo mặc định, Bazel ghi kết quả build vào bazel-bin, không phải cây thư mục nguồn. Tuy nhiên, công cụ xuất bản (publishing) của tôi mong đợi thư mục dist nằm ngay trong gói. Tôi sử dụng aspect_bazel_lib với các target copy_to_directorywrite_source_files để sao chép kết quả build quay lại dist/ một cách tự động.

Tích hợp Testing và Linting

Chạy Jest với Bazel

Gói hooks sử dụng Jest để test. Thông qua aspect_rules_jest, tôi có thể định nghĩa target jest_test. Một lưu ý nhỏ là các cấu hình Jest cần được truy cập từ sandbox của Bazel, do đó chúng ta cần dùng js_library tại gốc để expose file cấu hình.

Để xử lý báo cáo độ phủ (coverage), do Jest chạy trong sandbox nên đường dẫn file trong báo cáo sẽ trỏ vào cache của Bazel, khiến công cụ báo cáo (như NYC) không tìm thấy source. Giải pháp là viết lại (remap) các đường dẫn này tại thời điểm thu thập dữ liệu về dạng đường dẫn tương đối của repo gốc.

Tích hợp ESLint

Cuối cùng, tôi sử dụng aspect_rules_lint để chạy ESLint. Quy trình này yêu cầu tạo một package công cụ tại tools/lint để định nghĩa binary eslint và aspect lint_eslint_aspect. Các gói con chỉ cần load tệp cấu hình Starlark này và chạy target eslint_test.

Tổng kết

Việc chuyển đổi một gói monorepo sang Bazel không hề đơn giản; tài liệu đôi khi còn thiếu sót và các thông báo lỗi chưa thân thiện. Tuy nhiên, kết quả thu được rất đáng giá:

  • Quy trình build, test và lint được quản lý chặt chẽ qua các Bazel target.
  • Tận dụng được cơ chế local caching, giúp các lần chạy lặp lại gần như tức thì.
  • Đặt nền móng vững chắc để triển khai remote caching trong CI/CD trong tương lai.

Hiện tại, gói hooks đã được Bazel hóa và đưa vào sản xuất thực tế. Dù toàn bộ monorepo chưa được di chuyển hoàn toàn, nhưng đây là bước đi đầu tiên hứa hẹn sẽ cải thiện đáng kể hiệu suất phát triển và tích hợp liên tục.

Bài viết được tổng hợp và biên soạn bằng AI từ các nguồn tin tức công nghệ. Nội dung mang tính tham khảo. Xem bài gốc ↗