Every millisecond of load time costs conversions. Google's research pegs mobile conversion rates dropping 20% for every second of delay past 3 seconds. Yet most web projects treat performance as something you optimize after the fact — a post-launch sprint, a GTmetrix embarrassment.
Performance isn't something you retrofit. It's a consequence of architectural decisions made at the start.
The Three Layers
Web performance bottlenecks cluster into three places:
1. Network — What travels over the wire
The most impactful lever most developers ignore: reducing what you send.
- Serve optimized images in modern formats (WebP, AVIF). A JPEG hero image at 400KB becomes 60KB in AVIF.
- Lazy-load below-the-fold content aggressively.
- Split your JavaScript bundle. Users on the homepage don't need checkout logic.
2. Render — What the browser does with it
The shift from client-side rendering to server components (React 18+, Next.js 13+) is the biggest architectural win in years.
Static pages don't need JavaScript to render. Most content sites should be 90%+ static, with islands of interactivity where needed.
The React Server Components model makes this precise: opt in to client-side JS with 'use client', rather than opting out with a build flag.
3. Perceived — What the user experiences
Sometimes the right move isn't making things faster — it's making them feel faster.
Techniques:
- Skeleton loading states — Show structure immediately, fill in data as it arrives
- Optimistic UI — Update the interface before the server confirms
- Streaming — Pipe HTML in chunks rather than waiting for the full page
The first two layers affect real performance. This layer affects perceived performance — which is what determines whether users notice.
The Next.js Stack Decision
For new projects in 2024, the App Router is the clear choice. Here's my current default architecture:
app/
├── layout.tsx ← global shell, fonts, metadata
├── page.tsx ← homepage (static by default)
├── [dynamic]/
│ └── page.tsx ← generateStaticParams for pre-rendering
└── api/
└── route.ts ← edge functions where needed
Key decisions:
- Static generation by default —
generateStaticParamsfor all known slugs - ISR for semi-dynamic content — revalidate: 3600 for content that changes daily
- Server Components for data fetching — no client-side waterfalls
- Edge runtime for global latency — API routes that need to be fast everywhere
The Audit Workflow
Before launching any site, I run through:
- Lighthouse in incognito — Clean baseline, no extensions
- WebPageTest with 3G throttling — How does it feel on a real mobile connection?
- Core Web Vitals in Search Console — Field data, not lab data
- Bundle Analyzer —
ANALYZE=true next build— what's actually in the JS?
Most performance problems are visible before launch if you look. The ones you find in production are the ones you chose not to look for earlier.
Build fast. It respects your users' time.