jvinhit//lab

Search posts

Type to search across journal entries.

navigate open esc close

Web Performance · Part 21 — Budgets, Automation & Performance Culture at Scale

Giữ hiệu năng không trôi theo thời gian: ngân sách bundle với size-limit, treemap analysis, cổng CI ổn định, bisect hồi quy, scorecard, và văn hóa performance.

Phần 12 đặt budget cho một trang. Phần này nâng lên quy mô tổ chức và thời gian: làm sao giữ hiệu năng không trôi khi hàng chục người merge code mỗi ngày trong nhiều năm. Hiệu năng bền vững không đến từ một đợt tối ưu anh hùng — nó đến từ guardrails tự động chặn hồi quy ngay khi nó sinh ra, cộng văn hóa coi tốc độ là trách nhiệm chung.

Định luật entropy hiệu năng: nếu không có guardrails, mọi codebase chậm dần theo thời gian. Mỗi tính năng thêm vài KB; không ai cố ý làm chậm, nhưng tổng lại thì có.


1. Ba loại budget

① Budget số lượng (quantity)   — kích thước/số request
   vd: JS ≤ 170KB nén, ảnh ≤ 500KB, ≤ 50 request
② Budget chỉ số (milestone)    — thời gian/metric
   vd: LCP p75 ≤ 2.5s, INP p75 ≤ 200ms, TTFB ≤ 600ms
③ Budget điểm (rule-based)     — điểm Lighthouse ≥ 90

Budget tốt phải cụ thể, đo được, và gắn với field data (Part 13). “Làm trang nhanh” không phải budget; “INP p75 < 200ms trên mobile” thì có. Đặt budget hơi chặt hơn ngưỡng “Good” để có biên an toàn.


2. Ngân sách bundle — size-limit

Bundle JS là thứ dễ trôi nhất. Size Limit chặn PR nếu output vượt ngưỡng. Plugin @size-limit/file đo kích thước file bằng Brotli mặc định; đặt gzip: true để đo gzip, hoặc brotli: false để tắt nén. Chỉ preset/plugin có @size-limit/time mới đo thời gian tải và thực thi bằng headless browser.

// .size-limit.cjs — fail CI nếu vượt; cần size-limit + @size-limit/file
module.exports = [
  { path: 'dist/main.*.js', limit: '170 kB', gzip: true },
  { path: 'dist/vendor.*.js', limit: '120 kB', gzip: true },
];
# Sau khi pin dependency trong lockfile; exit code != 0 nếu vượt budget
npm exec -- size-limit
@size-limit/file: theo glob output + Brotli mặc định; bật gzip hoặc tắt nén bằng config
@size-limit/time: tùy chọn, thêm phép đo thời gian bằng headless browser
bundler-native budget/plugin: hữu ích để bắt warning ngay trong build
→ đặt budget cho TỪNG entry/chunk quan trọng và cả tổng route, không chỉ một file

3. Treemap analysis — biết byte đến từ đâu

Khi vượt budget, cần biết cái gì phình. Treemap trực quan hóa bundle theo module.

Rollup/Vite  → cài rollup-plugin-visualizer, thêm visualizer() cuối plugins
Webpack      → tạo stats.json rồi chạy webpack-bundle-analyzer
Framework    → ưu tiên analyzer chính thức/tích hợp của framework nếu có
Tìm trong treemap:
  - thư viện to bất ngờ (moment.js, lodash full, cả icon set)
  - phụ thuộc trùng lặp (nhiều version cùng lib)
  - polyfill không cần cho target hiện đại
  - import nguyên package thay vì tree-shakeable import
→ thay bằng lib nhẹ, import lẻ, hoặc code-split (đẩy ra khỏi entry chính).

Mẹo: trước khi thêm dependency, xem ước lượng trên Bundlephobia/import-cost rồi xác nhận bằng production build của chính app. Tree shaking, export được import, target browser và dependency dùng chung có thể làm con số thực khác xa trang tra cứu.


4. Cổng CI — chặn hồi quy nhiều tầng

Tự động hóa nhiều tầng để bắt hồi quy ở các góc khác nhau:

