Cơn ác mộng Hotfix lúc 2:15 sáng
Đó là một buổi sáng thứ Ba, và đồng hồ đã điểm 2:15 sáng. Một lỗi nghiêm trọng vừa xuất hiện trên môi trường production, làm sập trang thanh toán của khoảng 15% người dùng. Tôi đã chuẩn bị xong bản sửa lỗi trong năm phút—một thay đổi chỉ duy nhất một dòng mã trong một thư viện tiện ích xác thực dùng chung. Tôi đẩy code lên và chờ đợi tín hiệu xanh từ hệ thống.
Thay vì được deploy nhanh chóng, tôi phải ngồi nhìn pipeline CI xoay vòng. Mười phút trôi qua. Rồi hai mươi. Rồi ba mươi. Kho lưu trữ TypeScript nguyên khối (monolithic) của chúng tôi đang build lại mọi thứ: trang cửa hàng React, NestJS API, bảng điều khiển admin và hơn 30 thư viện dùng chung. Tất cả những việc này chỉ vì một thay đổi ký tự duy nhất trong một thư mục tiện ích.
Vào thời điểm bản sửa lỗi lên được production, 45 phút đã trôi qua vô ích. Trong một môi trường đầy áp lực, sự chậm trễ đó không chỉ gây khó chịu; nó còn rất tốn kém. Đây chính là “chi phí monorepo” (monorepo tax). Khi codebase của bạn phát triển, thời gian build thường tăng theo cấp số nhân cho đến khi nó dập tắt hoàn toàn động lực của đội ngũ.
Nguyên nhân gốc rễ: Sai lầm “Build mọi thứ”
Nhiều đội ngũ bắt đầu monorepo bằng cách gom các thư mục lại với nhau bằng Lerna hoặc npm workspaces cơ bản. Thông thường, script build là một công cụ thô sơ: "build": "npm run build --workspaces". Cách này hoạt động ổn khi bạn chỉ có hai ứng dụng. Nhưng nó sẽ thất bại ngay khi bạn mở rộng quy mô.
Vấn đề thực sự là sự thiếu hụt khả năng nhận diện phụ thuộc (dependency awareness). Hầu hết các hệ thống build coi một kho lưu trữ là một danh sách phẳng các dự án. Họ không nhận ra rằng nó thực chất là một Đồ thị có hướng không chu trình (Directed Acyclic Graph – DAG). Nếu hệ thống của bạn không biết rằng App A phụ thuộc vào Lib B, nhưng App C hoàn toàn không liên quan, nó sẽ mặc định chọn con đường an toàn nhất nhưng chậm nhất: build lại tất cả mọi thứ.
Chuyển đổi từ một lập trình viên senior sang một kiến trúc sư đòi hỏi sự thay đổi trong tư duy. Bạn phải ngừng tập trung duy nhất vào code. Bạn phải bắt đầu tối ưu hóa cơ sở hạ tầng để phân phối mã nguồn đó.
Cách Nx lập bản đồ kiến trúc của bạn
Nx không chỉ là một trình chạy tác vụ (task runner). Nó là một hệ thống build thông minh được thiết kế để hiểu các mối quan hệ giữa các dự án của bạn. Trong khi các công cụ như Turborepo phổ biến vì tốc độ, Nx cung cấp một hệ sinh thái sâu hơn cho các dự án TypeScript đa framework. Nó cho phép bạn trộn lẫn React, Angular và Node.js trong khi vẫn duy trì một đồ thị phụ thuộc chặt chẽ và có thể tìm kiếm được.
Trực quan hóa sự phức tạp
Trước khi có thể tối ưu hóa, bạn cần phải nhìn thấy đống “spaghetti” đó. Nx bao gồm một công cụ tích hợp để trực quan hóa đồ thị dự án của bạn:
npx nx graph
Lệnh này mở ra một bản đồ tương tác của toàn bộ kiến trúc. Bạn có thể thấy chính xác thư viện nào được liên kết chặt chẽ và thư viện nào bị cô lập. Đồ thị này đóng vai trò là “bộ não” mà Nx sử dụng để quyết định cái gì cần được kiểm thử và cái gì có thể bỏ qua.
Chiến lược 1: Chỉ build những gì thay đổi
Cách nhanh nhất để tăng tốc độ build là đừng chạy nó chút nào. Đây là lúc nx affected phát huy tác dụng. Thay vì chạy các tác vụ cho mọi dự án, Nx so sánh nhánh git hiện tại của bạn với một nhánh cơ sở (như main). Sau đó, nó tính toán khối lượng công việc tối thiểu cần thiết.
Nếu tôi sửa đổi một hàm trong libs/shared-ui, Nx sẽ kiểm tra đồ thị. Nếu chỉ có ứng dụng storefront import thư viện đó, Nx sẽ bỏ qua hoàn toàn việc build cho backend-api và admin-panel.
Tối ưu hóa quy trình CI của bạn
Hãy cập nhật cấu hình CI của bạn—cho dù bạn sử dụng GitHub Actions, GitLab hay Jenkins—để ngừng sử dụng các lệnh build chung chung. Hãy chuyển sang cú pháp affected:
# Đừng làm thế này: npx nx run-many -t build
# Hãy làm thế này: Chỉ build các dự án bị ảnh hưởng bởi PR của bạn
npx nx affected -t build --base=origin/main
Khi chúng tôi triển khai điều này, thời gian CI trung bình đã giảm từ 45 phút xuống còn khoảng 12 phút. Đó là một thắng lợi lớn cho năng suất của lập trình viên.
Chiến lược 2: Computation Caching (Bộ nhớ đệm tính toán)
Ngay cả với các lệnh affected, bạn vẫn thường thấy mình đang build lại mã nguồn không hề thay đổi. Có lẽ bạn vừa chuyển lại một nhánh trước đó, hoặc một đồng nghiệp đã build đúng phiên bản thư viện đó rồi. Nx giải quyết vấn đề này bằng thuật toán băm dựa trên nội dung (content-addressing hashing).
Nó tạo ra một mã băm (hash) dựa trên các tệp nguồn, biến môi trường và phiên bản công cụ của bạn. Nếu mã băm đó khớp với một lần chạy trước đó, Nx chỉ đơn giản là lấy kết quả từ cache. Cảm giác thật kỳ diệu khi một bản build đầy đủ kết thúc sau 200ms vì mọi tác vụ đều “khớp cache” (cache hit).
Vấn đề với Cache cục bộ
Theo mặc định, cache này nằm trong node_modules/.cache/nx trên máy cục bộ của bạn. Điều này giúp ích cho bạn, nhưng không giúp ích cho đội ngũ. Mỗi khi một trình chạy CI bắt đầu một công việc mới, đó là một môi trường “lạnh” (cold environment) với lịch sử bằng không. Để khắc phục điều này, bạn cần Remote Caching.
Chia sẻ Cache qua Nx Cloud
Nx Cloud cho phép toàn bộ tổ chức của bạn chia sẻ một bộ nhớ đệm duy nhất. Khi CI build nhánh main, nó sẽ tải các artifact lên. Khi bạn kéo code mới nhất về laptop và chạy build, máy tính của bạn sẽ tải xuống các tệp đã được build sẵn chỉ trong vài giây. Về cơ bản, bạn đang tận dụng công việc mà CI đã thực hiện.
Để thiết lập, hãy chạy:
npx nx connect-to-nx-cloud
Lệnh này sẽ thêm một accessToken vào tệp nx.json của bạn. Giờ đây, mọi artifact của bản build đều được lưu trữ trên đám mây. Nó giống như một thư mục dist dùng chung luôn được đồng bộ hoàn hảo trong toàn công ty.
Quản lý dự án đa Framework
Các monorepo lớn thường gặp khó khăn với các yêu cầu build khác nhau. Bạn có thể có một backend NestJS sử dụng SWC và một frontend React sử dụng Vite. Nx xử lý việc này thông qua Executors.
Bạn xác định cách mỗi dự án sẽ hoạt động trong tệp project.json:
{
"name": "api-gateway",
"targets": {
"build": {
"executor": "@nx/js:swc",
"outputs": ["{options.outputPath}"],
"options": {
"outputPath": "dist/apps/api-gateway",
"main": "apps/api-gateway/src/main.ts"
}
}
}
}
Bằng cách trừu tượng hóa logic build, Nx đảm bảo cơ chế caching vẫn nhất quán. Cho dù bạn sử dụng Webpack, Rollup hay Vitest, logic vẫn giữ nguyên: nếu đầu vào không thay đổi, hãy lấy đầu ra từ cache.
CI nâng cao: Thực thi tác vụ phân tán (DTE)
Để đạt được hiệu suất cao nhất, CI của bạn cần phải “hiểu biết về đồ thị”. Một sai lầm phổ biến là chạy các tác vụ trong một khối tuần tự duy nhất. Thay vào đó, hãy sử dụng Distributed Task Execution (DTE).
DTE điều phối nhiều agent CI. Thay vì một máy phải vật lộn with 10 bản build, Nx Cloud sẽ bảo Agent A build Lib 1, bảo Agent B build Lib 2. Agent C sẽ bắt đầu App 1 ngay khi các phụ thuộc của nó đã sẵn sàng. Đây là sự điều phối thông minh, không chỉ đơn giản là chạy song song.
Ví dụ GitHub Action
jobs:
main:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: nrwl/nx-set-shas@v4
- run: npm ci
- run: npx nx-cloud start-agent
- run: npx nx affected -t build lint --parallel=3
Kết quả: Từ 45 phút xuống còn 4 phút
Sau khi chúng tôi thắt chặt ranh giới dự án và bật remote caching, kết quả khác biệt một trời một vực. Cùng một bản hotfix lúc 2 giờ sáng từng mất 45 phút thì nay chỉ mất chưa đầy 5 phút để deploy. Phần lớn thời gian đó chỉ là chi phí vận hành của GitHub Actions khi khởi tạo máy ảo.
Tác động đến tinh thần của đội ngũ cũng quan trọng không kém. Khi việc build diễn ra nhanh chóng, các lập trình viên sẽ commit những mẩu code nhỏ hơn và chạy test thường xuyên hơn. Nỗi khiếp sợ về một “CI bị hỏng” hoàn toàn biến mất.
Các thực hành tốt nhất để thành công
- Thư viện nhỏ gọn (Granular Libraries): Thư viện càng nhỏ thì lệnh
nx affectedcàng hiệu quả. Đừng tạo một thư mục “utils” chung chung; hãy tạo một thư việnvalidation-utilsriêng biệt. - Thực thi ranh giới (Enforce Boundaries): Sử dụng các tag của Nx để ngăn chặn phụ thuộc vòng. Nếu thư viện UI của bạn bắt đầu import các model database, đồ thị build của bạn sẽ trở thành một nút thắt cổ chai.
- Tự động hóa Cache: Sử dụng nhà cung cấp remote cache như Nx Cloud hoặc S3 bucket. Cache chỉ ở cục bộ mới chỉ là một nửa giải pháp.
- Kiểm tra đồ thị định kỳ: Chạy
nx graphhàng tháng. Tìm kiếm những “thư viện vạn năng” (god-libraries) mà mọi dự án đều phụ thuộc vào. Nếu bạn chạm vào một thư viện mà 50 ứng dụng sử dụng, bạn vừa kích hoạt 50 bản build đấy.
Mở rộng một monorepo TypeScript là nỗ lực không ngừng để giữ cho các phụ thuộc luôn sạch sẽ. Tuy nhiên, với sự hiểu biết vững chắc về đồ thị dự án và chiến lược caching phù hợp, bạn có thể duy trì tốc độ triển khai cao ngay cả khi codebase của bạn phát triển lên đến hàng triệu dòng mã.

