Mình bắt đầu bài Polaris Web Components migration bằng một màn hình rất nhỏ: hai khối xác nhận giống nhau, mỗi khối có checkbox, link điều khoản, nút mở modal và lỗi validation. Code build được ngay, nhìn qua cũng ổn. Nhưng khi nhân component lần thứ hai, câu hỏi khó mới xuất hiện: ID có còn duy nhất, nút có mở đúng modal, label có giữ được link và lỗi có tới được người dùng bàn phím hay không?
Đó cũng là chỗ mình thấy migration design system dễ bị hiểu thiếu. Đổi React component thành thẻ s-* chỉ là phần cơ học. Phần quyết định UI có “vỡ” hay không nằm ở parity: thứ bậc thông tin, trạng thái, hành vi và accessibility có còn tương đương sau khi đổi rendering model hay không.
Mục lục [Hiển thị]
- Vì sao build chạy được vẫn chưa gọi là migrate xong?
- Polaris React deprecated ảnh hưởng đúng phần nào?
- Chọn ba màn hình đại diện trước khi đổi hàng loạt
- Lập component parity matrix thay vì search-replace
- Migrate một vertical slice và giữ đường rollback
- Audit keyboard, focus và screen reader sau mỗi lát cắt
- Đo bundle và load time cho đúng phạm vi
- Dùng AI toolkit nhưng review output như code chưa tin cậy
- Checklist rollout để UI không “tới công chuyện”
Vì sao build chạy được vẫn chưa gọi là migrate xong?
TypeScript và build chỉ xác nhận một phần hợp đồng. Chúng bắt được import sai, prop đã biến mất hoặc type không khớp, nhưng không biết một tiêu đề bị dồn sang trái, hai trigger cùng trỏ vào một ID hay câu “Tôi đồng ý với điều khoản” đã mất link. Shopify kể đúng ba lỗi kiểu này trong quá trình chuyển năm Checkout Blocks extension: ID trùng, text alignment thiếu capability và label checkbox cần nội dung phong phú hơn.
Mục tiêu hợp lý không phải pixel-perfect tuyệt đối. Web Components nhận style và branding từ host, nên cố ép mọi pixel của hệ cũ thường dẫn tới CSS hack khó sống lâu. Mình chọn bốn lớp parity để kiểm: nội dung và hierarchy; layout và alignment; state và interaction; accessible name, focus và thông báo lỗi. Chỉ cần một lớp chưa có bằng chứng, hàng đó vẫn là gap, không được đánh dấu hoàn tất.
Cách nhìn này khá giống khoảng cách giữa design canvas và CSS thật: tên thuộc tính giống nhau không bảo đảm hành vi cuối cùng giống nhau. Migration cũng vậy, một mapping 1:1 trên giấy có thể thành 0,8:1 khi chạy trong surface thật.
Polaris React deprecated ảnh hưởng đúng phần nào?
Repository Shopify Polaris React đã được archive ngày 6/1/2026. README nói package deprecated, không còn nhận contribution hay feature request, và khuyến nghị Polaris Web Components cho phát triển mới. Đây là trạng thái bảo trì của thư viện; nó không đồng nghĩa mọi app Shopify bỗng ngừng chạy cùng một ngày.
Mốc 1/10/2026 cũng cần đọc theo đúng phạm vi. developer changelog ngày 11/6/2026 nói Shopify CLI sẽ chặn cập nhật app nếu app còn checkout hoặc customer account UI extension ở API 2025-07 trở xuống. Các version 2025-10 trở đi dùng Web Components mặc định. Engineering case study lại mô tả Checkout Blocks nâng lên 2026-01 và diễn đạt deployment block theo bối cảnh của họ. Hai câu không nên trộn thành “mọi embedded app bị khóa”.
| Phạm vi | Baseline cũ cần chú ý | Hành động có bằng chứng |
|---|---|---|
| Checkout UI extension | 2025-07 là version cuối của React-based UI components | Dùng guide checkout, nâng API được hỗ trợ và test đúng target |
| Customer account UI extension | Version trước 2025-10 cần nhận Web Components khi nâng | Dùng guide customer account, kiểm tra thêm thay đổi metafield/API |
| Embedded Admin app thông thường | Polaris React deprecated là tín hiệu cần lập kế hoạch | Đọc reference theo surface; không tự áp deadline của UI extension |
Chốt ngắn gọn: trước khi sửa code, hãy ghi rõ surface, target và api_version. Nếu ba ô này còn mơ hồ thì checklist phía sau cũng chưa có nền để chạy.
Chọn ba màn hình đại diện trước khi đổi hàng loạt
Mình sẽ không bắt đầu từ component dễ nhất rồi tuyên bố migration ổn. Ba lát cắt đáng chọn là: một form có validation, checkbox và link trong label; một list có filter, trạng thái rỗng và phân trang; một flow có banner, action và modal. Bộ ba này ép migration chạm vào content, layout lẫn interaction thay vì chỉ đổi vài thẻ hiển thị.
Mỗi màn hình cần baseline cố định: viewport desktop và mobile, locale tiếng Việt, theme/branding, dữ liệu normal–empty–error, thứ tự Tab, focus trước và sau modal, ảnh chụp cùng kích thước. Typography tiếng Việt nên thử ở ít nhất hai viewport và mức zoom 200%; cách mình từng kiểm tra tiêu đề tiếng Việt theo viewport vẫn hữu ích ở đây vì chuỗi dài dễ phơi ra alignment và wrapping sai.
Không có dev store trong lần thử này, nên mình dựng một lab checkout đại diện bằng @shopify/ui-extensions và @shopify/ui-extensions-tester 2026.7.0 trên Windows x64, Node 22.22.3, jsdom và Vitest. Lab xác minh cấu trúc/hành vi public API trong DOM giả lập; nó không thay thế preview trong Shopify host, branding thật, one-page checkout, Shop Pay hay screen reader.
Lập component parity matrix thay vì search-replace
Parity matrix của mình không chỉ có hai cột “cũ” và “mới”. Mỗi hàng phải ghi capability, state, gap, owner và bằng chứng test. Nếu component mới không hỗ trợ đúng kiểu nội dung, hãy ghi quyết định sản phẩm hoặc design-system gap; đừng lách qua shadow boundary bằng CSS không được tài liệu hỗ trợ.
| Hàng kiểm | Legacy | Web Component | Bằng chứng cần có | Trạng thái |
|---|---|---|---|---|
| Checkbox điều khoản | Children có text/link | s-checkbox với label slot | Accessible name, link và error | Match trong lab |
| Nút mở modal | Overlay prop/callback | commandFor trỏ ID | Hai instance mở đúng target | Match trong lab |
| Text căn giữa | Prop alignment cũ | Phụ thuộc capability theo surface/version | Screenshot ở dữ liệu thật | Cần kiểm trong host |
| Custom CSS | Có thể từng dùng selector riêng | Host kiểm soát style | Decision log, không xuyên boundary | Blocked nếu thiếu capability |

