Skip to content

mountly

v0.1 · 9 KB gzipped runtime

Load rich UI only when the user actually needs it.

A small frontend runtime that splits code at the feature level and activates widgets on intent — hover, click, focus, viewport, or idle. No microfrontend orchestrator. No new component model. No host-app rewrite.

~9KBcore gzipped
5trigger types
3frameworks supported
0SPA required
01 · The problem

Modern web apps ship too much JavaScript upfront.

Component libraries load everything at once. Microfrontends are operationally heavy. Framework lazy-loading lacks standardised interaction patterns. There’s no unified system for “load rich UI only when the user actually needs it”.

Before mountly

  • Payment widget
  • Video player
  • Image lightbox
  • Analytics panel
  • Chat widget

All shipped on first paint. Slow TTI, heavy bundle.

After mountly

  • Page shell (light)
  • User hovers → load module
  • → fetch data in parallel
  • → mount into shadow DOM

Fast TTI. Each widget pays only on intent.

02 · Triggers

Six ways to say “now”.

A trigger is the signal that moves a feature from idle to preload to mount. Pick the one that matches user intent — or compose your own with a plugin.

01on intenthoverPreload on mouseenter, mount on click. The default for discoverable controls.
02explicitclickMount on click without preloading. Use when intent is committed, not exploratory.
03accessibilityfocusPreload on focus, mount on commit. Keyboard parity with hover.
04below the foldviewportMount when the element scrolls into view. Configurable threshold.
05predictiveidleUse requestIdleCallback to preload during quiet moments.
06routingurl-changeMount when the URL matches a pattern. Pairs with client-side navigation.
03 · Lifecycle

One state machine, five states. The same shape on every framework.

Every feature moves through the same sequence regardless of trigger or framework. You can hook into any phase, abort in flight, and unmount cleanly.

01idleNothing loaded. Waiting for the trigger.
02preloadModule fetched, data fetch may begin in parallel.
03activateCommitment signal. Finalise data, prepare to mount.
04mountWidget rendered into the container’s shadow root.
05unmountRemoved from DOM. Caches retained for re-mount.
04 · The whole API

A widget. A feature. A trigger. Three lines you’ll write.

Components stay components. The adapter wraps them as widgets. createOnDemandFeature adds the on-demand lifecycle. attach wires it to a DOM element.

signup-card.ts
import { createWidget } from "mountly-react";
import SignupCard from "./SignupCard.tsx";
import styles from "./SignupCard.css?inline";
// 1. Wrap a component as a framework-agnostic widget
export default createWidget(SignupCard, { styles });
page.ts
import { createOnDemandFeature } from "mountly";
// 2. Add the on-demand lifecycle around a widget
const signup = createOnDemandFeature({
moduleId: "signup-card",
loadModule: () => import("./signup-card.js"),
render: ({ mod, container, props }) => mod.mount(container, props),
});
// 3. Attach it to a DOM trigger — preload on hover, mount on click
signup.attach({
trigger: document.querySelector("#cta")!,
preloadOn: "hover",
activateOn: "click",
});
05 · How it compares

Performance benefits of microfrontends. None of the orchestration.

ApproachInitial bundleUser experienceComplexity
Traditional SPALargeSlow first paint, fast afterLow
Framework code-splittingMediumBetter, but route-boundMedium
Microfrontend orchestratorsVariableGood, runtime-coordinatedHigh
mountly~9 KB coreInstant shell, features on intentLow
06 · Packages

One runtime, three adapters, one design preset.

mountly · MIT · pre-1.0github.com/jagreehal/mountlysee ./examples in the repo