← Back to DevBytes

Server-Side Rendering with Svelte: SSR, SSG, ISR

Server-Side Rendering with Svelte: SSR, SSG, ISR

Modern web applications demand fast initial page loads, excellent SEO, and a smooth user experience. Svelte, paired with its meta-framework SvelteKit, offers a powerful rendering model that lets you choose exactly how and where your pages are rendered. In this tutorial, we'll explore the three core rendering strategies — Server-Side Rendering (SSR), Static Site Generation (SSG), and Incremental Static Regeneration (ISR) — and learn how to apply them in real Svelte projects.

What Is Rendering in Svelte?

Rendering refers to where and when the HTML for a page is produced. SvelteKit, the official application framework for Svelte, supports multiple rendering modes that can be configured per route. Understanding the differences is essential for building performant applications.

Why Rendering Strategy Matters

Choosing the right rendering strategy affects three critical areas: performance, SEO, and infrastructure cost. SSR ensures search engines see fully rendered HTML but requires a running server. SSG delivers the fastest possible load times and can be deployed to any CDN, but content becomes stale until the next build. ISR bridges the gap by allowing static pages to be updated without rebuilding the entire site.

SvelteKit makes all three approaches available through a unified API, so you can mix strategies within a single application. A marketing site might use SSG for most pages, SSR for a user dashboard, and ISR for a blog that updates periodically.

Setting Up a SvelteKit Project

Before diving into rendering modes, let's create a fresh SvelteKit project. Make sure you have Node.js 18 or later installed.

npm create svelte@latest my-rendering-app
cd my-rendering-app
npm install
npm run dev -- --open

The wizard will ask you to choose a project template. Select "Skeleton project" and enable TypeScript if you prefer. Once the dev server is running, you'll have a basic SvelteKit app ready for experimentation.

Server-Side Rendering (SSR) in SvelteKit

SSR is the default rendering mode in SvelteKit. When a user requests a page, the server executes the load function, renders the Svelte component to HTML, and sends it to the browser. The client then "hydrates" the HTML to make it interactive.

How SSR Works

Each route can have a +page.svelte component and an optional +page.ts or +page.server.ts load function. The load function fetches data on the server before the component renders. Here's a basic example:

// src/routes/users/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ fetch }) => {
  const res = await fetch('https://jsonplaceholder.typicode.com/users');
  const users = await res.json();
  return { users };
};
<!-- src/routes/users/+page.svelte -->
<script lang="ts">
  let { data } = $props();
</script>

<h1>Users</h1>
<ul>
  {#each data.users as user}
    <li>{user.name} — {user.email}</li>
  {/each}
</ul>

Because the load function lives in a .server.ts file, it runs exclusively on the server. The user's browser receives fully rendered HTML, which is excellent for SEO and perceived performance.

Disabling SSR for Specific Routes

Sometimes you want a page to render only on the client — for example, a highly interactive dashboard behind authentication. You can disable SSR with a +page.ts file:

// src/routes/dashboard/+page.ts
export const ssr = false;
export const prerender = false;

This tells SvelteKit to skip server rendering for this route entirely. The browser receives a minimal HTML shell, and Svelte builds the page after hydration.

Static Site Generation (SSG) in SvelteKit

SSG pre-renders pages at build time. The output is a set of static HTML, CSS, and JavaScript files that you can deploy to any static host like GitHub Pages, Netlify, or Cloudflare Pages.

Enabling Prerendering

SvelteKit calls SSG "prerendering." You can enable it globally or per route. To prerender an entire site, set the adapter and configuration in svelte.config.js:

// svelte.config.js
import adapter from '@sveltejs/adapter-static';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter({
      pages: 'build',
      assets: 'build',
      fallback: '404.html',
      precompress: false,
      strict: true
    })
  }
};

export default config;

Install the static adapter if you haven't already:

npm install -D @sveltejs/adapter-static

Next, create a root layout that enables prerendering for all pages:

// src/routes/+layout.ts
export const prerender = true;

Now every page in your application will be generated as a static HTML file at build time. Run the build to see the output:

npm run build

Prerendering Individual Pages

You don't have to prerender the entire site. You can enable prerendering on a per-route basis by adding a +page.ts file to that route:

// src/routes/about/+page.ts
export const prerender = true;

This is useful when most of your app needs SSR but certain pages, like an about page or privacy policy, are static.

Handling Dynamic Routes During Prerendering

When prerendering, SvelteKit needs to know all the URLs ahead of time. For dynamic routes like src/routes/blog/[slug]/+page.svelte, you must provide an entries export or a prerender entry function:

// src/routes/blog/[slug]/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ params }) => {
  const res = await fetch(`https://api.example.com/posts/${params.slug}`);
  const post = await res.json();
  return { post };
};

export const entries = async () => {
  const res = await fetch('https://api.example.com/posts');
  const posts = await res.json();
  return posts.map((post) => ({ slug: post.slug }));
};

The entries function returns an array of parameter objects. SvelteKit iterates over them during the build, generating a static page for each slug.

Incremental Static Regeneration (ISR) in SvelteKit

ISR is a hybrid approach: pages are generated statically, but they regenerate in the background when new requests come in after a specified time. This gives you the performance of SSG with data freshness approaching SSR.

SvelteKit does not have a built-in revalidate option like Next.js, but you can achieve ISR using adapter-specific features or by combining prerendering with on-demand revalidation. The most common approach uses the @sveltejs/adapter-vercel or @sveltejs/adapter-netlify adapters, which expose platform-specific ISR capabilities.

ISR with Vercel Adapter

First, install the Vercel adapter:

npm install -D @sveltejs/adapter-vercel

Update your SvelteKit configuration:

// svelte.config.js
import adapter from '@sveltejs/adapter-vercel';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter()
  }
};

