Micro-frontend Learning
Các cách tích hợp khác

So sánh các cách tích hợp

Chọn Module Federation, single-spa, Web Components hay iframe theo boundary, mức độ tin cậy, trải nghiệm và yêu cầu vận hành.

Giả sử bạn cần ghép catalog mới vào shell của shop, trong khi cart vẫn chạy trên stack cũ. Module Federation, single-spa, Web Components và iframe đều có thể giúp giao diện xuất hiện, nhưng mỗi cách giải quyết một phần khác nhau. Nếu chỉ so bằng câu hỏi “cái nào hỗ trợ nhiều framework?”, bạn dễ chọn đúng công cụ tải code nhưng thiếu lifecycle, hoặc chọn đúng boundary UI nhưng không có isolation cần thiết.

Bài này giúp bạn chốt cách tích hợp theo yêu cầu thực tế: phần nào cần deploy độc lập, code có đáng tin cậy không, hai phía cần chia sẻ gì và ai chịu trách nhiệm khi một phần lỗi. Mình ưu tiên cơ chế ít nhất đủ đáp ứng yêu cầu, thay vì đưa cả bốn vào cùng một hệ thống ngay từ đầu.

Phạm vi so sánh

Bài tập trung vào browser-side integration của shop giả định gồm shell, catalog và cart. Module Federation được xét theo mô hình container và shared dependencies của webpack 5; runtime/plugin khác có thể có hành vi khác. single-spa được xét ở vai trò orchestrator, Web Components ở boundary DOM, còn iframe ở boundary document và browsing context. Đây không phải hướng dẫn cấu hình các cơ chế này trong site Next.js hiện tại.

Mục lục

Đừng so các cơ chế như thể chúng cùng một lớp

Bạn có thể hình dung việc ghép frontend như ghép các gian hàng vào một khu chung: cần đưa hàng đến, quyết định gian nào mở, thống nhất cổng giao tiếp và xác định khu vực nào có quyền truy cập riêng. Nhưng trong browser, “vách ngăn giao diện” không mặc nhiên là “vách ngăn bảo mật”. Shadow DOM ngăn nhiều selector CSS đi xuyên boundary; nó không ngăn JavaScript của widget đọc dữ liệu của shell.

Bốn câu hỏi cần tách riêng

Câu hỏiCơ chế phù hợp để xem xétPhần vẫn phải thiết kế
Code ở build khác được tải và dependency được chia sẻ thế nào?Module FederationPublic API, mount/unmount, routing, CSS và fallback
Ứng dụng nào hoạt động theo URL, khi nào render và cleanup?single-spaLoader, dependency policy, state và CSS boundary
Shell giao tiếp với UI đa framework qua API nào?Web ComponentsTải bundle, versioning, routing và policy bảo mật
Có cần document và môi trường JavaScript riêng không?iframeOrigin, sandbox, quyền được cấp và protocol giao tiếp

Module Federation có thể expose một React component, một hàm mount() hoặc một module đăng ký custom element. Nó không bắt buộc bạn chọn một UI contract cụ thể. single-spa nhận lifecycle từ loader rồi điều phối chúng; loader có thể dùng federation hoặc ESM.

Vì vậy, đây không phải bốn lựa chọn luôn loại trừ nhau. Bạn đang chọn cơ chế cho từng lớp, nhưng cũng không cần lắp đủ mọi lớp bằng một thư viện riêng. Shell nhỏ có thể tự quản lý một lifecycle contract mà chưa cần orchestrator.

Import maps nằm ở đâu

Import maps ánh xạ module specifier sang URL cho ESM. Chúng giúp shell resolve @shop/catalog tới một release cụ thể; bản thân map không gọi mount, cô lập CSS hay kiểm tra version range như cấu hình shared dependency của federation.

Native ESM có thể tái sử dụng module khi các import resolve về cùng module identity, nhưng không tự loại bỏ React đã được bundle bên trong từng artifact. Nếu muốn chia sẻ dependency qua URL, build phải externalize đúng dependency và map phải thống nhất graph cần chia sẻ. Một map cũng không tự chứng minh các exports tương thích.

Khi so lựa chọn thực tế, hãy ghi cả tổ hợp: “single-spa + ESM/import maps” rõ nghĩa hơn chỉ ghi “single-spa”. Tương tự, “custom element + Shadow DOM + ESM loader” rõ hơn “Web Components”.

Ma trận so sánh

