Lighthouse CI 2026: Performance Budget và Regression Detection cho Mỗi PR
Cách dựng Lighthouse CI với GitHub Actions để chặn PR làm chậm site: cấu hình budgets.json, so sánh baseline, kết hợp Playwright, và khi nào tự host LHCI Server.
Lighthouse CI là bộ công cụ mã nguồn mở của Google giúp chạy Lighthouse tự động trên mỗi pull request, so sánh điểm số hiệu suất với ngưỡng (budget) đã định, và fail build nếu regression vượt ngưỡng. Nhờ đó, các thay đổi làm chậm site bị chặn lại trước khi merge vào main. Ở bản 2026, Lighthouse CI đo trực tiếp Core Web Vitals gồm LCP, INP và CLS theo mô hình lab, kết hợp được với Playwright cho các luồng user thật, và có thể tự host server dashboard để lưu lịch sử theo commit. Trong bài này tôi chia sẻ đúng cấu hình mình từng dùng ở Spotify và ở các dự án tư vấn fintech gần đây.
Lighthouse CI 0.14 (tháng 3/2026) đã chuyển hoàn toàn sang INP làm metric responsiveness thay cho FID, mặc định preset là lighthouse:recommended.
Cấu hình budgets.json nên chia theo route thay vì áp một ngưỡng cho toàn site, vì trang home và trang product detail có budget khác nhau.
Median-of-5 runs cho ra kết quả ổn định hơn best-of-3; noise của LCP trong headless Chrome vẫn ở mức ±150ms nên đặt minScore quá gắt sẽ gây flaky build.
Assertions kiểu warn giúp catch regression sớm mà không block hoàn toàn team, còn error chỉ dành cho ba metric CWV cốt lõi.
LHCI tự host chỉ đáng đầu tư khi team > 20 dev hoặc cần lưu > 90 ngày lịch sử; dưới mức đó, temporary-public-storage là đủ.
Kết hợp Lighthouse CI (lab) với RUM là cách duy nhất bắt được regression mà lab không tái tạo được, ví dụ third-party script chậm ở châu Á.
Lighthouse CI là gì và tại sao mỗi PR cần budget riêng
Lighthouse CI (viết tắt là LHCI) là công cụ dòng lệnh chính thức từ nhóm Chrome, cho phép chạy Lighthouse trong môi trường CI như GitHub Actions, GitLab CI hoặc Jenkins. Nó bọc bên ngoài Chrome Headless, khởi động một static server (hoặc trỏ vào một URL đã deploy), thực thi audit, rồi so sánh kết quả với budget được khai báo trong lighthouserc.js. Điểm khác biệt với việc chạy Lighthouse tay là LHCI tạo ra quy trình chặn: nếu LCP tăng 400ms so với baseline, build fail, và PR không merge được cho tới khi ai đó xử lý.
Ở Spotify, tôi từng dựng dashboard nội bộ theo dõi TTI regression cho Web Player. Cứ mỗi PR mở ra, hệ thống lôi commit hash, chạy Lighthouse 5 lần rồi lấy trung vị, và post kết quả lên GitHub check. Trong 18 tháng vận hành, chúng tôi chặn được khoảng 40 PR làm chậm nghiêm trọng trước khi vào staging. Phần lớn là do dev import nhầm một thư viện chart khổng lồ hoặc để lộ một endpoint chậm. LHCI 0.14 hiện tại về cơ bản đóng gói lại cùng ý tưởng đó, nhưng dùng được ngay mà không cần team infra.
Điểm quan trọng cần hiểu: budget theo route, không phải theo site. Trang product detail của một site travel có thể chấp nhận LCP 2.4s vì có hero image lớn, nhưng trang search results phải dưới 1.8s. Nếu dùng chung một ngưỡng, hoặc bạn quá gắt với trang detail, hoặc quá lỏng với trang list.
Cài đặt Lighthouse CI với GitHub Actions trong 15 phút
Cấu hình tối thiểu cần ba file: lighthouserc.js ở gốc repo, một GitHub Actions workflow, và budgets.json nếu bạn muốn tách config budget riêng. Cá nhân tôi luôn cài LHCI cục bộ trước để test config trước khi push, tránh vòng lặp fail-then-fix trên CI (bài học từ một chiều thứ Sáu ngồi debug workflow đến 8h tối).
# Cài LHCI CLI toàn cục hoặc trong dev deps
npm install --save-dev @lhci/[email protected]
# Test cấu hình trước khi push
npx lhci autorun --collect.url=http://localhost:3000
Tiếp theo, tạo lighthouserc.js ở gốc repo. Đây là cấu hình tôi đang dùng cho một dự án Next.js 15 gần đây:
// lighthouserc.js
module.exports = {
ci: {
collect: {
// Build tĩnh rồi serve, hoặc dùng startServerCommand cho SSR
staticDistDir: './out',
// Lặp 5 lần để lấy median (chuẩn cho môi trường headless)
numberOfRuns: 5,
url: [
'http://localhost:3000/',
'http://localhost:3000/products',
'http://localhost:3000/checkout',
],
settings: {
// Giả lập mobile Moto G4 + slow 4G (mặc định của Lighthouse)
preset: 'desktop', // đổi sang undefined cho mobile
chromeFlags: '--no-sandbox --headless=new',
},
},
assert: {
// Kế thừa preset chuẩn, override từng key ở dưới
preset: 'lighthouse:recommended',
assertions: {
'categories:performance': ['error', { minScore: 0.9 }],
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'interaction-to-next-paint': ['error', { maxNumericValue: 200 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
'total-blocking-time': ['warn', { maxNumericValue: 300 }],
'uses-responsive-images': 'warn',
},
},
upload: {
// temporary-public-storage: miễn phí, URL công khai 7 ngày
target: 'temporary-public-storage',
},
},
};
Cuối cùng, workflow GitHub Actions. File này chạy trên mỗi PR, và comment kết quả vào PR nếu bạn cài lhci-action phiên bản v12:
# .github/workflows/lighthouse.yml
name: Lighthouse CI
on: [pull_request]
jobs:
lhci:
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: 'npm'
- run: npm ci
- run: npm run build
- name: Run Lighthouse CI
run: npx lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}
Token GitHub App là tùy chọn. Nếu bạn cài Lighthouse CI GitHub App, các assertion fail sẽ hiển thị như một status check riêng thay vì chỉ nằm trong log workflow.
Cấu hình budgets.json: ngưỡng nào là hợp lý cho 2026
Có hai cách khai báo giới hạn trong Lighthouse CI: thông qua assertions (như ví dụ ở trên) hoặc thông qua file budgets.json theo chuẩn Performance Budget của Chrome. Với các dự án tôi làm gần đây, tôi thường tách budgets.json ra để dev frontend có thể sửa mà không đụng vào CI config.
Vì sao chọn các con số này? Ngưỡng LCP 2500ms và INP 200ms lấy trực tiếp từ threshold "Good" chính thức của Core Web Vitals trên web.dev; nếu team bạn đạt được cho 75% traffic thật thì đó là site nhanh. Ngưỡng script 170KB dựa trên nghiên cứu của Alex Russell về "mobile perf budget" cập nhật 2025, mà tôi thấy vẫn giữ được relevance trong 2026: một chiếc Android tầm trung ở Nigeria trên 4G lởm cần ít nhất 3 giây để parse và execute mỗi 100KB JavaScript. Với các bài phân tích sâu về từng metric, tôi khuyên đọc trước bài về tối ưu LCP với fetchpriority và Early Hints để hiểu vì sao 2500ms không phải là một con số cào bằng.
Assertions vs Budgets: khác biệt và khi nào dùng cái nào
Đây là câu hỏi tôi được hỏi nhiều nhất khi tư vấn: nếu đã có assertions thì tại sao còn cần budgets.json? Trả lời ngắn: cả hai đều tạo ra assertion trong LHCI, nhưng ngữ nghĩa khác nhau.
Assertions là bất kỳ audit nào của Lighthouse, gồm cả các test binary như uses-http2, uses-text-compression, cũng như các metric có ngưỡng. Chúng tồn tại trong lighthouserc.js và thường quản lý bởi team platform hoặc SRE. Budgets chỉ có ba loại (timing metrics, resource sizes, resource counts) nhưng chúng theo chuẩn khai báo mà cả WebPageTest và SpeedCurve cùng đọc được, và có thể phân theo path pattern. Với budgets, bạn khai một lần và chạy trên nhiều tool.
Chiến lược tôi khuyên: đặt hard threshold (level error) cho ba metric CWV (LCP, INP, CLS) trong assertions, còn resource size budgets thì đặt qua budgets.json ở level warn. Lý do là các metric CWV ảnh hưởng trực tiếp đến ranking Google, còn resource size là chỉ báo sớm nhưng có thể chấp nhận vượt ngắn hạn nếu có lý do (ví dụ thêm một feature quan trọng). Cách phân cấp này giúp CI vừa nghiêm với chất lượng người dùng thấy, vừa không cản trở tốc độ phát triển.
// Trong lighthouserc.js, kết hợp cả hai
assert: {
preset: 'lighthouse:recommended',
budgetsFile: './budgets.json', // load resource budgets
assertions: {
'largest-contentful-paint': ['error', { maxNumericValue: 2500 }],
'interaction-to-next-paint': ['error', { maxNumericValue: 200 }],
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
// Các test binary
'uses-text-compression': 'error',
'uses-http2': 'warn',
'unused-javascript': ['warn', { maxLength: 5 }],
},
}
Chạy Lighthouse CI trên user journey với Playwright
Lighthouse mặc định chỉ audit trạng thái cold-load của một URL. Nó không đo được INP thật khi user tương tác qua nhiều màn hình. Đây là hạn chế lớn với single-page app, nơi metric quan trọng nhất thường xảy ra sau khi user đã click, mở modal, hoặc filter. Từ 2024, Lighthouse hỗ trợ user flows, và cách chạy chúng trong CI là kết hợp với Playwright.
Tôi dùng cách này cho hầu hết dự án tư vấn: Playwright điều khiển browser qua các bước realistic, gọi Lighthouse ở các điểm mốc, thu snapshot performance. Code mẫu:
Chạy kịch bản này trong cùng job LHCI, upload report qua --upload.target=filesystem, rồi post link vào PR comment. Cách này bắt được các regression mà Lighthouse cold-audit bỏ sót: ví dụ hàm handler onClick nặng chỉ chạy lần đầu sau khi user tương tác, hay memory leak qua các trang khiến trang thứ ba chậm hơn trang đầu. Kỹ thuật debug INP sau interaction tôi mô tả kỹ hơn trong bài về Long Animation Frames API cho INP.
LHCI Server tự host: khi nào đáng đầu tư
Tùy chọn upload mặc định (temporary-public-storage) chỉ giữ report 7 ngày trên storage của Google. Nếu team bạn cần lịch sử dài hơn, muốn dashboard nội bộ, hoặc có yêu cầu compliance không cho upload code public, cần dựng LHCI Server.
Server là một app Node.js đơn giản, dùng SQLite hoặc Postgres, đóng gói sẵn Docker image. Cấu hình docker-compose:
Đánh giá thẳng: dưới 20 dev active hoặc dưới 50 PR/tuần, LHCI server không đáng effort duy trì. Team fintech tôi tư vấn gần đây có 8 dev, cuối cùng vẫn quay lại temporary-public-storage sau ba tháng dựng server. Không ai vào dashboard xem trend, ai cũng chỉ nhìn PR check. Ngược lại, ở Spotify chúng tôi có khoảng 60 dev đụng vào Web Player, dashboard là công cụ chính để giải thích trend hàng tháng cho management, và server tự host là bắt buộc.
Xử lý nhiễu và baseline so sánh giữa các PR
Vấn đề đau đầu nhất khi vận hành Lighthouse CI là variance. Cùng một commit, chạy 10 lần, LCP có thể dao động ±150ms, TBT dao động ±80ms, performance score dao động ±3 điểm. Điều này khiến minScore: 0.9 có thể fail ngẫu nhiên trong 10% builds ngay cả khi code không đổi.
Ba kỹ thuật giảm noise thực tế:
numberOfRuns: 5, lấy median. Đây là mặc định LHCI khuyến nghị. Ba runs không đủ để loại outlier; bảy runs mất thêm 40% thời gian CI mà cải thiện marginal. Năm là sweet spot.
Warmup run. Trước khi collect thật, chạy một Lighthouse audit "wasted" để làm nóng Chrome cache và JIT. Với LHCI 0.14, thêm settings.throttlingMethod: 'devtools' giúp giảm noise so với default simulated throttling.
So sánh với baseline commit trên main, không so với ngưỡng tuyệt đối. Đây là mẹo tôi học được ở Spotify: thay vì fail khi LCP > 2500ms, fail khi LCP tăng > 200ms so với main. Cách này chấp nhận rằng site đã sẵn chậm, nhưng chặn được PR làm chậm thêm.
# Trong workflow, checkout main để lấy baseline
- name: Get baseline from main
run: |
git fetch origin main
BASELINE_SHA=$(git rev-parse origin/main)
echo "BASELINE_SHA=$BASELINE_SHA" >> $GITHUB_ENV
# So sánh dùng lhci diff plugin (community)
- run: npx lhci autorun --compare-to=$BASELINE_SHA
TTFB cũng là nguồn variance lớn nếu bạn chạy CI trên GitHub-hosted runner mà backend ở region xa. Trong trường hợp đó nên chuyển sang self-hosted runner cùng region với backend, hoặc mock backend calls. Chi tiết về việc TTFB thay đổi theo cấu hình rendering có ở bài về TTFB, Edge Rendering và Streaming SSR.
Lighthouse CI so với WebPageTest: nên dùng cái nào
Câu hỏi so sánh này xuất hiện trong hầu hết dự án. Ngắn gọn: dùng cả hai, ở các giai đoạn khác nhau. Lighthouse CI cho PR check nhanh; WebPageTest cho phân tích waterfall sâu và simulation với network profile thật.
Tiêu chí
Lighthouse CI
WebPageTest
Thời gian mỗi run
15–40s
60–180s
Chi phí
Miễn phí (self-host)
Free tier giới hạn; Enterprise $/tháng
Phù hợp CI/PR check
Rất tốt
Chậm, dùng sparingly
Waterfall & network analysis
Cơ bản
Chi tiết, filmstrip, request timings
Network profile
Simulated throttling
Packet-level throttling, thiết bị thật
Location testing
Chỉ tại runner
> 40 vị trí toàn cầu
Chrome version
Tự chọn qua Puppeteer
Chrome stable trên hardware thật
Workflow tôi khuyên: LHCI chạy trên mọi PR (fast feedback), WebPageTest chạy nightly hoặc pre-release trên staging. Với những waterfall diff phức tạp giữa hai deploy, cá nhân tôi đang viết một tool CLI nhỏ bằng Rust để lấy hai HAR export từ WebPageTest và in ra diff dạng bảng, chỉ ra vòng đời request nào tăng đáng kể. Bạn có thể tham khảo source Lighthouse CI trên GitHub để hiểu cách nó gọi Lighthouse engine, và tài liệu chính thức Lighthouse trên developer.chrome.com để tra cứu preset và audit list.
Checklist triển khai Lighthouse CI cho team mới
Nếu team bạn chưa từng chạy Lighthouse tự động, thứ tự tôi khuyên như sau:
Cài @lhci/cli local, chạy lhci autorun với preset: 'lighthouse:no-pwa' để lấy baseline hiện tại.
Ghi lại điểm số hiện tại của các trang quan trọng nhất. Đừng đặt ngưỡng cao hơn hiện tại + 5 điểm ngay, vì team sẽ ghét.
Bật workflow trên GitHub với continue-on-error: true tuần đầu tiên. Chỉ observe, chưa block.
Tuần thứ hai, chuyển 3 assertion CWV (LCP, INP, CLS) sang error. Còn lại vẫn warn.
Tuần thứ tư, review với team: có PR nào bị block sai? Điều chỉnh ngưỡng.
Tháng thứ hai, thêm Playwright user-flow audit cho 2–3 luồng critical.
Tháng thứ ba, đánh giá xem cần LHCI Server chưa dựa trên số PR/tuần và nhu cầu dashboard.
Đừng bao giờ bật full budget từ ngày đầu. Ở fintech client gần nhất, tôi mắc đúng lỗi này: bật cả 12 assertion error ngay tuần đầu, team block 8 PR trong 3 ngày và cuối cùng disable toàn bộ workflow. Rollout dần luôn thắng.
Câu hỏi thường gặp
Lighthouse CI có mất phí không?
Không. Lighthouse CI là mã nguồn mở dưới giấy phép Apache 2.0. Tùy chọn temporary-public-storage upload cũng miễn phí (giữ report 7 ngày). Chỉ nếu bạn dùng dịch vụ dashboard bên thứ ba như Foo.software hoặc SpeedCurve mới có chi phí.
Lighthouse CI có chính xác bằng RUM không?
Không hoàn toàn. Lighthouse CI đo trong môi trường lab với network và CPU giả lập, nên bắt được regression về mặt kỹ thuật (bundle to hơn, hình chưa optimize) nhưng không tái tạo được điều kiện thật của user châu Á trên 4G lởm. Kết hợp cả LHCI (lab) và RUM (field) là chiến lược chuẩn 2026.
Nên chạy Lighthouse trên trang production hay preview deploy?
Preview deploy tốt hơn cho PR check vì test được đúng code của branch đó. Production dùng cho monitoring định kỳ (nightly cron) để catch regression từ third-party script hoặc CDN. Đừng chạy LHCI trên localhost nếu app SSR gọi API thật, vì noise TTFB sẽ lấn át tất cả metric khác.
Tại sao điểm Lighthouse CI khác điểm Lighthouse trong DevTools?
Ba lý do chính: (1) DevTools chạy trên máy dev với hardware và network thật của bạn, LHCI chạy trong Docker với throttling giả lập; (2) DevTools có sẵn cache và cookies, LHCI luôn cold-load; (3) DevTools bị ảnh hưởng bởi extension. Để so sánh chính xác, chạy DevTools ở incognito với "Clear storage" trước mỗi audit.
Làm sao chặn PR khi INP tăng nhưng cho phép khi giảm?
Dùng assertMatrix với --compare-to flag của LHCI, hoặc script custom đọc file manifest.json mà lhci collect tạo ra và so sánh với baseline commit. Có plugin community lhci-plugin-diff làm việc này tương đối gọn, đủ dùng cho phần lớn team.
Daniel started in performance work on the SRE side. He spent six years at Spotify on the Web Player team, where he owned the TTI regression budget for the desktop web app and built the internal dashboard that flagged perf regressions per PR before merge. He left in 2023 to join a small consultancy doing performance audits for fintech and travel companies, mostly in the UK and Nigeria.
His subspecialty is server-side rendering tradeoffs: when streaming SSR actually helps, when it makes things worse on flaky 4G, and the real numbers behind React Server Components for content-heavy sites. He's a heavy Playwright user for perf testing, mistrusts most npm dependencies on principle, and is currently writing a small Rust tool to diff WebPageTest waterfalls across deploys. Outside of work he coaches a junior dev meetup in Manchester.
Cách trích xuất Critical CSS above-the-fold và inline vào <head> để cải thiện LCP 300-1200ms. Bao gồm Beasties, Penthouse, Next.js 15, Nuxt 4, Astro và các bẫy phổ biến.
Long Animation Frames API (LoAF) là công cụ mạnh nhất để debug INP trong sản xuất. Hướng dẫn đầy đủ về script attribution, sửa third-party CORS, và gate LoAF trong CI Playwright để giữ INP dưới 200ms tại P75 năm 2026.