PERF-19: Why We're Moving Public Pages Off Force-Dynamic
PERF-19: Why We're Moving Public Pages Off Force-Dynamic
Release: v0.1.180 · Category: Performance — Build & Deploy
The problem
Every page in the application currently exports dynamic = 'force-dynamic'. In Next.js, this directive tells the framework to skip static generation completely and render the page on the server for every incoming request — even when the page content never changes between requests.
For authenticated, data-heavy pages (dashboards, dispute timelines, inspection reports) this is the right call. For the landing page, pricing page, and auth screens, it is unnecessary overhead.
The practical consequence:
- Higher TTFB. Every visitor to
/,/pricing,/sign-in, and/sign-upwaits for a cold server render before receiving any HTML. - No CDN edge caching. Because the response is always dynamic, CDN nodes cannot cache and serve the pages locally. Every request travels back to origin.
- Wasted compute. Server capacity is consumed rendering identical HTML that could have been generated once at build time.
Why it happened
The home page (src/app/page.tsx) reads the user's auth session on the server to decide whether to show authenticated navigation links. Next.js cannot statically generate a route that reads request-time data, so it marks the entire route as dynamic. The force-dynamic export was most likely propagated to the other public pages for consistency — but those pages have no request-time data dependencies at all.
The fix
1. Remove force-dynamic from public pages
The following files should have their force-dynamic directive removed:
src/app/page.tsx
src/app/pricing/page.tsx
src/app/sign-in/[[...sign-in]]/page.tsx
src/app/sign-up/[[...sign-up]]/page.tsx
With no dynamic export present, Next.js will attempt to statically generate these routes at build time, which is exactly what we want.
2. Move the session check to a client component
The landing page (src/app/page.tsx) fetches the auth session to conditionally render nav links. This fetch is the sole reason the entire page shell is dynamic. The fix is straightforward:
- Extract the conditional nav links into a dedicated
<NavLinks />client component. - Inside
<NavLinks />, use the client-side session hook (e.g.useSession) to determine which links to render. - The page shell — hero text, calls to action, footer — is now fully static and pre-rendered at build time.
- On page load, the static shell is served instantly from the CDN. The client component hydrates and shows the correct nav state once the session is resolved.
This is a standard pattern for mixing static shells with personalised UI elements in Next.js App Router.
3. Add ISR to the pricing page
The pricing page content changes infrequently but does need to stay up to date. Rather than forcing a full server render on every request, Incremental Static Regeneration (ISR) is the right model:
// src/app/pricing/page.tsx
export const revalidate = 3600; // re-generate at most once per hour
Next.js will serve the cached static page from the CDN and silently regenerate it in the background at most once per hour. Pricing updates are reflected within 60 minutes with zero TTFB cost for visitors.
Expected impact
| Page | Before | After |
|---|---|---|
/ (landing) | Server render on every request | Static, CDN-cached, near-zero TTFB |
/pricing | Server render on every request | ISR, CDN-cached, revalidates hourly |
/sign-in | Server render on every request | Static, CDN-cached, near-zero TTFB |
/sign-up | Server render on every request | Static, CDN-cached, near-zero TTFB |
Authenticated and data-driven pages are unaffected — force-dynamic remains appropriate wherever server-side data fetching is genuinely required.
Scope
This change is limited to public, unauthenticated routes. No changes are made to the rendering strategy of protected pages, API routes, or server actions.