ID và command wiring dễ vỡ ở component lặp
Polaris Web Components dùng commandFor để nối trigger với modal, sheet hoặc action target. ID viết cứng có vẻ vô hại khi chỉ render một block, rồi tới công chuyện khi cùng block xuất hiện hai lần. Trong lab, mình render hai ConsentPanel, tạo ID bằng useId(), rồi kiểm bốn command mở/đóng đều trỏ tới một trong hai modal tồn tại và hai modal không dùng chung ID.
Sáu test đều xanh: hai panel xuất hiện; ID duy nhất; wiring hợp lệ; label slot giữ link; submit rỗng tạo error; tick checkbox trước submit không tạo error. Đây là kiểm thử hành vi nhỏ nhưng cụ thể hơn câu “migrate xong, build pass”.
Typography và label không chỉ là chuyện thẩm mỹ
Shopify mô tả trường hợp text bị dồn trái như một gap “cosmetic”, nhưng checkout của merchant sẽ trông hỏng thật. Với label, rủi ro còn lớn hơn: thay chuỗi có link thành plain string làm mất đường dẫn hoặc accessible name không còn nói đúng hành động. Reference checkbox hiện hỗ trợ label string và label slot; slot thắng khi cả hai cùng tồn tại, vì vậy matrix phải ghi chính xác mình đang dựa vào cơ chế nào.
Migrate một vertical slice và giữ đường rollback
Shopify Engineering đi từ extension nhỏ, rủi ro thấp rồi mới tới dynamic-content. Với app nhỏ hơn, mình cũng chọn một flow end-to-end: render dữ liệu, nhập, validation, modal, submit và trạng thái thành công. Commit tách phần chuyển API, phần mapping component và phần sửa parity để khi regression xuất hiện còn biết nó đến từ đâu.
Nếu phải chạy song song, feature flag hoặc route preview dễ rollback hơn trộn hai design system trong cùng subtree. Điều kiện rollback nên viết trước: error rate tăng, interaction mất, bundle vượt budget, visual diff ở màn hình quan trọng hoặc focus không quay về trigger. Một “cosmetic gap” trên flow checkout vẫn đủ lý do dừng rollout.
Audit keyboard, focus và screen reader sau mỗi lát cắt
Checklist bàn phím tối thiểu gồm Tab và Shift+Tab theo reading order, Enter/Space kích hoạt đúng control, Escape đóng overlay, focus bị giữ hợp lý khi modal mở và quay lại trigger khi đóng. Phần modal nên đối chiếu với bài focus và hành vi dialog, nhưng nhớ rằng Shopify host mới là nơi quyết định hành vi cuối cùng của component.

