Khi đã có quy ước về code, phải có thứ gì đó giữ cho chúng đúng khi hàng chục người commit mỗi tuần. Đó là việc của hai pillar tiếp theo. Process bao quát con đường từ một ý tưởng tới code đã deploy: story được viết thế nào, build diễn ra ở đâu, component được đóng gói ra sao. Testing bao quát lưới an toàn: unit test cho logic, performance budget cho tốc độ, và visual regression test cho giao diện của từng component.
Đây là những chương mà tên tool đã cũ nhiều nhất. Cấu hình Grunt, Travis CI, PhantomJS và CasperJS chiếm nhiều trang. Mình đọc chúng để lấy lập luận và các quyết định về workflow, và trong đầu thay bằng tool hiện tại trong lúc đọc. Ý tưởng schema-driven trong chương 10 là thứ mình sẽ chỉ cho mọi người đầu tiên; đến giờ nó vẫn ít được dùng.
Ứng với chương 8 đến 14 trong sách.
Ý chính
Viết story cho hệ thống, không phải cho trang
Chương 8 đối chiếu kiểu bàn giao waterfall cũ (requirement, wireframe, bản comp tĩnh, rồi frontend tô lên markup backend đã làm xong) với một workflow mà UX, design, backend và frontend cùng thu thập requirement và lặp trên một prototype chạy trong browser. Chỉ prototype đã được chấp nhận mới chuyển sang development, vì prototype rẻ để thay đổi còn production code thì không. Nếu prototype và sản phẩm dùng chung CSS và JS, QA có thể so sánh kết quả với prototype thay vì với một bản spec cũ.
Phần mình hay quay lại nhất là lời khuyên về cách viết ticket. Một ticket như "cập nhật trang pricing" gộp cả thay đổi toàn cục lẫn cục bộ, không rõ một component dùng chung có nên đổi ở mọi nơi hay không, và dễ dẫn tới hai người sửa cùng một component cho hai trang khác nhau. Cách thay thế của Godbolt là một story nêu tên một thay đổi đối với hệ thống: tạo một variation, opt in, rồi cập nhật trang trong một story riêng. Dù vậy, ông cho thấy một variation nhỏ vẫn có thể được code theo nhiều cách (một class mới, một mixin, một modifier class, một data attribute), và đó là lý do các quy ước được viết ra của Code pillar phải có trước thì workflow mới trơn tru được.
Weak: Update the pricing page (bigger button, new heading colour, tighter cards)
Better: Add a "large" size to Button, opt-in only, default unchanged
Add a "compact" density to PlanCard, opt-in only
Use Button size=large and PlanCard density=compact on /pricing
Build artefact trong CI, phát hành dưới dạng package có version
Sách đi qua quy trình onboarding (provisioning, thiết lập local bằng một lệnh được ghi trong README, feature branch, pull request có review) rồi tới câu hỏi về compiled asset. Commit CSS đã sinh ra nghĩa là mọi lần merge đều conflict ở các file output, và production nhận về bất cứ thứ gì máy của developer cuối cùng tạo ra. Để CI server compile từ source cho ra build giống hệt nhau mọi lần. Với code mà nhiều site cùng dùng, Godbolt khuyên phát hành qua package manager, vì bên dùng nhận được release có version, có thể chọn khi nào nâng cấp, và chỉ nhận những file họ cần.
Chương 9 sau đó đưa ra luận điểm lớn hơn về task runner. Compile, lint, sinh icon, chạy test, build style guide và deploy đều trở thành các bước được script hoá, và tự động hoá là cách architect thực thi process thay vì chỉ mô tả nó. Phần so sánh Grunt với Gulp kết thúc mà không có người thắng: hãy chọn cái nào giúp developer bắt đầu nhanh và ship an toàn.
Schema là contract của component
Chương 10 là ý tưởng độc đáo nhất của phần này. Team frontend của Red Hat làm việc đi trước backend vài sprint, nhưng prototype và template của CMS cứ lệch nhau. Godbolt gọi đây là vấn đề "last mile" (chặng cuối): CSS và JS được dùng chung, nhưng HTML bị viết hai lần, một lần trong prototype và một lần trong CMS. Họ giải quyết bằng cách render cùng một bộ template Twig trong prototype và trong Drupal, điều này biến design system thành thứ ông gọi là design system API, hay theme as a service: các nền tảng gửi dữ liệu vào và nhận lại markup giống hệt nhau. Sau đó mỗi component được ship thành một bộ chuẩn: schema, template, style, visual test, test data, tài liệu và dữ liệu cho tài liệu.
Schema là file mà mọi thứ khác dẫn xuất từ nó, đó là lý do Godbolt gọi cách tiếp cận này là schema-driven, tương tự test-driven development. Viết nó trước buộc phải trả lời những câu hỏi thiết kế mà bản comp bỏ ngỏ: cái gì bắt buộc, cái gì tuỳ chọn, giá trị nào được phép, chuyện gì xảy ra khi có hai item thay vì ba. Cùng schema đó sau đó validate dữ liệu, gợi ý giao diện editor, và cho người viết template biết mọi nhánh họ phải xử lý. Đây là ví dụ của mình cho một component testimonial, dùng draft 2020-12 hiện hành:
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://design.example.com/schemas/testimonial.json",
"title": "Testimonial",
"type": "object",
"required": ["quote", "layout"],
"properties": {
"quote": { "type": "string", "minLength": 20, "maxLength": 280 },
"author": { "type": "string" },
"role": { "type": "string" },
"layout": { "enum": ["stacked", "inline"] },
"avatar": {
"type": "object",
"required": ["src", "alt"],
"properties": {
"src": { "type": "string", "format": "uri" },
"alt": { "type": "string" }
}
}
},
"additionalProperties": false
}
Trade-off là sự dài dòng và một nguồn thông tin type thứ hai bên cạnh prop TypeScript của bạn. Trong một codebase React ngày nay, mình sẽ sinh cái này từ cái kia (ví dụ, định nghĩa schema bằng một thư viện như Zod hoặc TypeBox rồi suy ra type TypeScript), để vẫn chỉ có một single source of truth.
Unit test: đơn vị nhỏ và thời gian được tính trước
Chương 11 coi unit test là nền móng mà mọi thay đổi về sau dựa vào. Lời khuyên là kinh điển: một hàm vừa tra cứu, vừa đo, vừa tính giá trong một lần nên được tách thành ba hàm, mỗi hàm tái sử dụng được và dễ test. Test-driven development viết test thất bại trước rồi làm cho chúng pass.
Thứ mình đánh giá cao hơn là hướng dẫn thực tế về coverage. Với một dự án có sẵn, hãy bắt đầu bằng những test dễ viết mà vẫn hữu ích để tạo đà, rồi nhắm vào những vùng quan trọng nhất hoặc hay có bug nhất. Với một dự án mới, architect đảm bảo mọi story đều có thời gian dành cho test; Red Hat cộng thêm khoảng một phần tư vào ước lượng development, và chỉ bỏ task test khi mọi người đồng ý. Một ví dụ nhỏ hiện tại với Vitest, ở một domain khác với sách:
import { describe, expect, it } from "vitest";
export function columnsForWidth(widthPx: number): 1 | 2 | 3 | 4 {
if (widthPx < 480) return 1;
if (widthPx < 768) return 2;
if (widthPx < 1200) return 3;
return 4;
}
describe("columnsForWidth", () => {
it.each([
[320, 1],
[480, 2],
[767, 2],
[1199, 3],
[1440, 4],
])("width %i gives %i columns", (width, expected) => {
expect(columnsForWidth(width)).toBe(expected);
});
});
Performance budget có hiệu lực thật
Chương 12 coi performance là một loại test như mọi loại khác: đặt mục tiêu, kiểm tra trước mỗi lần merge hoặc deploy, và khi một feature vượt budget thì hoặc sửa nó, hoặc bỏ một thứ khác. Sách đưa ra hai cách đặt con số. Một là nhanh hơn các đối thủ chính khoảng 20 phần trăm, khoảng cách mà sách nói người dùng có thể nhận ra. Hai là vượt mức trung bình của ngành theo HTTP Archive. Các metric chia thành ba nhóm: raw (dung lượng trang, số request), timing (time to first byte, start render, document complete) và điểm hybrid (PageSpeed score, Speed Index). Phần tự động hoá được trình bày là các plugin Grunt bọc PageSpeed và WebPageTest.
Ý tưởng budget là thứ đáng giữ. Một số chi tiết cụ thể đã cũ, và mình nói về chúng ở bên dưới.
Visual regression: một ảnh chuẩn cho mỗi trạng thái
Chương 13 giải thích vì sao bug giao diện cứ quay lại: thay đổi không liên quan của một developer khác rò rỉ vào component của bạn, designer so sánh với các phiên bản mockup khác nhau, và yêu cầu thay đổi đến dưới dạng báo lỗi. Visual regression testing trả lời cả ba bằng cách lưu các screenshot đã duyệt và so sánh mọi build mới với chúng. Sách phân loại tool theo cách tiếp cận: so sánh cả trang, so sánh theo component, test assertion trên CSS, headless so với browser thật, script cho tương tác, và duyệt qua giao diện web hoặc dòng lệnh.
Chương 14 cho thấy cách Red Hat thiết lập và, hữu ích hơn, các quy ước của họ. Test nhắm vào component trong style guide thay vì site thật, vì style guide dùng chung CSS và ổn định. Mỗi thư mục component chứa test và ảnh baseline của riêng nó, nên một pull request thay đổi component cũng mang theo baseline mới. Các suite chạy theo từng component, nên một lỗi chỉ thẳng vào một chỗ. Và một lỗi ở component bạn không đụng tới chính là tín hiệu: hoặc thay đổi của bạn quá toàn cục, hoặc hai component lẽ ra nên dùng chung một style và designer cần xác nhận điều đó.
Đọc lại ở năm 2026
- Task runner. Grunt và Gulp vẫn còn, nhưng một cách thiết lập phổ biến bây giờ là package scripts, một bundler như Vite cho compile và dev server, và một monorepo runner như Turborepo để sắp thứ tự task và cache output của chúng. Luận điểm vẫn đứng; chỉ là tầng thực hiện đã dịch chuyển.
- CI và phân phối. GitHub Actions và các dịch vụ tương tự đảm nhận vai trò của Jenkins và Travis. Compiled asset nằm ngoài git, và design system được ship dưới dạng npm package có version, thường từ một monorepo dùng workspaces. Release note kiểu Changesets giúp bên dùng quyết định khi nào nâng cấp.
- Unit test. QUnit và NodeUnit đã nhường chỗ cho Vitest và Jest ở hầu hết team frontend. Vitest 4, ra mắt tháng 10 năm 2025, đưa Browser Mode lên ổn định, nên component test có thể chạy trong browser thật.
- Performance. Time to first byte vẫn quan trọng, nhưng các metric mà team báo cáo bây giờ là Core Web Vitals. Interaction to Next Paint đã thay First Input Delay làm Core Web Vital vào tháng 3 năm 2024. Lighthouse CI hoặc kiểm tra bundle size trong CI đóng vai trò của các plugin Grunt.
- Visual regression. PhantomJS đã ngừng phát triển năm 2018 và repository đã được archive, nên PhantomCSS và CasperJS không còn là lựa chọn cho việc mới. Playwright có screenshot assertion sẵn với
toHaveScreenshot, Vitest 4 thêmtoMatchScreenshot, và các dịch vụ hosted như Chromatic hoặc Percy bổ sung giao diện duyệt mà sách mô tả. Chạy visual test trên các story của Storybook là phiên bản hiện đại của việc test trên style guide. - Schema. JSON Schema vẫn là contract đúng đắn, hiện ở draft 2020-12. Headless CMS và block editor dựa vào content model có cấu trúc, nên ý tưởng suy ra các field của editor từ schema của component giờ thực tế hơn nhiều so với năm 2016.
Áp dụng vào dự án
- Viết lại ba ticket cấp trang tiếp theo thành các thay đổi component cộng một ticket ghép trang.
- Biến phần thiết lập trong README thành một lệnh duy nhất, và coi bất kỳ bước thêm nào mà người mới cần làm là một bug.
- Giữ build output ngoài git. Build trong CI, và phát hành design system dưới dạng package có version kèm changelog.
- Cho mọi component một schema hoặc contract có type trước template của nó, và liệt kê rõ các variant được phép.
- Mặc định thêm ước lượng cho test vào mọi story; chỉ bỏ đi khi có sự đồng ý rõ ràng.
- Đặt budget cho mỗi route quan trọng (ví dụ LCP, INP và kích thước JavaScript) và cho CI fail khi vượt.
- Lưu visual baseline cạnh từng component và chạy visual test theo từng component ở mọi pull request.
Tự kiểm tra
Vì sao prototype nên dùng chung CSS và JS với production? Để development chỉ còn việc nối dữ liệu vào markup đã được duyệt, và QA có thể so sánh sản phẩm với prototype thay vì với một tài liệu đã cũ.
Commit compiled asset gây ra hai vấn đề gì? Conflict liên tục ở các file được sinh ra, và output production phụ thuộc vào máy của developer nào compile sau cùng.
Vì sao Godbolt gọi cách tiếp cận của mình là schema-driven? Vì JSON schema được viết trước và mọi sản phẩm khác (template, test, tài liệu, giao diện editor) đều theo sau nó, giống như test trong TDD.
Team nên phản ứng thế nào khi một visual test fail ở component họ không đụng tới? Hoặc thay đổi đã lan quá xa và phải sửa, hoặc hai component thật sự nên dùng chung thay đổi đó và designer phải xác nhận.
Tool nào thay thế PhantomCSS cho việc mới? Screenshot assertion trong Playwright hoặc Vitest Browser Mode, thường kết hợp với một dịch vụ review hosted và dùng các story của Storybook làm mục tiêu.
Đọc thêm
- Playwright: visual comparisons: cách
toHaveScreenshottạo và so sánh baseline. - Vitest 4 announcement: Browser Mode ổn định và visual regression testing.
- JSON Schema specification: draft 2020-12 hiện hành và các vocabulary của nó.
- web.dev: Web Vitals: các Core Web Vitals hiện tại và ngưỡng của chúng.