04 — SUBSCRIPTION PAYWALL / 2026

A COLD WEBVIEW,
A WARM FIRST PAINT

A single-page mobile subscription paywall for a mobile gaming client's WebView app, engineered around a real constraint: no warm cache, no browser chrome, every millisecond before first paint fully visible to the user. Blocking critical CSS, a shimmer skeleton, and an architecture built to grow past its ~35 lines of core logic.

ROLE
Frontend Engineer (Freelance)
YEAR
2026
STACK
Vue 3 + TypeScript + Vite
CONSTRAINT
Mobile WebView, cold-start perf
01 — CONTEXT & PROBLEM

A WEBVIEW HAS NO WARM CACHE

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.

The functional requirements were trivial. The performance constraint was the real brief.
02 — VISUAL EVIDENCE

REAL-USER METRICS, NOT A LAB SCORE

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.

Vercel Speed Insights dashboard showing a mobile Real Experience Score of 100, with FCP 0.8s, LCP 1.16s, INP 24ms, CLS 0, and TTFB 0.43s
FCP
0.8s
LCP
1.16s
INP
24ms
CLS
0
TTFB
0.43s
03 — ARCHITECTURE

A SKELETON THAT DOESN'T LIE TO THE USER

┌─────────────────────────────────────┐
│  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 skeleton isn't decoration. It's the same grid, rendered early.
04 — IMPLEMENTATION

THREE KEY DECISIONS

A — CRITICAL CSS

Layout arrives before JavaScript does

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
/* 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;
}
B — FONT LOADING

Text is never invisible while waiting for a font

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
<!-- index.html — inline, above the fold -->
<style>
  @font-face {
    font-family: 'Inter';
    src: url('/fonts/Inter-Regular.woff2') format('woff2');
    font-display: swap;
  }
</style>
C — ADAPTER PATTERN

A small feature with a full product's seams

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
// 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}`)
  },
}
05 — TRADEOFFS

DECISIONS & THEIR COSTS

Separate component SCSS, not merged into critical.css

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.

LCP text rendered by Vue, not baked into the HTML skeleton

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.

Full Inter family shipped, not subset to Cyrillic + Latin

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.

06 — OUTCOMES

WHAT THE NUMBERS SHOW

100
Real Experience Score

Vercel Speed Insights, mobile, production traffic, last 7 days

0
Cumulative Layout Shift

Skeleton and mounted UI share layout classes exactly — nothing reflows on hydration

94
Lighthouse accessibility

Semantic landmarks, role="radiogroup", full Enter/Space keyboard support

07 — REFLECTION

SCOPE SMALL, STANDARDS DON'T

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.

07 — WHY THIS MATTERS

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.

NEXT STEP

LIKE WHAT YOU SEE? LET'S TALK ABOUT YOURS.