Các bảng dưới mô tả cơ chế nền tảng, không chấm điểm một implementation cụ thể. “Cần tự thiết kế” không có nghĩa là không làm được; nó cho biết trách nhiệm chưa được công cụ đó giải quyết.

Boundary và khả năng tích hợp

Tiêu chíModule Federationsingle-spaWeb Componentsiframe
Boundary chínhExposed module giữa các buildApplication/parcel và lifecycleCustom element với DOM contractDocument và browsing context riêng
Document và JavaScriptChạy trong document/realm của shell khi tải vào trangCác application thông thường cùng document/realmElement cùng document/realm với shellDocument và realm riêng; quyền truy cập phụ thuộc origin/sandbox
Tải codeRuntime tải container và module/chunkGọi loading function do bạn cung cấpCần loader hoặc script để đăng ký elementĐiều hướng src tới một document
Chia sẻ dependencyCó share scope và policy version theo cấu hìnhKhông tự deduplicate hay chọn versionKhông có registry quản lý dependency dùng chungKhông tự dùng chung module instance với shell
Lifecycle UIModule/adapter phải cung cấpĐiều phối bootstrap, mount, unmountCallbacks như connected/disconnected; cleanup do element viếtLifecycle của document; readiness nghiệp vụ cần protocol
Nhiều frameworkCó thể expose adapter thay vì component cùng frameworkPhù hợp điều phối app với adapter cho từng frameworkDOM contract giúp consumer không phụ thuộc framework bên trongFramework bên trong không cần tích hợp trực tiếp với shell
CSS boundaryKhông tự cô lập CSSKhông tự cô lập CSSShadow DOM nếu dùng; custom element light DOM thì khôngStylesheet không cascade qua hai document
Security boundaryKhôngKhôngKhông, kể cả closed Shadow DOMCó thể tạo boundary với origin và sandbox phù hợp

Chi phí trải nghiệm và vận hành

Tiêu chíModule Federationsingle-spaWeb Componentsiframe
Ghép UI sâuThuận lợi nếu consumer hiểu API component; cùng React tree còn cần runtime tương thíchApplication thường có root riêng; parcels cần adapterProps/properties, slots và events; cần adapter ở consumerLayout, modal và focus xuyên document khó hơn
RoutingShell/remote tự phân quyền routerRoute activation cấp application; router nội bộ xử lý route conKhông tự điều phối URL cấp trangURL của frame riêng; shell phải đồng bộ nếu muốn deep link cấp trang
State dùng chungCó thể truyền service/props; shared library không tự tạo đúng ownershipTruyền qua contract như custom props hoặc utility moduleProperties, methods và DOM eventsThường dùng message protocol hoặc backend; không truyền function trực tiếp bằng structured clone
Theme và accessibilityDùng nền tảng UI chung thuận lợi nếu contract thống nhấtPhải thống nhất giữa các app/rootTheme qua CSS variables/parts; kiểm tra focus và semantics qua Shadow DOMCần title, focus flow, resize và giao tiếp qua boundary
FallbackLoader boundary và UI boundary do shell/remote cung cấpCó status/error handling; UI fallback vẫn cần thiết kếLỗi tải/đăng ký và lỗi bên trong element cần xử lý riêngDùng handshake/timeout; sự kiện load không chứng minh app đã sẵn sàng
Version/rollbackPin remote và dependency policy; không mặc định hot-swap tab cũTheo loader và release config, không do lifecycle quyết địnhVersion artifact và tên tag; global registry không cho redefine cùng tênVersion URL document và protocol; reload frame cần tính đến state
SSRPhụ thuộc runtime, framework và pipeline hỗ trợKhông tự cung cấp pipeline SSR của toàn trangCần chiến lược render/hydration; có tag không nghĩa có nội dung SSRFrame có thể tự SSR, nhưng không hợp nhất HTML đó thành nội dung shell

Đừng đọc hàng “nhiều framework” như lời khuyên dùng nhiều framework. Có thể tích hợp không có nghĩa là ít chi phí: mỗi framework vẫn có runtime, tooling, accessibility conventions và lifecycle cần bảo trì.

Isolation là tiêu chí loại trừ trước tiên

Trước khi cân nhắc bundle size hay developer experience, hãy xác định code có được phép chạy với quyền của shell hay không. Nếu câu trả lời là không, những cơ chế cùng document không trở thành an toàn chỉ vì thêm error boundary hoặc prefix CSS.

