PavilionMfe: A 500-Line Micro-Frontend Core That Tracks Side Effects Instead of Proxying window
Teams maintaining admin dashboards with frequent tab switches hit real performance cliffs from full sandbox teardown and CSS specificity wars. PavilionMfe replaces both with lighter primitives—side-effect tracking and zero-specificity scoping—that keep sub-apps framework-agnostic and cut the runtime to ~15 KB, at the cost of requiring Vite and Chrome 88+.
PavilionMfe isolates JavaScript sub-applications by patching globals once and tracking timers, intervals, and event listeners on a module-level stack, then cleaning them up on deactivation. CSS scoping runs through a 130-line PostCSS plugin that wraps selectors in `:where()` to add a namespace without increasing specificity, so library defaults are never overridden. Route conflicts are prevented by intercepting `popstate` listeners and only forwarding events to the sub-app whose route matcher claims the current path.
Sub-apps export a three-function lifecycle contract with zero framework imports, and a built-in LRU keep-alive cache hides DOM nodes instead of destroying instances, preserving form state and scroll position across tab switches. The whole runtime ships as five packages totaling roughly 15 KB, with bottom-layer packages carrying no dependencies.
A single `mfe.json` registry drives route registration, Module Federation remote declarations, and dev port allocation. Per-module logging and a route event bus expose hooks for analytics, loading spinners, and permission guards.
Stack-based side-effect tracking is a deliberate trade-off: it avoids the overhead of per-app window proxies but trusts that sub-app code won't deliberately mutate shared globals beyond the patched APIs.
Choosing `:where()` over Shadow DOM keeps sub-app CSS fully authorable with standard tooling, but it ties the framework to Chrome 88+ and sacrifices the hard boundary that Shadow DOM provides.
The popstate proxy approach treats route matching as a first-class concern of the sandbox rather than the router, which means the sandbox must understand URL patterns—a coupling that simplifies the API but complicates the sandbox's responsibilities.
Requiring only a three-function export from sub-apps makes the framework trivially adoptable across Vue 2, Vue 3, and React, but it also means the framework provides no standard way to share state or coordinate lifecycles beyond the EventBus.