Pipeline guardrails:
  ① Pre-commit/PR   size-limit → chặn bundle phình
  ② Lighthouse CI   chạy trên build preview → assert LCP/CLS/TBT/score ổn định
  ③ RUM alert       cảnh báo khi field p75 xấu đi sau deploy
  ④ Bundle diff bot  comment PR: "+12KB so với main"
# Lighthouse CI navigation run: TBT là proxy lab; INP phải đến từ interaction/RUM
# lighthouserc.yml
ci:
  collect:
    staticDistDir: './dist'
    numberOfRuns: 5
  assert:
    assertions:
      largest-contentful-paint:
        - error
        - { maxNumericValue: 2500, aggregationMethod: median }
      total-blocking-time:
        - warn
        - { maxNumericValue: 300, aggregationMethod: median }
      cumulative-layout-shift:
        - error
        - { maxNumericValue: 0.1, aggregationMethod: median }

Quan trọng: Lighthouse navigation run không có tương tác thật nên không có audit INP để assert; dùng TBT như tín hiệu tải trang, thêm user flow script cho interaction quan trọng, và dùng RUM cho INP field. CI có variance: chạy nhiều lần trên runner ổn định, chọn aggregation rõ ràng, và ưu tiên hard gate metric/byte ít nhiễu hơn một điểm performance tổng hợp.


5. Bisect hồi quy — tìm commit làm chậm

Khi field data cho thấy hiệu năng xấu đi sau một khoảng deploy, cần tìm commit nào. Quy trình:

1. Khoanh vùng theo thời gian deploy (RUM cho thấy bước nhảy ngày nào)
2. So bundle diff giữa hai build (size report theo commit)
3. git bisect trên metric đo được (script đo tự động pass/fail)
4. Xác nhận bằng đo lại lab + field sau khi revert/sửa

Lưu lịch sử bundle size theo commit (artifact CI) giúp bisect nhanh: thường một bước nhảy size trùng với commit thêm dependency nặng.


6. Scorecard & văn hóa hiệu năng

Guardrails kỹ thuật chỉ bền khi có văn hóa đỡ. Cách nhúng hiệu năng vào quy trình:

- Performance scorecard: dashboard p75 LCP/INP/CLS theo route, công khai cho cả team
- Đưa metric vào Definition of Done: "không hồi quy CWV" là tiêu chí merge
- Owner cho từng metric/route → có người chịu trách nhiệm, không "của chung = của không ai"
- Perf review cho thay đổi lớn (thêm dep, đổi kiến trúc render)
- Ăn mừng cải thiện (giảm INP cũng đáng kể như thêm tính năng)
- Đào tạo: chia sẻ kỹ thuật (chính series này) cho cả team

Nguyên tắc: làm cho việc đúng trở thành việc dễ. Nếu budget tự động chặn hồi quy và dashboard hiển thị rõ, dev không cần “nhớ” tối ưu — hệ thống nhắc họ. Đó là khác biệt giữa hiệu năng như nỗ lực cá nhân và hiệu năng như thuộc tính hệ thống.


7. Tóm tắt

  • Không có guardrails, hiệu năng trôi dần theo entropy; cần ngân sách + tự động hóa, không chỉ một đợt tối ưu.
  • Ba loại budget: số lượng (KB/request), chỉ số (LCP/INP/CLS/TTFB), điểm (Lighthouse); đặt cụ thể, đo được, gắn field data, chặt hơn ngưỡng “Good” một biên.
  • size-limit chặn bundle phình trong CI; chọn plugin file/time đúng mục tiêu. Treemap cho biết byte đến từ đâu; xác nhận “cost” bằng production build thật.
  • Cổng CI nhiều tầng: size-limit + Lighthouse CI nhiều run (LCP/CLS/TBT, không giả lập INP bằng audit không tồn tại) + user-flow test + RUM alert + bundle diff bot.
  • Bisect hồi quy bằng lịch sử bundle size + git bisect trên metric khi field data xấu đi.
  • Bền vững nhờ văn hóa: scorecard công khai, metric trong Definition of Done, owner rõ ràng, perf review — làm việc đúng thành việc dễ.

Phần tiếp theo: Capstone 2 — điều tra hiệu năng end-to-end: một khung quyết định triệu chứng → metric → công cụ → cách sửa, ghép mọi phần chuyên sâu thành quy trình chẩn đoán một app chậm thật.