Cùng document là cùng vùng tin cậy

Trong cách dùng browser thông thường, code tải bằng federation, application single-spa và custom element có thể truy cập window, sửa DOM và gọi API mà trang được phép gọi. Việc tải JavaScript từ CDN khác origin không chuyển nó sang một sandbox của CDN: code vẫn chạy trong ngữ cảnh trang đã tải nó.

CSS Modules, prefix và Shadow DOM là các cách hạn chế xung đột style. Chúng không bảo vệ token, không ngăn sửa global và không chặn vòng lặp đồng bộ làm nghẽn main thread. React Error Boundary cũng không bắt mọi lỗi event handler, asynchronous task hoặc lỗi mạng của loader.

Đừng dùng UI encapsulation thay security isolation

Nếu code bên thứ ba không được phép đọc dữ liệu hoặc điều khiển DOM của shell, đừng chỉ bọc nó bằng custom element hoặc single-spa parcel. Hãy đánh giá iframe trên origin riêng với quyền tối thiểu, hoặc không nhúng code đó. Code supply chain và quyền promote release vẫn cần được kiểm soát.

iframe cần policy chứ không chỉ một tag

iframe có document và JavaScript realm riêng, nhưng frame cùng origin không bị same-origin policy ngăn truy cập DOM của parent. Cross-origin frame hạn chế truy cập trực tiếp; sandbox bổ sung các hạn chế về script, navigation, form và những khả năng khác theo token được cấp.

Với nội dung không đáng tin cậy, mình ưu tiên origin riêng và chỉ cấp quyền thực sự cần. Tránh ghép allow-scripts với allow-same-origin cho frame cùng origin với parent: nội dung có thể truy cập frame element để gỡ sandbox, làm mất ý nghĩa hạn chế đó. allow-same-origin giữ origin thực của document, không biến một URL cross-origin thành cùng origin với shell.

Message bridge cũng là nơi cấp quyền. Với frame có origin xác định, bên gửi dùng targetOrigin cụ thể; bên nhận kiểm tra event.origin, event.source và schema payload. Origin hợp lệ chưa đủ để cho phép mọi command. Không tạo API kiểu “frame gửi URL bất kỳ, shell đính kèm token rồi fetch hộ”.

Nếu sandbox không cấp allow-same-origin, frame thường có opaque origin và message gửi về mang origin "null". Không coi chuỗi đó là danh tính đáng tin cậy. Bạn cần protocol riêng, kiểm tra source và kiểm soát capability; đừng giải quyết bằng cách bật mọi quyền hoặc truyền bí mật với targetOrigin: '*'.

Ngoài ra, cần kiểm tra CSP frame-src của shell, frame-ancestors hoặc X-Frame-Options của nội dung nhúng, Permissions Policy và chính sách cookie/storage của browser. iframe không đảm bảo một process riêng hay miễn nhiễm vấn đề performance; khả năng phân tách tài nguyên phụ thuộc browser và môi trường chạy.

Chọn theo tình huống

Mình chọn theo thứ tự: mức độ tin cậy → boundary route hay widget → yêu cầu giao tiếp → khả năng release và vận hành. Công cụ chỉ đến sau khi các điều kiện đó đã rõ.

Cùng React và cần ghép sâu trong một trang

Giả sử shell và catalog đều dùng React, catalog cần release riêng, còn shell cần render public component của catalog trong cùng React tree. Module Federation là ứng viên mặc định nếu bundler/runtime được chọn hỗ trợ mô hình này và team sẵn sàng quản lý shared dependencies.

Lợi thế là shell nhận exports JavaScript thay vì phải chuyển mọi tương tác sang DOM events. Nhưng dùng React Context xuyên boundary chỉ hoạt động khi component thực sự nằm trong cùng tree, React/runtime tương thích và hai bên dùng cùng identity của context object. singleton: true không tự biến hai createRoot() riêng thành một tree hay hợp nhất mọi store.

Mình không chọn federation chỉ để chia một frontend nhỏ thành nhiều build. Nếu hai phần vẫn phải đổi và deploy cùng lúc vì import nội bộ của nhau, package/module boundary thường đơn giản hơn. Đọc shared dependencies trước khi quyết định chia sẻ React, router hoặc design system.

Migration nhiều framework theo route

Giả sử /catalog đã chuyển sang React, còn /cart vẫn ở framework cũ. Bạn muốn chuyển từng nhóm route mà không bắt shell trực tiếp hiểu cách khởi tạo và dọn từng framework. single-spa là ứng viên mặc định cho lớp orchestration.

