All Docs
FeaturesMaking Tax DigitalUpdated March 10, 2026

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

SignalBeforeAfter
TTFBHigh (full SSR, uncached)Low (edge-cached static HTML)
LCPDegraded by TTFB overheadImproved
CLSUnaffectedUnaffected
FID / INPUnaffectedUnaffected
Cache hit rate0% (force-dynamic)High (ISR from CDN)
Server loadEvery request renderedOnly 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