SEO-18: Fixing Core Web Vitals — Why force-dynamic Was Hurting Our Homepage
SEO-18: Fixing Core Web Vitals — Why force-dynamic Was Hurting Our Homepage
Version: 1.0.369
Category: SEO Performance
The Problem
Our homepage (src/app/page.tsx) was exported with:
export const dynamic = 'force-dynamic'
This Next.js directive tells the framework to skip all static generation and ISR, forcing a fresh server-side render on every single request — for every visitor, whether authenticated or not.
The entire motivation was a single concern: knowing whether a user is logged in so we can conditionally render either a Get Started or a Dashboard CTA button.
Everything else on the homepage — the hero section, feature descriptions, testimonials, pricing links, legal links — is 100% static content that does not change between requests.
Why This Hurts Core Web Vitals
Core Web Vitals, particularly LCP (Largest Contentful Paint), are directly sensitive to how quickly the browser receives the first bytes of a page response. The chain looks like this:
User requests homepage
→ Next.js invokes server render
→ auth() is called (waits on session/cookie resolution)
→ Full HTML is generated
→ Response is sent
→ Browser can begin painting
Every step in that chain adds latency before the browser can display anything. This is measured as TTFB (Time to First Byte), and a high TTFB is one of the most reliable ways to damage an LCP score.
By contrast, our pricing, privacy, and terms pages already use:
export const revalidate = 3600
This means Next.js generates those pages statically and serves them from a CDN edge cache — TTFB is measured in single-digit milliseconds.
The Fix
Step 1 — Switch to ISR on the homepage
Replace the force-dynamic export with an ISR revalidation window:
// Before
export const dynamic = 'force-dynamic'
// After
export const revalidate = 3600 // re-generate at most once per hour
// or
export const revalidate = 86400 // once per day if content changes rarely
This allows Next.js to serve the homepage from a pre-rendered static snapshot, dramatically reducing TTFB.
Step 2 — Move the auth check out of the critical render path
There are two valid approaches:
Option A — Client component (post-hydration)
Extract the CTA button into a small 'use client' component that checks auth state after the page has already painted:
// components/HomeCTA.tsx
'use client'
import { useSession } from 'next-auth/react'
import Link from 'next/link'
export function HomeCTA() {
const { data: session } = useSession()
return session ? (
<Link href="/dashboard">Dashboard</Link>
) : (
<Link href="/get-started">Get Started</Link>
)
}
The hero and all static content renders instantly from cache. The CTA resolves a fraction of a second later on the client — imperceptible to users and invisible to Core Web Vitals scoring.
Option B — Server-side via cookies() within ISR
Read the session token directly from cookies server-side. Because cookies() is read at request time in Next.js App Router, this works inside an ISR page without forcing a full dynamic render of the entire page:
import { cookies } from 'next/headers'
export const revalidate = 3600
export default async function HomePage() {
const cookieStore = cookies()
const sessionToken = cookieStore.get('next-auth.session-token')
const isLoggedIn = Boolean(sessionToken)
return (
<main>
{/* static hero, features, etc. */}
{isLoggedIn
? <Link href="/dashboard">Dashboard</Link>
: <Link href="/get-started">Get Started</Link>
}
</main>
)
}
Note: Option A (client component) is generally preferred as it keeps the static shell fully cacheable and avoids any server computation per request.
Expected Outcome
| Signal | Before | After |
|---|---|---|
| TTFB | High (full SSR, uncached) | Low (edge-cached static HTML) |
| LCP | Degraded by TTFB overhead | Improved |
| CLS | Unaffected | Unaffected |
| FID / INP | Unaffected | Unaffected |
| Cache hit rate | 0% (force-dynamic) | High (ISR from CDN) |
| Server load | Every request rendered | Only on cache miss / revalidation |
Affected File
src/app/page.tsx
Related
- Pages already using ISR correctly:
src/app/pricing/page.tsx,src/app/privacy/page.tsx,src/app/terms/page.tsx - Control reference: SEO-18