Mỗi application cung cấp lifecycle và vùng DOM riêng; root config quyết định application nào active. Loader có thể là ESM/import maps hoặc federation, tùy artifact và dependency policy. Việc chọn single-spa không đồng nghĩa phải chọn SystemJS.

Mình không dùng application-level orchestration cho từng nút hay mỗi product card. Nếu cùng framework, component composition thường tự nhiên hơn. Trong migration, vẫn cần quy định route prefix, deep-link fallback phía server, CSS scope và cleanup; single-spa không giải quyết những phần đó thay team.

Widget cần dùng trong nhiều stack

Giả sử một vùng catalog cần được nhúng vào shell React, portal Vue và một trang HTML thuần. Custom element với contract nhỏ là ứng viên mặc định, thêm Shadow DOM khi cần boundary style rõ ràng.

Consumer biết properties, methods và events thay vì API nội bộ của framework. Đổi lại, bạn phải thống nhất cách truyền object, thời điểm element đã ready, event có đi qua Shadow DOM hay không, theme và overlay. Tooltip hoặc modal được đưa ra document.body không còn nằm trong boundary style của shadow root.

Mình không bọc component React bằng custom element nếu mọi consumer đều là React và cần chia sẻ Context sâu. Khi đó DOM adapter thêm một lớp chuyển đổi mà không đem lại giá trị tương xứng. Cũng không tách mỗi instance product card thành một remote: nhiều element có thể dùng chung một bundle của vùng catalog.

Ứng dụng bên ngoài hoặc legacy khó sửa

Giả sử bạn phải nhúng công cụ của đối tác, hoặc một ứng dụng cũ chỉ cung cấp URL trang và chưa thể viết lifecycle adapter. iframe là ứng viên mặc định để giữ boundary document, miễn là sản phẩm chấp nhận giới hạn UX và phía cung cấp cho phép embedding.

Cách này tránh để stylesheet và framework của ứng dụng đó trộn trực tiếp vào shell. Nhưng bạn phải giải quyết chiều cao, nested scrolling, focus, modal trong phạm vi frame, auth và protocol khi cần báo “đã hoàn tất”. Cookie bên thứ ba có thể bị chặn hoặc partition; đừng suy ra “mở riêng được” thì “nhúng vào cũng login được”.

Mình không dùng iframe cho luồng catalog/cart cần tương tác dày đặc và trải nghiệm như một trang duy nhất nếu không có yêu cầu isolation. Khi protocol ngày càng nhiều message để mô phỏng component API, hãy xem lại boundary. Với legacy nội bộ đáng tin cậy, iframe có thể là bước tạm thời rồi chuyển sang adapter theo route; kế hoạch đó cần mốc tháo bỏ cụ thể.

Một team hoặc trang phụ thuộc SSR

Nếu một team sở hữu toàn bộ shop và chấp nhận cùng lịch release, mặc định mình chọn frontend modular hoặc build-time composition. Đó là phương án đối chứng, không phải phương án “kém hiện đại”. Nó giảm số release graph, loader và tổ hợp version phải vận hành.

Nếu catalog phụ thuộc SSR/SEO, hãy quyết định cách tạo HTML trước khi quyết định browser loader. Không cơ chế nào trong bảng tự đảm bảo nội dung sản phẩm, metadata và hydration đúng cho toàn trang. Nội dung SSR bên trong iframe không tương đương nội dung sản phẩm được render trong document shell; cũng không nên kết luận tuyệt đối rằng crawler không thể thấy iframe.

Với Next.js hoặc framework có server/client boundary, cần kiểm tra chính xác router, version, runtime và plugin định dùng. Thành công trong demo webpack client-side không chứng minh hỗ trợ React Server Components hoặc SSR. Khi yêu cầu chính là HTML hợp nhất từ server, hãy đánh giá server-side composition và Next.js, SSR và RSC riêng.

Giữ cùng contract khi đổi cách tích hợp

Để so công bằng, giữ nguyên ownership của shop: shell quản lý navigation và trạng thái tích hợp; catalog sở hữu trải nghiệm chọn sản phẩm; cart sở hữu thao tác giỏ và summary. Chọn loader không nên làm catalog được quyền sửa reducer nội bộ của cart.

Luồng thêm sản phẩm vào giỏ

