Angular SSR vs SSG vs Prerendering: Per-Route Render Modes [2026]

Link copied
Angular SSR vs SSG vs Prerendering: Per-Route Render Modes [2026]

Angular SSR vs SSG vs Prerendering: Per-Route Render Modes [2026]

Every Angular app has to answer one question for every URL: where does the HTML come from? It can be produced in the browser after JavaScript downloads, on a server when the request arrives, or once at build time and served as a file. Picking one answer for the whole app is the most common mistake teams make with SSR. A marketing page, a product detail page and a logged-in dashboard have different needs, and Angular lets you give each route its own answer.

This is lesson 9.1 of the Angular Tutorial, and the first lesson of Module 9. It builds on lesson 8.9, which closed the client-side performance module, and on the router lessons from Module 5. You'll learn the three render modes Angular supports, how to assign them per route with ServerRoute[], how to prerender parameterised routes with getPrerenderParams, and a decision tree for choosing between them.

Three ways to produce HTML #

Angular's @angular/ssr package supports three render modes. "SSG" (static site generation) and "prerendering" mean the same thing in Angular: HTML generated at build time.

Mode When HTML is produced What the first response contains Best for
Client (CSR) In the browser, after the JavaScript bundles run An almost empty index.html Authenticated dashboards, editors, anything behind a login
Server (SSR) On every request, by a Node or edge server Fully rendered HTML for that request Pages whose content changes often or depends on the request
Prerender (SSG) Once, at build time A static HTML file, identical for every visitor Content that changes only when you deploy

All three modes produce the same running application once the browser has loaded the JavaScript. The differences are about the first response: how fast it arrives, whether it contains real content for users and crawlers, and how much server work it costs.

The trade-offs in numbers you can feel #

Each mode moves work to a different place, and that shows up in the metrics from lesson 8.6:

Concern Client Server Prerender
Time to first byte Fast (static file) Slower: rendering happens per request Fast (static file)
Largest Contentful Paint Slow: waits for JS, then data Fast if data fetching is quick Fastest
Content visible to crawlers without JS No Yes Yes
Server cost per request None CPU per request None
Freshness Always current Always current As old as the last build
Request-specific content (cookies, headers) In the browser only Yes No
Needs a running server No Yes No

Two rows matter most. Freshness rules out prerendering for anything that changes between deploys, unless you are willing to rebuild on content change. Request-specific content rules out prerendering for anything that differs per user, because the same file is served to everyone.

Setting up hybrid rendering #

New projects get SSR support with ng new --ssr. For an existing project, add it with:

ng add @angular/ssr

This adds @angular/ssr, a server.ts entry, a server application config, and a file called app.routes.server.ts. That last file is where render modes live. It is separate from your normal app.routes.ts: the client router config stays the same, and the server routes add rendering instructions on top, matched by path.

// app.routes.server.ts
import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [
  { path: '', renderMode: RenderMode.Prerender },          // home page: static
  { path: 'pricing', renderMode: RenderMode.Prerender },   // changes per release
  { path: 'search', renderMode: RenderMode.Server },       // depends on query string
  { path: 'account/**', renderMode: RenderMode.Client },   // behind login
  { path: '**', renderMode: RenderMode.Server },           // everything else
];

The server routes are wired in through provideServerRendering(withRoutes(serverRoutes)) in the server config; lesson 9.2 covers that file and the bootstrap in detail. Order matters in the same way it does in the client router: put specific paths before the ** catch-all.

Prerendering parameterised routes #

A route like blog/:slug can't be prerendered without knowing which slugs exist. getPrerenderParams tells the builder:

import { inject } from '@angular/core';
import { PrerenderFallback, RenderMode, ServerRoute } from '@angular/ssr';
import { PostService } from './posts/post.service';

export const serverRoutes: ServerRoute[] = [
  {
    path: 'blog/:slug',
    renderMode: RenderMode.Prerender,
    async getPrerenderParams() {
      const posts = inject(PostService);          // inject() first, before any await
      const slugs = await posts.getAllSlugs();
      return slugs.map((slug) => ({ slug }));     // one object per page to render
    },
    fallback: PrerenderFallback.Server,           // unknown slugs are rendered on demand
  },
  { path: '**', renderMode: RenderMode.Server },
];

Three details are easy to miss:

  • inject() must run synchronously. It works inside getPrerenderParams because the function runs in an injection context, but only before the first await. Grab every service you need at the top.
  • The function returns parameter objects, not URLs. Each object's keys match the route's parameter names. Catch-all segments use the key '**'.
  • fallback decides what happens for paths that weren't prerendered, such as a post published after the build.
PrerenderFallback Request for a path that wasn't prerendered
Server (default) Rendered on the server, like RenderMode.Server
Client Served as a client-rendered page
None Not handled by Angular; your server's next handler (usually a 404) responds

Server is the right default for content sites: new posts work immediately, and the next build turns them into static files. None makes sense for a fully static deployment where no server will be running.

Fully static output #

If every route is prerendered or client-rendered, you don't need a server at all. Set the build's output mode to static:

{
  "projects": {
    "my-app": {
      "architect": {
        "build": {
          "options": {
            "outputMode": "static"
          }
        }
      }
    }
  }
}

The build then writes HTML files and assets only, which you can upload to any static host or object storage behind a CDN. Routes marked RenderMode.Server don't make sense in this mode, so keep them out of your server routes when you choose it.

Setting status codes and headers per route #

Server routes can also attach response metadata. This is useful for cache headers on prerendered pages and for marking a catch-all page as a real 404:

