Last updated: May 30, 2026 at 7:46 PM UTC
AI-agent ready. Share this article with your coding agent and say: "Read this guide, run the detect commands on my project, and create an implementation plan for the fixes that apply."
Every website we build or optimize uses the same battle-tested stack. It's not experimental — it's the result of optimizing many real-world sites to perfect 100/100 PageSpeed scores on mobile, all with Google Analytics and call tracking fully active.
This article breaks down every tool in our stack and explains why each piece matters for performance.
Next.js 16 (App Router)
Next.js is the foundation. We use the App Router with React Server Components, which fundamentally changes how JavaScript reaches the browser.
Why it matters for speed:
- Server Components render to pure HTML on the server. The browser receives ready-to-display markup with zero JavaScript hydration cost for static sections like headers, footers, and content areas.
- Automatic code-splitting ensures only the JavaScript needed for the current page is loaded. Visit the homepage? You don't download the contact page's form validation library.
- Static Site Generation (SSG) compiles pages into static HTML files at build time. When a user visits, they get an instant response from the CDN edge — no server computation, no database queries.
- Turbopack (the Rust-based bundler) handles compilation, producing optimized bundles with tree-shaking that eliminates dead code paths.
The Server Component advantage:
On a traditional React site, every component ships its JavaScript to the browser for "hydration." On a page with 15 components, that's 15 bundles the browser must download, parse, and execute before the page becomes interactive.
With Server Components, only interactive components (forms, carousels, toggle menus) ship JavaScript. Everything else — headings, paragraphs, images, static layouts — renders as pure HTML. On our sites, this typically means 3-4 client components per page instead of 15+.
Vercel Edge Network
We deploy exclusively on Vercel's edge network. Every page is served from the CDN node closest to the visitor.
Why it matters for speed:
- Time to First Byte (TTFB): Static pages served from edge nodes typically achieve TTFB under 50ms — compared to 200-800ms for traditional server-rendered WordPress sites hitting a MySQL database.
- Global distribution: Vercel has edge nodes on every continent. A visitor in Tokyo gets the same sub-50ms TTFB as a visitor in New York.
- Image optimization at the edge: The
/_next/imageendpoint processes and caches images at the edge, so subsequent requests for the same image dimensions are instant cache hits.
Vercel Blob Storage (Asset CDN)
All heavy assets — images, videos, PDFs, fonts — are hosted on Vercel Blob Storage, a globally distributed CDN. We never commit binary files to the Git repository.
Why it matters for speed:
- Separation of concerns: Code deploys are fast (seconds) because they don't include hundreds of megabytes of images.
- CDN caching: Assets are cached at edge nodes worldwide with long TTLs (1 year). After the first request, every subsequent visitor gets the asset from cache — zero origin server contact.
- Pre-optimized pipeline: For maximum performance, we maintain a separate
S3_OPTbucket with pre-optimized WebP images at target dimensions. These are served with theunoptimizedprop in next/image, bypassing the Lambda processing entirely and eliminating cold-start latency.
// lib/s3.ts
export const S3 = 'https://STORE.public.blob.vercel-storage.com/project/';
export const S3_OPT = 'https://STORE.public.blob.vercel-storage.com/project-opt/';
// Usage — zero Lambda cost:
// <Image src={`${S3_OPT}assets/hero.webp`} width={1200} height={675} sizes="100vw" unoptimized />
SVGs stay on the original
S3path — they don't benefit from WebP conversion andunoptimizedis unnecessary for them.
next/image (Responsive Image Delivery)
Every image on our sites goes through Next.js's <Image> component. This is non-negotiable.
What it does automatically:
- Format conversion: Serves AVIF (30-50% smaller than JPEG) to browsers that support it, falling back to WebP.
- Responsive srcsets: Generates multiple image sizes (640px, 750px, 828px, 1080px, 1200px, 1920px) so the browser downloads only the size it needs. An iPhone SE downloads a 750px image, not a 1920px desktop version.
- Lazy loading: Images below the fold load only when the user scrolls near them, preserving bandwidth for the critical above-fold content.
- Cache headers: Processed images get
minimumCacheTTL: 31536000(1 year) at the CDN edge.
The sizes prop — the most common mistake:
Without a sizes prop, next/image defaults to sizes="100vw" and the browser downloads the largest srcset entry (up to 3840px) even when the image displays at 665px. We always specify exact sizes matching the component's container width:
sizes="(max-width: 768px) 100vw, 640px"
This single prop can save 100-300KB per image.
AVIF + WebP Image Formats
We configure every project to serve AVIF first, with WebP as fallback:
// next.config.ts
images: {
formats: ['image/avif', 'image/webp'],
minimumCacheTTL: 31536000,
qualities: [22, 55, 75],
}
The qualities array is required in Next.js 15+ — any quality value used on an <Image> component must be explicitly allowlisted here. Without it, the image optimizer returns a 400 Bad Request and the image silently fails to load.
Real-world size comparison (hero background image, 750px wide):
| Format | Size |
|---|---|
| JPEG (default) | ~24 KB |
| WebP | ~12 KB |
| AVIF | ~6.6 KB |
AVIF delivers a 72% reduction compared to JPEG at perceptually identical quality. On a 1.6 Mbps throttled mobile connection (which is what PageSpeed simulates), this is the difference between a 120ms image load and a 33ms image load.
Tailwind CSS v4
We use Tailwind CSS for all styling with a utility-first approach.
Why it matters for speed:
- Tree-shaking: Tailwind v4 scans source files and generates CSS only for the classes actually used. A complex site with 720 unique classes produces ~84KB of CSS — but gzipped, that's only ~20KB.
- Inline CSS (
inlineCss: true): We inline the entire CSS bundle into the HTML stream. This sounds counterintuitive (bigger HTML!), but it eliminates a render-blocking round-trip. Without inline CSS, the browser must download an external.cssfile before it can paint anything. On mobile, that extra round-trip costs 200-400ms of blocked rendering. - Design tokens: We define a consistent typography scale and color palette in
@themeblocks, reducing the number of unique CSS rules generated.
Critical rule:
inlineCss: truemust never be disabled without benchmarking. On one project, disabling it to "reduce HTML size" caused LCP to regress from 1.1s → 2.6s. The 83.8KB of inline CSS is a false economy concern — the external CSS file creates a render-blocking round-trip that always hurts LCP more than the larger HTML payload.
| inlineCss: true | inlineCss: false | |
|---|---|---|
| CSS delivery | Inline in HTML <head> (immediate) | External file (blocking round-trip) |
| LCP (measured) | 1.1s ✅ | 2.6s ❌ |
| HTML size | +83.8 KB | Unchanged |
Server-Side Google Analytics (GA4 Measurement Protocol)
This is our biggest single optimization. Traditional gtag.js is 171KB of client-side JavaScript. Even with lazyOnload, it loads during PageSpeed's measurement window and is flagged as "unused JavaScript."
Our replacement: ~1KB total client code.
Instead of loading Google's tracking library in the browser, we send analytics data server-side:
- A lightweight client module (~1KB) captures page views and sends them via
navigator.sendBeaconto our own/api/analyticsendpoint. - The server-side API route forwards events to GA4 via the Measurement Protocol v2 — authenticated with an API secret.
- The browser never contacts
google-analytics.comorgoogletagmanager.com.
What we preserve: All page views, session tracking, and SPA navigation tracking via usePathname().
What we trade: Automatic scroll depth, outbound click tracking, enhanced measurement, and Google Ads remarketing. For most lead generation sites, page views and form submissions are the only metrics that matter.
Font Optimization Pipeline
Fonts are a hidden LCP killer. Every font file preloaded competes with the hero image for bandwidth on PageSpeed's throttled 1.6 Mbps connection.
Our font rules:
- Max 2 font families per project (e.g., one sans-serif body + one display/heading font). On one project, 8 font preloads across 5 families totaled 388KB — consuming 2+ seconds of bandwidth on PSI and causing 4.1s LCP.
- ≤3 font files total, ≤50KB combined. Every KB of font preload is a KB the hero image can't use.
- Variable fonts over static. A variable font serves all weights from a single file. Switching from a static font (4 weight files, 77KB) to a variable alternative (1 file, 33KB) saved 44KB.
- woff2 format only. We convert all TTF files to woff2 using fonttools + brotli — 69% smaller:
pip3 install fonttools brotli --break-system-packages
for f in public/fonts/**/*.ttf; do
python3 -c "
from fontTools.ttLib import TTFont
font = TTFont('$f'); font.flavor = 'woff2'; font.save('${f%.ttf}.woff2')
"
done
- Fonts hosted on Blob CDN, never committed to Git. Referenced via
@font-faceinglobals.csswithfont-display: optional.
| Font budget | Threshold |
|---|---|
| Max font families | 2 |
| Max font files preloaded | 3 |
| Max combined font size | 50 KB |
| Format | woff2 only |
DeferredClientShell Pattern
Components like cookie banners, navigation loaders, and chat widgets have zero effect on the initial visual render — but if imported eagerly in layout.tsx, they add to the critical-path JS bundle and increase FCP.
Our pattern: A single "use client" wrapper component holds all ssr: false dynamic imports:
// components/deferred-client-shell.tsx
"use client";
import dynamic from "next/dynamic";
const NextTopLoader = dynamic(() => import("nextjs-toploader"), { ssr: false });
const CookieConsent = dynamic(
() => import("@/components/cookie-consent").then((m) => m.CookieConsent),
{ ssr: false }
);
export function DeferredClientShell() {
return (
<>
<NextTopLoader color="#dc5a31" height={3} showSpinner={false} />
<CookieConsent />
</>
);
}
Imported once in layout.tsx, this removes those components from the initial JS bundle entirely. Less main-thread work at FCP → faster Speed Index.
content-visibility: auto (TBT Reduction)
Below-fold sections force the browser to run Style & Layout computation for content thousands of pixels off-screen. On complex pages, this can add 2-3 seconds of main-thread work.
We apply content-visibility: auto to every below-fold section wrapper:
/* globals.css */
.content-lazy {
content-visibility: auto;
contain-intrinsic-size: auto 600px;
}
// page.tsx
<Hero /> {/* LCP — NO content-lazy */}
<div className="content-lazy"><Services /></div> {/* below fold ✅ */}
<div className="content-lazy"><Testimonials /></div> {/* below fold ✅ */}
<div className="content-lazy"><Footer /></div> {/* below fold ✅ */}
The browser skips layout computation for .content-lazy sections until they approach the viewport, dramatically reducing Total Blocking Time (TBT) for real users.
PageSpeed caution:
content-visibility: autois a real-user optimization only. PageSpeed's Speed Index is calculated from screenshots during a simulated scroll — deferred sections appear as blank rectangles, causing Speed Index to spike. A real-world 100/100 site collapsed to 72 after addingcontent-visibility: autoto 5 below-fold sections. Use it for real-user performance, but be aware it is fundamentally incompatible with PageSpeed's screenshot-based measurement.
AI-Powered Optimization Workflow
This is what makes our process fundamentally different from other agencies. We pair autonomous AI agents with hands-on human expertise — the agents execute at scale, but the human drives the loop with real-world diagnostics that no automated tool can replicate.
Our tooling:
- Antigravity — Google DeepMind's agentic coding system. It reads our optimization playbook, implements fixes across the entire codebase, runs verification builds, and iterates. What takes a human team days, an agent completes in hours with zero missed regressions.
- Claude Opus 4.6 — Anthropic's reasoning model powering Antigravity's decision-making. It handles the nuanced judgment calls: which preload strategy fits this specific page, whether a
"use client"directive is actually needed, when to usedecoding="auto"vsdecoding="async". These aren't template fixes — they require understanding how 15+ browser behaviors interact. - Chrome DevTools MCP — Our agents connect directly to a real Chrome instance via the Model Context Protocol for automated traces and bottleneck identification.
Why human interaction is essential:
AI agents are powerful executors, but they can't open your production site and see what the browser actually does. The critical diagnostic step is human-driven: we paste specific scripts into Chrome DevTools Console on the live site and feed the raw output back to the agent. This is where the real optimization happens.
For example, our LCP diagnostic script (paste in Console, get instant results):
new Promise(r => {
new PerformanceObserver(l => {
const e = l.getEntries().at(-1);
r({ element: e?.element?.tagName + ' ' + e?.element?.className?.substring(0, 60),
url: e?.url, startTime: Math.round(e?.startTime),
renderTime: Math.round(e?.renderTime), size: e?.size });
}).observe({ type: 'largest-contentful-paint', buffered: true });
setTimeout(() => r('timeout'), 6000);
}).then(console.log)
That output tells the agent exactly which element is slow and why — is it a /_next/image cold Lambda? A font swap updating LCP? A carousel slide the browser shouldn't have picked? The agent then implements the precise fix, but it's the human-provided console data that makes the diagnosis accurate.
The loop:
- Human runs diagnostic scripts on the live site → pastes output to the agent
- Agent reads our optimization playbook (2,800+ lines of battle-tested rules) and interprets the diagnostics
- Agent implements the fix across all affected files, builds, and verifies
- Human re-runs PageSpeed and console scripts → confirms the fix worked or provides new data
- Repeat until target score is achieved
Every rule in our playbook started as a real discovery from this human-agent feedback loop. The agent finds patterns across codebases at scale; the human validates them against real browser behavior. Neither works as well alone — together, they're how we optimize entire portfolios of sites simultaneously.
pnpm (Package Manager)
We use pnpm instead of npm or yarn for strict dependency resolution.
Why it matters:
- Strict mode: pnpm prevents "phantom dependencies" — packages that work by accident because a transitive dependency happens to install them. This catches unused or misconfigured packages early.
- Smaller
node_modules: pnpm uses hard links and a content-addressable store, reducing disk usage by 50-70% compared to npm. - Faster installs: CI/CD builds are 2-3x faster, which matters when we're iterating on performance optimizations and rebuilding frequently.
LCP <Image> Props Reference (Next.js 16)
The LCP image is the single most impactful element on your PageSpeed score. In Next.js 16 with React 19, the correct prop combination is critical — wrong props cause duplicate preloads, delayed decoding, or missing fetchpriority signals.
The exact prop set that achieves 100/100:
<Image
src={lcpImageUrl}
alt="Descriptive alt text"
fill={true} // for full-bleed hero; use width/height for fixed-size images
priority={true} // generates <link rel="preload" fetchpriority="high"> in <head>
fetchPriority="high" // also adds fetchpriority="high" to <img> element itself
decoding="auto" // browser chooses sync-decode for small images → LCP ≈ FCP
quality={55} // explicit; must be in next.config.ts qualities[]
sizes="100vw" // required; prevents downloading 1920px image for 375px screen
className="object-cover"
/>
Why each prop matters:
priority={true}— generates a<link rel="preload">in the HTML<head>so the browser discovers the image before parsing the body.fetchPriority="high"— in Next.js 16,fill={true}+priority={true}generates the preload but skips addingfetchpriorityto the<img>element. This prop adds it explicitly.decoding="auto"— overrides Next.js's defaultdecoding="async", which queues the image decode as a background task. For small hero images (<30KB),autolets the browser decode synchronously so the image appears in the same frame as FCP.quality={N}— in Next.js 16 with React 19, omitting this causes two separate preloads at different quality values (one from SSR, one from React 19'sReactDOM.preload()), splitting bandwidth and doubling the LCP download cost.
LCP image checklist:
| Check | Rule |
|---|---|
priority={true} | Preload link with fetchpriority in <head> |
fetchPriority="high" | fetchpriority attribute on the <img> element |
decoding="auto" | Sync decode for small hero images (<30KB) |
quality={N} | Explicit value present in next.config.ts qualities array |
sizes | Accurate to actual rendered dimensions |
No loading="lazy" | Never lazy-load the hero image |
No will-change: transform | Causes FCP→LCP gap by forcing compositor layer |
| No CSS animation | Chrome measures LCP at animation end, not first paint |
The Full Stack at a Glance
| Layer | Tool | Impact |
|---|---|---|
| Framework | Next.js 16 (App Router) | Server Components = zero hydration for static content |
| Hosting | Vercel Edge | Sub-50ms TTFB globally |
| Assets | Vercel Blob Storage + S3_OPT | CDN-cached, pre-optimized images |
| Images | next/image + AVIF | 72% smaller than JPEG, responsive srcsets |
| LCP Image | Optimized <Image> props (Next.js 16) | priority + fetchPriority + decoding + explicit quality |
| Styling | Tailwind CSS v4 (inlineCss: true) | Inline CSS eliminates render-blocking round-trip |
| Fonts | woff2 + variable fonts + ≤50KB budget | 69% smaller than TTF, max 2 families |
| Analytics | Server-side GA4 | ~1KB client JS (replaces 171KB gtag.js) |
| Client Shell | DeferredClientShell | Cookie banners, loaders deferred from critical path |
| TBT Reduction | content-visibility: auto | Skips layout for off-screen sections (real-user only) |
| AI Agents | Antigravity + Claude Opus 4.6 | Autonomous optimization: diagnose → implement → verify |
| Profiling | Chrome DevTools MCP | AI-powered 30-second performance diagnosis |
| Version Control | GitHub + Vercel CI/CD | Auto-deploy on push, instant rollback |
| Package Manager | pnpm | Strict deps, fast installs |
Every piece is chosen for one reason: it makes the Largest Contentful Paint faster. LCP is the metric that determines your PageSpeed score, your Core Web Vitals pass/fail, and ultimately your search ranking.
Want to See the Results?
Visit our main speed optimization page to see 10 live showcases of sites we've optimized to 100/100 — with full PageSpeed screenshots and Chrome Performance traces.
Or dive into our optimization guides:
- Improve Your PageSpeed Score (65 to 85) — quick fixes for image priorities, sizes, and CLS
- Reduce Unused JavaScript (86 to 93) — dynamic imports, dead dependencies, server-side analytics
- Fix LCP Issues (93 to 98) — advanced preload engineering for near-perfect scores
Want a Perfect 100/100?
Let Us Handle It.
Our team has achieved 100/100 PageSpeed scores on many real-world sites with Google Analytics and CallRail fully active. We'll do the same for yours.