Contract dưới đây là minh họa nghiệp vụ, không phải API có sẵn trong repository:

Public contract minh họa của cart
type AddItemCommand = {
  requestId: string;
  sku: string;
  quantity: number;
};

type CartSummary = {
  revision: number;
  totalQuantity: number;
};

type AddItemResult =
  | { ok: true; summary: CartSummary }
  | { ok: false; code: 'INVALID_ITEM' | 'UNAVAILABLE' };

interface CartApi {
  addItem(command: AddItemCommand): Promise<AddItemResult>;
  getSummary(): Promise<CartSummary>;
  subscribe(listener: (summary: CartSummary) => void): () => void;
}

Catalog chỉ yêu cầu thêm SKU và quantity. Cart xác nhận kết quả, sau đó summary mới được dùng để cập nhật badge; catalog không tự coi một click là thao tác đã thành công. requestId phục vụ đối chiếu request/response và có thể hỗ trợ idempotency nếu phía xử lý triển khai nó, không tự ngăn thao tác trùng.

direction: right
catalog: Catalog
cart: Cart domain
shell: Shell
catalog -> cart: "Yêu cầu addItem"
cart -> catalog: "Kết quả thao tác"
cart -> shell: "Summary đã xác nhận"
CatalogCart domainShell Yêu cầu addItem Kết quả thao tác Summary đã xác nhận
CatalogCart domainShell Yêu cầu addItem Kết quả thao tác Summary đã xác nhận
Luồng thêm sản phẩm vào giỏ

Nếu cart UI chưa mount, cart domain vẫn phải nhận được command theo contract đã chọn. Đừng đăng ký listener xử lý giỏ chỉ bên trong trang /cart rồi để thao tác từ catalog biến mất khi trang đó inactive.

Transport thay đổi nhưng ownership không đổi

Cách tích hợpCách mang contract qua boundaryBẫy cần tránh
Module FederationTải public module hoặc truyền một CartApi qua props/lifecycle APIImport store/reducer nội bộ vì “đều là JavaScript”
single-spaRoot config truyền service qua custom props hoặc loader tải utility moduleCoi custom props là kênh reactive tự cập nhật mọi state
Web ComponentsTruyền property/method qua adapter; dùng event cho yêu cầu hoặc notificationCho rằng CustomEvent tự trả về Promise kết quả; cần callback hoặc response protocol
iframeAdapter chuyển call thành message có request ID, response và summary notificationGửi function qua postMessage, thiếu timeout/validation hoặc chấp nhận mọi sender

TypeScript interface chỉ kiểm tra code trong phạm vi typecheck của bạn. Các release độc lập vẫn cần runtime validation ở boundary phù hợp và contract tests, nhất là payload DOM events hoặc messages. Một frame trả summary hợp lệ về schema cũng không được mặc nhiên có quyền thực hiện mọi thao tác giỏ.

Với iframe, subscribe() trong interface là API của adapter phía shell: adapter nghe messages và trả disposer. Nó không phải truyền nguyên một function vào frame. Với mọi cách, UI teardown cần unsubscribe; unmount UI không có nghĩa là xóa state của cart domain.

Kết hợp cơ chế khi có nhu cầu rõ ràng

Một tổ hợp hợp lý có từng thành phần giải quyết một yêu cầu cụ thể:

Tổ hợpKhi đáng dùngTrách nhiệm cần giữ rõ
single-spa + Module FederationRoute-level lifecycle và artifact loading/sharing đều cần cơ chế riêngsingle-spa sở hữu activation; federation tải module export lifecycle
single-spa + ESM/import mapsApp theo route, artifact là ESM và muốn chọn dependency URL rõ ràngChốt map snapshot và external dependencies; single-spa không kiểm tra compatibility
Web Components + ESM hoặc federationCần DOM contract đa framework cùng artifact deploy riêngMột đường tải/đăng ký mỗi element; version tag không do loader tự giải quyết
Shell + iframe cho một vùng riêngChỉ một khu vực cần document/security boundary khácGiới hạn message protocol và capability; không mở lại toàn bộ quyền của shell

Mình không đặt một single-spa application, một custom element rồi một React root cho cùng vùng UI nếu chỉ cần một adapter đơn giản. Mỗi wrapper phải có lý do và người sở hữu cleanup; nếu không, lỗi “đã unmount nhưng listener còn chạy” sẽ khó truy ngược.