export default config;

Now, in your page's server load function, you can export a config object that enables ISR with a revalidation interval:

// src/routes/news/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ fetch }) => {
  const res = await fetch('https://api.example.com/news');
  const news = await res.json();
  return { news };
};

export const config = {
  isr: {
    expiration: 60
  }
};

In this example, the page is generated statically on the first request. Subsequent requests within 60 seconds receive the cached static page. After 60 seconds, the next request triggers a background regeneration, and the updated page replaces the stale cache.

On-Demand Revalidation

Sometimes you need to regenerate a page immediately — for instance, when content is published via a CMS. With the Vercel adapter, you can use a route handler to purge the ISR cache:

// src/routes/api/revalidate/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const POST: RequestHandler = async ({ request }) => {
  const { path, secret } = await request.json();

  if (secret !== process.env.REVALIDATE_SECRET) {
    return json({ error: 'Unauthorized' }, { status: 401 });
  }

  // Trigger revalidation through Vercel's API or platform hooks
  return json({ revalidated: true, path });
};

Your CMS can call this endpoint whenever content changes, ensuring users always see the latest version without waiting for the expiration interval.

ISR with Netlify Adapter

If you deploy to Netlify, the adapter provides similar ISR capabilities through Netlify's On-Demand Builders:

npm install -D @sveltejs/adapter-netlify
// svelte.config.js
import adapter from '@sveltejs/adapter-netlify';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';

const config = {
  preprocess: vitePreprocess(),
  kit: {
    adapter: adapter()
  }
};

export default config;
// src/routes/products/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ fetch }) => {
  const res = await fetch('https://api.example.com/products');
  const products = await res.json();
  return { products };
};

export const config = {
  isr: {
    expiration: 300
  }
};

This configuration tells Netlify to serve a cached version of the page and regenerate it after 5 minutes of inactivity.

Combining Rendering Strategies

One of SvelteKit's greatest strengths is the ability to mix rendering strategies within a single application. You can prerender your marketing pages, use SSR for authenticated areas, and apply ISR to frequently updated content — all without changing your deployment infrastructure.

Here's a typical layout configuration that demonstrates this flexibility:

// src/routes/+layout.ts
// Prerender the entire site by default
export const prerender = true;
export const ssr = true;
// src/routes/admin/+layout.ts
// Disable prerendering and SSR for the admin area
export const prerender = false;
export const ssr = false;
// src/routes/blog/[slug]/+page.ts
// Use ISR for blog posts
export const prerender = false;
export const ssr = true;

With this setup, marketing pages are static, the admin dashboard is client-rendered, and blog posts use SSR with optional ISR configuration in the server load function.

Best Practices

Choose the Right Strategy Per Route

Don't apply a single rendering mode to your entire application by default. Evaluate each route individually. Public, rarely changing content should be prerendered. Personalized or real-time data should use SSR. Content that updates on a schedule benefits from ISR.

Keep Load Functions Efficient

Load functions run on every request for SSR routes and on every regeneration for ISR routes. Keep them fast by minimizing external API calls, using caching headers, and fetching only the data you need. Avoid waterfall requests when parallel fetches are possible:

// src/routes/dashboard/+page.server.ts
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ fetch }) => {
  const [profileRes, statsRes, activityRes] = await Promise.all([
    fetch('https://api.example.com/profile'),
    fetch('https://api.example.com/stats'),
    fetch('https://api.example.com/activity')
  ]);

  const [profile, stats, activity] = await Promise.all([
    profileRes.json(),
    statsRes.json(),
    activityRes.json()
  ]);

  return { profile, stats, activity };
};

Handle Errors Gracefully

When a load function fails during SSR or prerendering, SvelteKit renders the error page. Always handle expected errors and provide meaningful fallbacks:

// src/routes/blog/[slug]/+page.server.ts
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';

export const load: PageServerLoad = async ({ params, fetch }) => {
  const res = await fetch(`https://api.example.com/posts/${params.slug}`);

  if (res.status === 404) {
    throw error(404, 'Post not found');
  }

  if (!res.ok) {
    throw error(500, 'Failed to load post');
  }

  const post = await res.json();
  return { post };
};

Use Environment Variables for Secrets

Never hardcode API keys or secrets in your load functions. Use environment variables prefixed with VITE_ for client-exposed values and private variables for server-only code:

// src/routes/api/data/+server.ts
import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';

export const GET: RequestHandler = async () => {
  const apiKey = process.env.API_KEY;
  const res = await fetch('https://api.example.com/data', {
    headers: { Authorization: `Bearer ${apiKey}` }
  });
  const data = await res.json();
  return json(data);
};

Test Your Build Output

Always run npm run build and npm run preview before deploying. This catches prerendering errors, missing entries for dynamic routes, and other issues that only surface during the build. Pay attention to warnings about routes that could not be prerendered — they often indicate missing data or incorrect configuration.

Monitor Cache Hit Rates

For ISR routes, monitor how often pages are served from cache versus regenerated. If your expiration interval is too short, you lose the performance benefits of static caching. If it's too long, users see stale content. Adjust based on your content update frequency and traffic patterns.

Conclusion

SvelteKit's flexible rendering model gives you fine-grained control over how every page in your application is delivered. SSR provides dynamic, personalized content with excellent SEO. SSG delivers blazing-fast static pages that deploy anywhere. ISR combines the best of both worlds by regenerating static content in the background. By understanding the strengths of each approach and applying them strategically per route, you can build applications that are fast, search-engine-friendly, and easy to maintain. Start with the default SSR mode, identify which pages can be prerendered, and introduce ISR where data freshness matters — your users and your infrastructure bill will thank you.

— Ad —

Google AdSense will appear here after approval

← Back to all articles