DEV Community

Cover image for Forty Mini-Apps, One Device: Building a Performance Budget Into Your Super App
FinClip Super-App
FinClip Super-App

Posted on

Forty Mini-Apps, One Device: Building a Performance Budget Into Your Super App

Every mini-app team optimizes for their own mini-app. Nobody optimizes for the host. Here's the layer that has to.

If you run a super app long enough, you hit a bug that no profiler points to and no single team owns: the app just gets slower. Cold start creeps up. Memory pressure triggers more background kills. Users start mentioning battery. Nothing broke — the platform just succeeded, and forty mini-apps' worth of individually reasonable decisions added up to a host app that feels heavy.

This isn't a discipline problem you can fix with a style guide. It's a commons problem, and commons problems are solved by governance, not good intentions. Let's build the mental model — and the enforcement — with code.

Why "just optimize your mini-app" doesn't work

Here's the local incentive every mini-app team faces. Keeping state warm makes their return-to-app instant:

// Rational for one mini-app. Catastrophic at 40.
miniApp.onHide(() => {
  // keep the websocket open so resume feels instant
  keepConnectionAlive();
  // keep the in-memory cache so we don't refetch
  retainHeavyState();      // ~30MB, "just in case"
  // refresh in the background so data's fresh on return
  startBackgroundPolling(15_000);
});
Enter fullscreen mode Exit fullscreen mode

Every line is defensible for this mini-app. Now run forty of them. Forty websockets, forty retained heaps, forty background timers — all billed to one shared host envelope that no team sees the whole of. The fix can't live inside the mini-app, because the mini-app is doing exactly what it should for itself. It has to live in the platform.

Step 1: give the host a budget, not a hope

Treat the host's resources as a fixed envelope divided into per-mini-app allocations — declared at the platform level, where mini-apps can't quietly widen them:

{
  "host_envelope": { "memory_mb": 512, "warm_miniapps_max": 3, "cold_start_budget_ms": 1800 },
  "per_miniapp_default": {
    "memory_mb": 64,
    "background": "suspend",
    "max_background_tasks": 0
  },
  "overrides": {
    "miniapp_maps_heavy": { "memory_mb": 128 }
  }
}
Enter fullscreen mode Exit fullscreen mode

background: suspend as the default is the whole philosophy: a mini-app does nothing in the background unless explicitly granted otherwise. The exception (maps needs more memory) is declared and visible, not discovered in a crash log.

Step 2: reclaim idle mini-apps instead of trusting them to behave

When a mini-app goes to the background, the platform — not the mini-app — decides what survives. A lifecycle that suspends and reclaims:

// Platform-level lifecycle manager (not mini-app code)
class MiniAppLifecycle {
  onBackground(app) {
    app.freeze();                 // pause JS execution, no timers fire
    app.closeNonEssentialIO();    // drop sockets the app tried to keep open
    this.lru.touch(app.id);
    this.reclaimIfOverBudget();
  }

  reclaimIfOverBudget() {
    while (this.totalMemory() > this.envelope.memory_mb) {
      const victim = this.lru.evictColdest();   // fully tear down, reclaim heap
      victim?.terminate();
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Note app.freeze() — a suspended mini-app's background timer simply does not fire, no matter how earnestly it called setInterval. The platform overrides the local optimization. That's the point.

Step 3: load on demand, don't warm the world at launch

A tempting way to make mini-apps feel instant is to preload the popular ones at host startup. Do this for a dozen mini-apps and you've traded a fast cold start for a slow one:

// ❌ Cold start now pays for 12 mini-apps nobody opened yet
await Promise.all(popularMiniApps.map(a => a.preload()));

// ✅ Pay only for what the user actually opens; cap warm set
async function openMiniApp(id) {
  if (warmSet.size >= envelope.warm_miniapps_max) {
    await lifecycle.suspendColdest();   // make room within budget
  }
  return runtime.launch(id);            // load on demand
}
Enter fullscreen mode Exit fullscreen mode

Keep at most N mini-apps warm (here, 3). The N+1th open evicts the coldest. Startup stays bounded no matter how many mini-apps the platform hosts.

Step 4: make consumption visible per mini-app

When the app gets heavier, "the app is slow" is useless. You need to know which tenant regressed. Per-mini-app accounting:

runtime.on("sample", (m) => {
  metrics.record(m.appId, {
    rssMb: m.memory,
    cpuPct: m.cpu,
    bgTasks: m.backgroundTasks,
    wakeups: m.timerWakeups,
  });
  // enforce, don't just observe
  if (m.memory > budgetFor(m.appId).memory_mb) {
    lifecycle.throttle(m.appId);   // or warn, or evict
  }
});
Enter fullscreen mode Exit fullscreen mode

The difference between observability and governance is that last if: you don't just chart the overage, you act on it. A mini-app over budget gets throttled, not just graphed.

Putting it together

Per-mini-app budgets, platform-controlled lifecycle, on-demand loading, and enforced per-tenant monitoring — stacked together, these turn host performance from a commons everyone overgrazes into a budget the platform allocates. Here's the shape:

The reason this is a build-vs-buy point: none of these mechanisms can be implemented by the mini-app teams, because their entire job is to constrain those teams from outside. They're properties of the runtime — and teams that hand-roll a super app usually build the runtime that launches mini-apps and defer the layer that contains them, until the app store flags battery drain. A platform built for this ships it as foundation. FinClip, for example, provides per-mini-app resource isolation and monitoring, on-demand loading, and lifecycle reclamation out of the box, so the host envelope is defended by the platform instead of negotiated between teams.

The test

Before you onboard the next batch of mini-apps, check your stack:

  1. Can one mini-app hold memory or run timers in the background indefinitely? (It shouldn't — suspend by default.)
  2. Does cold start time grow as you add mini-apps? (It shouldn't — load on demand, cap the warm set.)
  3. When the app gets heavier, can you name the mini-app responsible? (You'll need to.)
  4. Does anything actually enforce a per-mini-app budget, or do you just monitor and hope?

A "no" on #4 means you have a dashboard, not a budget. Which of these is hardest to answer "yes" to right now? 👇


More on super app architecture, resource governance, and runtime design → https://super-apps.ai/

Top comments (0)