Với các module cùng document, cần chọn một đường resolve rõ ràng cho mỗi entry. Tải cùng implementation qua cả native ESM lẫn một loader khác không đảm bảo cùng module instance. Với custom elements dùng global registry, customElements.define() không cho redefine cùng tên; guard “đã có tag thì bỏ qua” tránh lỗi đăng ký nhưng không nâng cấp implementation cũ.

Deploy độc lập cần nhiều hơn một loader

Build riêng là điều kiện hữu ích, nhưng deploy độc lập còn cần artifact discovery, contract tương thích và khả năng chọn lại một release đã biết. Web Components không tự cung cấp release registry; single-spa không tự canary; federation không tự bảo đảm shell cũ dùng được remote mới.

Một policy tối thiểu nên trả lời được các câu hỏi sau:

Câu hỏiQuyết định cần ghi
Shell lấy release ở đâu?Remote config, import map, URL bundle đăng ký element hoặc URL document frame
Tổ hợp nào được phép chạy?Shell version, remote release, dependency snapshot và protocol/contract version
Promote khi nào?Upload đủ entry, chunk, CSS và asset trước; validate tổ hợp rồi mới đổi con trỏ
Rollback tác động tới ai?Document mới, tab cũ hay frame được reload; cách giữ state đang thao tác
Ai biết phần nào đã lỗi?Log phase, route, shell/remote release và config revision; fallback có owner

Mặc định mình chọn release snapshot nhất quán cho một document, dùng URL artifact bất biến và giữ artifact cũ đủ lâu cho lazy chunks của tab cũ. Những thứ có thể đổi như HTML hoặc runtime config cần cache policy riêng.

Rollback không đồng nghĩa hot-swap

Đổi con trỏ remote hoặc import map thường tác động tới document mới. Module đã evaluate, dependency đã chọn và element đã đăng ký trong tab cũ không tự bị thay. iframe có thể điều hướng lại document riêng, nhưng thao tác đó có thể mất state và cần handshake lại. Hãy chốt recovery policy thay vì hứa thay release ngay trong mọi tab.

Thiết kế fallback theo phase: tải code/document, mount hoặc readiness, rồi thao tác nghiệp vụ. Timeout chỉ hết thời gian chờ; không mặc nhiên hủy module import hay command đang chạy. Tránh retry một mutation chỉ vì response chậm nếu chưa có policy idempotency. Xem thêm rollout và rollback.

Kiểm chứng lựa chọn bằng một lát cắt shop

Đừng làm bốn demo “Hello world” rồi so số dòng cấu hình. Hãy dùng cùng một lát cắt: mở /catalog, thêm một SKU, thấy badge đổi, đi /cart, quay lại và kiểm tra quantity. Artifact phải được phát hành theo mô hình định dùng; chạy tất cả từ một dev build chưa kiểm chứng deploy độc lập.

Bạn không nhất thiết thử cả bốn. Loại phương án vi phạm yêu cầu bắt buộc trước, rồi so các ứng viên còn lại với frontend modular làm baseline nếu phù hợp.

Các ca thử bắt buộc

Ca thửCách tạo tình huốngBằng chứng cần thấy
Deep link và historyMở URL con trực tiếp; Back/Forward giữa catalog và cartĐúng UI, đúng owner của URL, không rewrite asset lỗi thành HTML
UI chưa mountThêm sản phẩm từ catalog trước lần mở cart đầu tiênCart domain xử lý được; UI sau đó đọc đúng summary
Load/readiness lỗiLàm thiếu entry/chunk hoặc frame không gửi readyFallback cục bộ; shell vẫn điều hướng được; log đúng phase
Điều hướng nhanhLàm loader chậm rồi rời route trước khi hoàn tấtKhông mount kết quả cũ, không cập nhật vùng UI đã bị tháo
CleanupMount/unmount nhiều lần, kiểm tra listeners và subscriptionsKhông nhân đôi thao tác; không có update sau teardown
Release tương thíchPromote catalog mới mà không rebuild shellLuồng chính chạy với shell hiện tại và shell cũ còn được hỗ trợ
Rollback và tab cũGiữ một tab mở trước release, rồi đổi con trỏ về bản trướcTab mới nhận snapshot đúng; tab cũ còn tải được lazy asset
Dependency và CSSKiểm tra runtime thực tải; đưa selector rộng vào remoteKhông có dependency instance ngoài policy; boundary style đúng như cam kết
AccessibilityDùng keyboard đi qua header, vùng nội dung và modalFocus không mất/kẹt; thông báo pending/error đọc được; frame có title phù hợp
Boundary bảo mậtVới message bridge, gửi sai origin/source và payload không hợp lệBị từ chối, không có mutation; capability không vượt contract

