The brief was small on paper: build a paywall screen from a design, let the user switch between an annual and a monthly plan, and confirm the selection. Functionally, that's a few days of work for anyone.
The stated context — a mobile app WebView, not a browser tab — changes what "done" means. A WebView paywall opens cold: no warm HTTP cache from prior visits, no browser chrome giving the user something to look at while JS boots. Every millisecond before first paint is a millisecond of visible dead screen.
Vercel Speed Insights from the deployed demo — mobile, production traffic, last 7 days. This is field data from real devices, not a single synthetic Lighthouse run.
┌─────────────────────────────────────┐
│ HTML response (no JS yet) │
│ <link> critical.css — blocking │
└──────────────┬──────────────────────┘
▼
┌─────────────────────────────────────┐
│ First paint: skeleton + shimmer │
│ Same grid classes as the real UI │
└──────────────┬──────────────────────┘
▼
┌─────────────────────────────────────┐
│ Vue mounts, fonts swap in │
│ Skeleton → real UI, same layout │
│ → CLS stays at 0 │
└─────────────────────────────────────┘critical.css loads as a blocking <link> in the document head. It owns exactly two things: the shell grid and the skeleton's shimmer placeholders — no visual styling that belongs to the mounted components.
Because the skeleton and the real UI share the same layout classes, swapping one for the other on mount changes nothing about document geometry. That's the entire mechanism behind a CLS of 0 — not a trick, just matching selectors.
The shell grid and shimmer skeleton are the only things allowed in this file, kept deliberately separate from component-level SCSS so the two layers never fight over the same selector.
/* public/critical.css — blocking <link>, ships with the raw HTML */
.paywall-page { display: flex; flex-direction: column; min-height: 100vh; }
.paywall-page__skeleton {
background: linear-gradient(90deg, #1a1a1a 25%, #232323 50%, #1a1a1a 75%);
animation: shimmer 1.4s ease-in-out infinite;
}Fonts are self-hosted for control over loading, and font-display: swap is declared inline in the HTML — not in a component stylesheet that arrives after JS — so it applies from the very first paint.
<!-- index.html — inline, above the fold -->
<style>
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-Regular.woff2') format('woff2');
font-display: swap;
}
</style>The "Subscribe" button never calls alert() directly. It calls a subscribeAdapter, which happens to be an alert in this build.
Swapping the purchase flow for a native bridge or a real API — or running different adapters per A/B test group — means changing one file, not touching a single component.
// subscription/adapters/subscribeAdapter.ts
export interface SubscribeAdapter {
subscribe(planId: string): Promise<void>
}
// dev: alert() · prod: swap for a native bridge or purchase API —
// components call subscribe(), never know which
export const devAdapter: SubscribeAdapter = {
async subscribe(planId) {
alert(`Selected plan: ${planId}`)
},
}Keeps visual styling scoped to Vue SFCs instead of hand-maintaining a second copy of every rule inside the blocking critical file.
Leaves roughly 1s of LCP on the table versus a fully inlined stylesheet — a documented gap, not an oversight.
The skeleton only reserves layout and shows a shimmer placeholder — it does not duplicate copy that the mounted app already owns as its source of truth.
LCP depends on JS finishing (~99% render delay) instead of firing at raw HTML parse time.
Project scope favored shipping the font as delivered over adding a subsetting build step.
Two woff2 files at ~115KB and ~118KB, versus an estimated ~30KB for a subset.
Vercel Speed Insights, mobile, production traffic, last 7 days
Skeleton and mounted UI share layout classes exactly — nothing reflows on hydration
Semantic landmarks, role="radiogroup", full Enter/Space keyboard support
Nobody would have noticed if the adapter pattern or the critical-CSS split had been skipped — the brief only asked for a plan switcher and a working subscribe flow. Building it as if the purchase flow, the font strategy, and the layout shift budget all mattered anyway is what turns one screen into evidence of how every screen would get built.
A fast, stable load means future visual changes won't accidentally skew conversion data through layout shift or slow paint — true for a real paywall, and just as true for the one built to prove it.
FOR FREELANCE / PROJECT CLIENTS
A paywall this disciplined about layout shift and load time is a paywall that protects conversion data instead of quietly corrupting it — the same rigor a real subscription product needs from day one.
FOR FULL-TIME / HIRING TEAMS
A 100 Real Experience Score and a CLS of 0 on a single-screen engagement is a preview of the performance discipline that scales to every screen in a larger product.