Week 11

App Router

Data Fetching and Mutations

Migrating from React Router

Going to production

Rendering Strategies

Practice

Assignment

Front end Track

The Three Rendering Strategies

Client-Side Rendering (CSR)

The server sends a minimal HTML shell. The browser downloads JavaScript, runs it, fetches data, and renders the UI - all on the client.

A React app with useEffect fetching:

sequenceDiagram
    participant B as Browser
    participant S as Server
    participant A as API
    B->>S: GET /products page
    S->>B: empty HTML + JS bundle
    B->>B: executes JS
    B->>A: fetch data
    A->>B: JSON
    B->>B: renders UI

Trade-offs:

Pros Cons
Simple to build and host (just static files) Slow to first content (blank page while JS loads)
Rich interactivity Poor SEO - crawlers may not run JavaScript
No server needed Data fetching waterfalls

Server-Side Rendering (SSR)

The server renders the full HTML on every request - including the data and sends it to the browser. React then "hydrates" the page so it becomes interactive.

In Next.js: Any async Server Component that opts out of caching (cache: 'no-store') is SSR by default.

sequenceDiagram
    participant B as Browser
    participant S as Server
    participant A as API
    B->>S: GET /products page
    S->>A: fetch data
    A->>S: JSON
    S->>B: full HTML with content
    B->>B: hydrates (attaches JS)

Trade-offs:

Pros Cons
Fast first paint - real HTML arrives immediately Requires a server (more infrastructure)
Great SEO - crawlers see full content Every request hits the server
No client-side data waterfalls Slower time-to-interactive than fully static

Static Site Generation (SSG)

Pages are pre-rendered at build time and served as static HTML. Identical to CSR from an infrastructure standpoint (just files), but the HTML already contains the content.

In Next.js: This is the default for pages that don't opt out of caching. Next.js pre-renders them at build time and caches the result.

sequenceDiagram
    participant S as Server
    participant C as CDN
    participant B as Browser
    Note over S: build time
    S->>C: pre-rendered HTML
    B->>C: GET /products page
    C->>B: cached HTML

Trade-offs:

Pros Cons
Extremely fast (served from CDN edge) Content only updates when you redeploy or revalidate
Great SEO Not suitable for user-specific or frequently-changing data
No server computation per request

Bonus: Incremental Static Regeneration (ISR)

You can also use SSG with a hybrid approach: serve a cached static page, but regenerate it in the background after a time interval.

// Regenerate this page at most once per hour
const data = await fetch('<https://api.example.com/posts>', {
  next: { revalidate: 3600 }
})

When to Use Each

In practice, Next.js lets you mix strategies per route - even per component within a page.

Content type Strategy Example
Marketing homepage, about page, blog posts SSG Fetches at build time, fast and indexable
Product listings with inventory counts SSR or ISR Data changes, needs to be fresh
User dashboard, shopping cart CSR (Client Component) User-specific, no SEO needed
News feed, live scores SSR with cache: 'no-store' Always fresh
Mostly static, refreshes daily ISR (revalidate: 86400) Best of both - fast CDN + auto-refresh

<aside> ⌨️

Hands on: Run npm run build in your project. In the terminal output, find the table showing your routes. Identify which routes are marked ○ (Static) and which are λ (Dynamic). Add { cache: 'no-store' } to a fetch in one of your pages and rebuild - that route should flip from static to dynamic.

</aside>

Pre-rendering Dynamic Routes with generateStaticParams

Dynamic routes like /blog/[slug] are server-rendered by default because Next.js doesn't know all the possible slug values at build time. If the set of values is known (e.g., all blog posts in a CMS), you can export generateStaticParams to pre-render them as static HTML at build time:

// app/blog/[slug]/page.tsx

export async function generateStaticParams() {
  const posts = await fetch('<https://api.example.com/posts>').then(r => r.json())

  return posts.map((post: { slug: string }) => ({
    slug: post.slug,
  }))
}

export default async function BlogPostPage({
  params,
}: {
  params: Promise<{ slug: string }>
}) {
  const { slug } = await params
  const post = await fetch(`https://api.example.com/posts/${slug}`).then(r => r.json())

  return (
		  <article>
					  <h1>{post.title}</h1>
					  <p>{post.body}</p>
			</article>
	)
}

At build time, Next.js calls generateStaticParams, gets the list of slugs, and pre-renders an HTML file for each one. Requests to /blog/my-first-post are served instantly from the CDN - no server computation needed per request.

SEO Implications

Search engines (Google, Bing) crawl pages by requesting the URL and reading the HTML response. If your page is blank HTML waiting for JavaScript to run, crawlers may index nothing.

For public-facing pages that you want to appear in search results, use SSG or SSR. For private, authenticated pages (dashboards, account settings), SEO doesn't matter - but performance still does. SSR gives a faster first paint than CSR even for authenticated pages. CSR is a common choice there because the data is user-specific and can't be cached, but SSR is still an option if first-load performance matters.

Metadata in Next.js

Set page titles and descriptions using the metadata export:

// app/products/page.tsx
import type { Metadata } from 'next'

export const metadata: Metadata = {
  title: 'All Products',
  description: 'Browse our full product catalogue',
}

export default async function ProductsPage() { ... }

For dynamic pages, export generateMetadata instead:

export async function generateMetadata({
  params,
}: {
  params: Promise<{ id: string }>
}): Promise<Metadata> {
  const { id } = await params
  const product = await fetch(`https://api.example.com/products/${id}`).then(r => r.json())

  return {
    title: product.name,
    description: product.description,
  }
}

<aside> ⌨️

Hands on: Add a metadata export to your home page (src/app/page.tsx) with a custom title and description. Open the page in the browser and inspect the <head> - you should see <title> and <meta name="description"> tags injected by Next.js. Then add a generateMetadata function to a dynamic route (e.g. /posts/[id]) that sets the title to the fetched post's title.

</aside>

Image Optimisation with next/image

The native <img> tag loads images at their original size. next/image automatically:

import Image from 'next/image'

export function ProductCard({ product }) {
  return (
    <div>
      <Image
        src={product.imageUrl}
        alt={product.name}
        width={400}
        height={300}
      />
      <h2>{product.name}</h2>
    </div>
  )
}

For images that fill their container (responsive layouts), use fill:

<div style={{ position: 'relative', height: '300px' }}>
  <Image
    src={product.imageUrl}
    alt={product.name}
    fill
    style={{ objectFit: 'cover' }}
  />
</div>

<aside> ⚠️

External images need to be allowlisted in next.config.ts. Add the hostname to images.remotePatterns:

</aside>

// next.config.ts
const config = {
  images: {
    remotePatterns: [
      { hostname: 'images.unsplash.com' },
      { hostname: 'cdn.example.com' },
    ],
  },
}

Always provide width and height for next/image - this lets the browser reserve space before the image loads, preventing Cumulative Layout Shift (CLS), which affects both UX and SEO scores.

<aside> ⌨️

</aside>

Font Optimisation with next/font

next/font automatically self-hosts Google Fonts - the font files are downloaded at build time and served from your own domain. This eliminates a network request to Google's servers on every page load, improving performance and privacy.

// app/layout.tsx
import { Inter } from 'next/font/google'

const inter = Inter({
  subsets: ['latin'],
  display: 'swap', // show system font until Inter loads
})

export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

For local fonts:

import localFont from 'next/font/local'

const myFont = localFont({ src: '../public/fonts/MyFont.woff2' })

next/font also handles font-display: swap and subset loading automatically - no <link> tags in your HTML or manual font file downloads needed.

<aside> ⌨️

</aside>


Additional Resources

Reading