Nếu candidate dùng iframe, thêm ca cookie/storage theo browser target, frame đổi URL, resize và child lỗi sau sự kiện load. Nếu candidate dùng custom element, thêm ca mount trước khi bundle đăng ký, disconnect/reconnect và xung đột tên tag giữa release. Nếu dùng federation, thử dependency mismatch chứ không chỉ lỗi tải entry.

Đo performance trên cùng kịch bản

Không có thứ hạng tốc độ cố định giữa bốn cơ chế. Federation có thể giảm dependency trùng khi cấu hình đúng, nhưng cũng có thêm bước entry/chunk loading. Widget đa framework có thể mang nhiều runtime; iframe có document và resource graph riêng. Kết quả phụ thuộc nhiều hơn vào artifact, cache, preloading và cách chia boundary.

Giữ cùng dữ liệu, môi trường mạng/CPU, chức năng và điều kiện cache. Đo cold load và warm navigation riêng, ghi ít nhất:

  • JavaScript/CSS bytes thực tải, gồm dependency bị bundle lặp hoặc fallback copy.
  • Waterfall từ shell tới entry/document, chunk và request dữ liệu.
  • Thời gian tới UI có thể thao tác, long tasks và độ phản hồi khi thêm vào giỏ.
  • Memory và tài nguyên còn sống sau nhiều vòng mount/unmount.
  • Thời gian và hành vi recovery khi một phần bị chặn mạng hoặc release lỗi.

Với iframe, đừng chỉ đọc metric của parent rồi kết luận child nhẹ: cần quan sát cả document bên trong bằng công cụ phù hợp. Ngưỡng pass/fail nên lấy từ ngân sách của sản phẩm; không đặt một con số minh họa rồi coi đó là benchmark chung.

Ghi quyết định kiến trúc

Sau PoC, ghi một ADR ngắn để team hiểu vì sao chọn phương án đó và khi nào phải xét lại. Ví dụ dưới là quyết định giả định cho shop cùng React, không có code đối tác:

ADR minh họa
Bối cảnh:
  Catalog và cart thuộc hai team, cần release riêng.
  Shell và các remote dùng React; code cùng vùng tin cậy.
  Phạm vi hiện tại là client-side, chưa yêu cầu SSR.

Quyết định:
  Dùng Module Federation cho artifact loading và shared React policy.
  Shell sở hữu router và truyền public cart service qua contract.
  Chưa thêm single-spa, custom elements hay iframe.

Lý do:
  Cần ghép UI cùng React tree, không cần DOM contract đa framework
  hoặc security boundary riêng; shell có thể quản lý lifecycle hiện tại.

Chi phí chấp nhận:
  Quản lý shared versions, CSS scope, contract tests và release snapshot.

Điều kiện xét lại:
  Migration nhiều framework theo nhóm route, thêm code không đáng tin cậy,
  hoặc yêu cầu SSR làm thay đổi pipeline.

Bằng chứng bắt buộc trước production:
  Hoàn tất luồng shop, thử lỗi tải, cleanup, release tương thích và rollback;
  lưu kết quả đo theo ngân sách performance của sản phẩm.

Trước khi chấp nhận ADR, kiểm tra:

  • Có nhu cầu deploy/ownership độc lập thực sự, hoặc lý do khác đủ rõ để không chọn frontend modular.
  • Đã xác định vùng tin cậy và loại isolation cần thiết.
  • Có owner cho loader, routing, lifecycle, CSS, state và fallback.
  • Contract không phụ thuộc implementation store/router nội bộ của domain khác.
  • Browser target, framework/runtime và yêu cầu SSR đã được kiểm chứng trên stack định dùng.
  • Có release snapshot, retention, rollback và recovery cho tab/frame cũ.
  • PoC có bằng chứng cho failure paths, accessibility và performance, không chỉ happy path.

Bước tiếp theo là chọn một ứng viên còn phù hợp, thực hiện lát cắt catalog → cart và lưu kết quả các ca thử vào ADR. Chỉ bổ sung một cơ chế nữa khi có yêu cầu cụ thể mà phương án hiện tại chưa giải quyết.

Đọc tiếp

Tài liệu tham khảo

On this page