export const serverRoutes: ServerRoute[] = [
  {
    path: 'docs/**',
    renderMode: RenderMode.Prerender,
    headers: { 'Cache-Control': 'public, max-age=3600' },
  },
  {
    path: 'not-found',
    renderMode: RenderMode.Server,
    status: 404,
  },
  { path: '**', renderMode: RenderMode.Server },
];

Lesson 9.7 builds a full caching strategy on top of these headers. When the status depends on data, for example a product ID that doesn't exist, you set it from the component during the request instead; lesson 9.2 shows how with the RESPONSE_INIT token.

A decision tree for each route #

Walk each route through these questions in order. The first "yes" wins.

  1. Does the page only make sense for a logged-in user (dashboard, settings, editor)? Use Client. Crawlers can't see it anyway, and server-rendering per-user pages adds cost and caching risk for little gain.
  2. Is the content the same for every visitor and does it change only when you deploy (home, pricing, docs, legal pages)? Use Prerender.
  3. Is it the same for every visitor but drawn from a CMS or catalogue that changes between deploys? Use Prerender with getPrerenderParams and fallback: Server if a stale page for a few hours is acceptable and you rebuild regularly. Otherwise use Server with a CDN cache in front (lesson 9.7).
  4. Does it depend on the request (query string, cookies, geolocation headers, A/B bucket)? Use Server.
  5. Not sure? Start with Server. It is always correct, and you can move routes to Prerender later as you learn which pages are truly static.

For a typical content product, that produces a mix like this:

Route Mode Why
/ Prerender Same for everyone, changes per release
/blog/:slug Prerender + getPrerenderParams + fallback: Server Many pages, new ones appear between builds
/search Server Depends on query string
/products/:id Server, CDN-cached Price and stock change often
/account/** Client Personal, behind login

What hybrid rendering does not change #

A few things stay the same whichever mode a route uses, and they trip people up:

  • Client-side navigation is still client-side. Render modes apply to the first request for a URL. After the app boots, clicking a router link renders the next page in the browser, exactly as in a normal SPA.
  • Your components run on the server for Server and Prerender routes. Code that touches window, localStorage or the DOM directly will fail during rendering. Lesson 9.2 covers how to keep browser-only code out of the server path.
  • Server-rendered HTML still needs hydration. The browser downloads the same JavaScript and attaches it to the existing DOM rather than throwing it away. Lessons 9.3 to 9.5 cover how that works and how to make it cheaper.
  • Lazy loading still matters. Server rendering improves the first paint; it does not shrink your bundles. The route-level code splitting and @defer blocks from lesson 5.6 are just as important.

Gotchas #

Symptom Cause Fix
Build fails or times out while prerendering getPrerenderParams calls an API that is unreachable at build time, or returns thousands of pages Make the API reachable in CI; prerender only the most visited pages and let fallback: Server handle the rest
inject() must be called from an injection context in getPrerenderParams inject() called after an await Call every inject() at the top of the function
New blog post returns 404 in production Route prerendered with fallback: PrerenderFallback.None Use Server (the default) or rebuild when content is published
Logged-in user sees another user's name Per-user page prerendered or server-rendered and cached Use RenderMode.Client for personal pages, or never cache them publicly
A route renders in the wrong mode A broader pattern or ** appears before it Order specific paths first
RenderMode.Server route fails on a static host No server is running, outputMode: "static" Switch the route to Prerender or Client, or deploy the server build

What's next #

Lesson 9.2 opens up the machinery behind these modes: provideServerRendering(), the server application config, server.ts with AngularNodeAppEngine, what runs on the server versus the browser, and the REQUEST, RESPONSE_INIT and REQUEST_CONTEXT tokens. The rest of Module 9 then covers hydration pitfalls, event replay, incremental hydration with @defer, avoiding double data fetches with the transfer cache, and caching and edge deployment.

Try it yourself #

List the top-level routes of an app you work on and run each one through the decision tree above. Write the result as an app.routes.server.ts file, even if the app doesn't use SSR yet. Count how many routes end up as Prerender: that's how much of your app could be served as static files.

YouWe have about 4,000 product pages. Should they be prerendered with getPrerenderParams, or server-rendered? Prices change a few times a day.
Claude · used search_documentationServer-render them. Prices that change several times a day mean a prerendered page goes stale between builds, and rebuilding 4,000 pages on every price change makes your build the bottleneck. Use { path: 'products/:id', renderMode: RenderMode.Server } in app.routes.server.ts and put a CDN in front with a short shared cache, for example Cache-Control: public, s-maxage=300, stale-while-revalidate=60, so most requests never reach Node. If a subset is effectively static, such as discontinued items or category landing pages, prerender just those with getPrerenderParams and keep fallback: PrerenderFallback.Server. Keep the cart and account routes on RenderMode.Client so per-user data never ends up in a shared cache.

Up next in Angular

More from this topic

View all Angular articles →
Angular

When Angular is launched ?

Link copied Angular When Angular is launched ? February 8, 2024 · 1 min read When was Angular launched? # Angu…

Feb 8, 2024 Read →

Enjoyed this article?

Get new Angular tutorials delivered. No spam — just code-first articles when they ship.

1 comment

1 thought on “Angular SSR vs SSG vs Prerendering: Per-Route Render Modes [2026]”

  1. Pingback: Angular SSR Bootstrap: provideServerRendering [2026]

Leave a Comment

Your email stays private. Required fields are marked *

1 thought on “Angular SSR vs SSG vs Prerendering: Per-Route Render Modes [2026]”

  1. Pingback: Angular SSR Bootstrap: provideServerRendering [2026]

Leave a Comment

Your email stays private. Required fields are marked *