Lab jsdom của mình kiểm được DOM, event change/click, error attribute và command target; nó không mô phỏng focus management bên trong host component. Máy có Windows nhưng mình không có dev store/checkout preview được cấu hình cho bài này, nên không chạy NVDA trên surface thật và không tuyên bố WCAG pass. Khi triển khai thật, automated test chỉ đứng trước manual keyboard, Chrome/Edge, mobile viewport và ít nhất một screen reader.
Đo bundle và load time cho đúng phạm vi
Mình tạo hai flow có cùng ý định: bản legacy dùng React UI extensions 2025.7.4, bản mới dùng Preact và s-* 2026.7.0. Cả hai được esbuild minify ESM, bundle dependency, rồi gzip level 9 trên cùng máy. Đây là một lần build xác định kích thước, không phải 20 lần đo runtime.
| Mẫu local | Raw | Gzip | Ghi chú |
|---|---|---|---|
| Legacy React 2025-07 | 116.941 B | 37.229 B | React và reconciler nằm trong bundle tổng hợp |
| Web Components + Preact 2026-07 | 22.047 B | 8.724 B | Flow tương đương, dependency được bundle |
| Chênh lệch | −94.894 B | −28.505 B (−76,6%) | Chỉ đúng với lab này |
Con số này không phải output của Shopify CLI, không đo network transfer từ host và không chứng minh app thật nhanh hơn 76,6%. Nó chỉ cho thấy dependency graph của flow đại diện nhẹ hơn theo cùng bundler. Để ship, hãy build bằng CLI thật, xác nhận giới hạn bundle theo tài liệu version đang dùng, rồi đo request và Extension Load Time trong môi trường có traffic phù hợp.
Case study Checkout Blocks ngày 16/7/2026 báo mức giảm transferred bundle 40,5%–84,4% và ELT P50/P90 tốt hơn trên năm extension của họ. Mình xem đây là bằng chứng sản xuất của Shopify, không mượn làm benchmark cho lab hoặc app của các bạn.
Dùng AI toolkit nhưng review output như code chưa tin cậy
AI Toolkit có thể làm tốt phần lặp: đổi React sang Preact, mapping component, cập nhật API và nhắc các trang migration liên quan. Chính guide của Shopify cũng yêu cầu đọc lại migration guide và chạy extension local để tìm gap trước khi publish. Thế nên prompt tốt nên giới hạn surface, folder và API version, đồng thời yêu cầu xuất danh sách mapping chưa chắc chắn thay vì tự chế workaround.
Mình không cài hay chạy AI Toolkit trong lần này. Việc mình làm là test code đã viết và review public API. Nếu dùng toolkit ở dự án thật, mình sẽ lưu prompt, version, diff, log test và parity matrix; mọi mapping “blocked” phải có người chịu trách nhiệm. Agent có thể cầm tua-vít, nhưng designer và developer vẫn phải nhìn lớp phim before/after.
Một giới hạn khác đáng ghi: bản tester 2026.7.0 mình tải về dùng dynamic import bằng đường dẫn Windows dạng C:\..., khiến Node báo protocol c:. Mình vá tạm sang file URL và thêm cache-busting chỉ trong thư mục lab để chạy lại 6 test. Đây là workaround của lần thử, không phải hướng dẫn sửa dependency production; các bạn nên kiểm tra release/issue mới nhất trước khi áp dụng.
Checklist rollout để UI không “tới công chuyện”
- Ghi đúng surface, target và API version; không áp deadline checkout cho mọi app.
- Chọn ba màn hình đại diện và lưu baseline ở cùng viewport, locale, data state.
- Hoàn tất parity matrix; mỗi gap có owner, quyết định và test.
- Test component lặp, ID/command wiring, rich label, validation và modal.
- Chạy keyboard và screen reader trên Shopify host; automated scanner chỉ bổ sung.
- So bundle bằng cùng tool/mode, gắn nhãn rõ local hay production.
- Rollout theo lát nhỏ, có monitoring, canary và tiêu chí rollback viết trước.
Theo mình, team có checkout hoặc customer account extension ở 2025-07 trở xuống nên audit ngay vì mốc cập nhật đã rõ. Team embedded app dùng Polaris React cũng nên lập inventory và prototype, nhưng đừng tự dọa mình bằng deadline sai surface. Nếu parity matrix còn nhiều ô blocked, dừng ở prototype và đẩy gap lên design-system decision sẽ an toàn hơn một migration “xanh build” nhưng vỡ UI.
Nguồn đã kiểm tra ngày 3/8/2026: Shopify Engineering, “Upgrading Checkout Blocks app to Polaris web components” (16/7/2026); Shopify developer changelog về AI Toolkit (11/6/2026); migration guide cho Checkout và Customer Account UI extensions (tài liệu sống, không ghi ngày trên trang); repository Shopify Polaris React (archive 6/1/2026); và changelog UI Extensions Tester (13/4/2026).