Building Rich Experiences That Don’t Punish Real Users: Introducing Birthday-Cake Loading
I’ve been working on a promotional site for a fantasy game project I’m developing. The hero section was meant to feel magical: floating particle embers, subtle ambient voice narration, bell-like hover sounds, and smooth animated transitions between sections. On my desktop with a fast connection, it was exactly what I wanted—immersive and atmospheric.
Then I opened it on my phone over a mediocre 3G signal.
The experience fell apart. Long blank screen, audio starting late and stuttering, particles causing visible jank, battery drain, and the whole page feeling sluggish. Even after applying the usual optimizations—lazy-loading components, reducing particle count, compressing assets—the core problem remained: the rich version was simply too heavy for many real-world conditions.
Removing the effects entirely wasn’t an option; they were central to the feel I was going for. Media queries to hide them on mobile felt like a blunt instrument. What I really needed was a way to serve a lean, instantly usable baseline to everyone, then progressively add the richer layers only when the device and network could genuinely support them—without me having to write custom detection logic for every feature.
That’s when the “birthday cake” metaphor clicked: deliver the solid, edible cake first (the baseline experience that works everywhere), and add the fancy icing, sprinkles, and decorations only when there’s budget for it.
The Core Idea: Capability-Based Tiering
Birthday-Cake Loading (BCL) is a small runtime that:
- Collects best-effort signals (device memory, CPU cores, network type/speed, Save-Data header, prefers-reduced-motion, etc.).
- Derives a conservative tier:
base→lite→rich→ultra. - Exposes feature flags (motion, audio, rich images, smooth scrolling, etc.).
- Provides declarative components to gate content based on those flags or tiers.
The tiering is intentionally defensive: if there’s any doubt, it stays in a lower tier. This avoids the common pitfall of optimistic enhancements that end up hurting users on constrained devices.
How It Works in Practice
Here’s a simplified example from my game site:
import {
CakeProvider,
CakeLayer,
CakeUpgrade,
CakeWatch, // optional jank guard
} from "@shiftbloom-studio/birthday-cake-loading";
function HeroSection() {
return (
<CakeLayer feature="motion" fallback={<StaticHero />}>
<ParticleEmberHero /> {/* Only mounts if motion is allowed */}
</CakeLayer>
);
}
function AmbientAudio() {
return (
<CakeUpgrade
strategy="idle" // wait for idle time
loader={() => import("./AmbientNarration")}
fallback={<SilentVersion />}
>
<FullAudioExperience />
</CakeUpgrade>
);
}
export default function Page() {
return (
<CakeProvider>
<CakeWatch /> {/* Opt-in runtime jank detection */}
<HeroSection />
<AmbientAudio />
{/* rest of the page */}
</CakeProvider>
);
}
With this setup:
- On a low-end phone with Save-Data enabled → static hero, no audio, instant paint.
- On a high-end desktop → particles, narration, smooth upgrades after idle.
Core Web Vitals improved noticeably, and the site finally felt fast across the board.
Next.js Integration
Since the game site uses Next.js App Router, I added server helpers to read Client Hints and bootstrap the tier on the server:
// app/layout.tsx
import { headers } from "next/headers";
import { getServerCakeBootstrapFromHeaders } from "@shiftbloom-studio/birthday-cake-loading/server";
export default function RootLayout({ children }) {
const bootstrap = getServerCakeBootstrapFromHeaders(headers());
return (
<html lang="en">
<body>
<CakeProvider bootstrap={bootstrap}>
{children}
</CakeProvider>
</body>
</html>
);
}
This ensures the initial HTML already reflects the expected tier, avoiding flash of incorrect content.
A Note on the Development Process
I used several AI assistants (GPT, Grok, Gemini) extensively during research and early prototyping. They were invaluable for quickly surveying browser APIs for device signals, comparing tiering strategies, and stress-testing edge cases. The speed of iteration was genuinely higher than working entirely solo. That said, every architectural decision, API shape, and line of production code was mine—I treated the AIs as knowledgeable pair programmers rather than code generators. The result feels like a very human library because it is.
Why Not Just Use Existing Solutions?
I looked at libraries for feature detection, reduced-motion hooks, and lazy loading. None quite offered the full progressive-enhancement loop I needed:
- Declarative gating tied to a unified tier.
- Conservative defaults.
- Server-side bootstrap for Next.js.
- Opt-in runtime jank guard (CakeWatchtower).
BCL fills that gap without pulling in heavy dependencies.
Current State & What’s Next
The library is still young (v0.2.x as of this writing), but the core is stable and already powering my game site. It’s Apache-2.0 licensed, fully typed, and tree-shakeable.
If you’re building anything with rich media—games, portfolios, marketing sites, dashboards with heavy animations—I’d love to hear whether this approach resonates. Issues, PRs, and war stories are all welcome.
- GitHub: https://github.com/shiftbloom-studio/birthday-cake-loading
- npm: https://www.npmjs.com/package/@shiftbloom-studio/birthday-cake-loading
- Live demo in the repo (examples/next-demo)
Thanks for reading. 🎂



Top comments (1)
Small update: I just added a “Watchtower” to Birthday-Cake Loading.
It’s opt-in and basically a runtime safety web: it watches for jank (FPS / long tasks) and, if things get rough, it can** temporarily drop selected heavy layers back** to their fallbacks — then r*ecover once the page stabilizes*. The goal is the same as the library overall: fast baseline first, upgrades only when the device can actually handle them.
Demo if you want to see it in action: birthday-cake-loading-demo.vercel.app