A Deep Dive into Modern Web Performance
1 February 2025 — Francis Agbey
Why Performance Is a Product Problem
Performance isn't a metric to chase after launch — it's a product decision made at every layer of the stack. A 100 ms delay in server response correlates directly with drop-off, especially on mobile networks across sub-Saharan Africa where Agbey Tech serves a significant user base.
"The best performance optimization is the one you don't have to undo." — Internal design principle
The Stack We Optimise
Here's what a typical high-traffic project at Agbey Tech looks like:
- Next.js 15 with App Router and React Server Components
- PostgreSQL via Prisma, co-located with compute on Fly.io
- Redis for query-level caching
- Cloudflare as the global edge layer
- Vercel for preview deployments
Server Components vs Client Components
The golden rule: render on the server unless you need interactivity.
// ✅ Server Component — no JS shipped to the client
export default async function BlogList() {
const posts = await db.post.findMany({ orderBy: { date: "desc" } });
return (
<ul>
{posts.map((p) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
// ⚠️ Only go client when you need state / events
("use client");
export function LikeButton({ postId }: { postId: string }) {
const [liked, setLiked] = useState(false);
return (
<button onClick={() => setLiked((l) => !l)}>{liked ? "♥" : "♡"}</button>
);
}
Caching Strategy
We apply caching in three layers:
- In-memory — React's built-in
cache()for request deduplication - Redis — 60-second TTL on expensive aggregation queries
- CDN edge —
Cache-Control: s-maxage=300, stale-while-revalidate=600on static routes
| Layer | Scope | TTL | Invalidation |
|---|---|---|---|
cache() |
Per request | Auto | N/A |
| Redis | Cross-request | 60–300 s | On mutation |
| CDN edge | Global | 5 min | Purge API |
Route Segment Config
Next.js gives us fine-grained control at the file level:
// app/dashboard/page.tsx
export const revalidate = 60; // ISR — rebuild at most every 60 s
export const dynamic = "force-dynamic"; // opt out when data is always fresh
Image Optimisation
The <Image> component from next/image does a lot of heavy lifting:
- Converts PNGs and JPEGs to WebP / AVIF automatically
- Generates
srcsetfor responsive loading - Lazy-loads below-the-fold images by default
- Prevents Cumulative Layout Shift with explicit
width/height
import Image from "next/image";
<Image
src="/hero.jpg"
alt="Hero image"
width={1280}
height={720}
priority
sizes="(max-width: 768px) 100vw, 50vw"
/>;
Note: Always set
priorityon your LCP image, and never on below-the-fold images.
Measuring What Matters
Core Web Vitals are the production target:
- LCP (Largest Contentful Paint) — target < 2.5 s
- INP (Interaction to Next Paint) — target < 200 ms
- CLS (Cumulative Layout Shift) — target < 0.1
We instrument every deploy with Vercel Speed Insights and compare against a rolling P75 baseline.
Takeaways
- Reach for Server Components first
- Cache aggressively, invalidate precisely
- Optimise images at the framework level, not post-hoc
- Measure real-user data, not just Lighthouse synthetic audits
Have questions? Reach out