Skip to content
ExponaBLOG
SVELTE & SVELTEKIT · DAY 028 / 100

Rendering options in SvelteKit: best practices explained

SSR, prerendering and SPA mode.

Expona Admin7 min read

Introduction

By day 28 of building with SvelteKit, you've likely shipped features that work, but you might be asking: why does one page load instantly while another takes a moment? The answer lies in understanding rendering options in SvelteKit: best practices explained. SvelteKit gives you three distinct modes—server-side rendering (SSR), prerendering, and client-side rendering (CSR)—and the choice you make for each route dramatically affects performance, SEO, and maintainability.

In this article, we'll walk through each rendering option, show you when to use them, and explain how to mix them intelligently within a single application. By the end, you'll know exactly which strategy to apply to your pages and why.

Understanding SSR: The SvelteKit Default

Server-side rendering (SSR) is SvelteKit's default behavior, and it's enabled by default for good reason. When a request arrives, your SvelteKit server renders the page to HTML on the server and sends the fully formed HTML document to the browser. The browser paints pixels immediately, then hydrates the page with JavaScript for interactivity.

The benefits are significant. First-contentful paint (FCP) is typically fast because the browser receives meaningful HTML. Search engines receive complete, indexed content—no waiting for JavaScript to execute. Database queries and authentication checks happen on the server where they're secure and close to your data.

Consider a product listing page in an e-commerce application. On request, your server queries the database, formats the product data, renders it to HTML, and sends it to the browser. The user sees products instantly. Search engines crawl the full content. Analytics engines see the page title and metadata in the initial response.

typescript
// src/routes/products/+page.server.ts
export async function load({ fetch }) {
  const products = await fetch('/api/products').then(r => r.json());
  return { products };
}
typescript
// src/routes/products/+page.svelte
<script>
  export let data;
</script>

<h1>Products</h1>
{#each data.products as product (product.id)}
  <div>{product.name} - ${product.price}</div>
{/each}

By default, both +page.server.ts and +page.svelte execute on the server. The rendered HTML is sent to the browser, JavaScript loads, Svelte hydrates the component, and clicks and form submissions work.

The trade-off: every page request hits your server. For high-traffic sites, this means scaling your Node process or implementing caching strategies. But for most applications—blogs, dashboards, content sites—SSR is the right default.

Prerendering: Static Pages at Build Time

Prerendering takes pages you know won't change frequently and generates their HTML during the build. Instead of rendering on every request, you ship static HTML files that a CDN can serve from the edge. This is lightning-fast and removes server load entirely.

To enable prerendering, export a prerender constant from your +page.ts or +page.server.ts:

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

export function load() {
  return {
    title: 'About Us',
    content: 'We are a small team...'
  };
}

During npm run build, SvelteKit will visit this route, run the load function, render the page, and write the HTML to build/about/index.html. When your app deploys, that file is static—no server logic runs. A request for /about gets served directly from your hosting platform's edge or origin with zero server overhead.

This works beautifully for documentation sites, landing pages, and policy pages. A travel SaaS might prerender its pricing page, terms of service, and feature overview, then serve them globally from a CDN. The combination of instant load time and zero server cost is hard to beat.

But prerendering has limits. The page must be deterministic—the same load function returns the same data every time you build. You can't prerender a user profile page (different for each user) or a real-time price feed (changes constantly). And if you update your content, you must rebuild and redeploy the entire application.

For pages with limited, stable URLs, prerendering is a performance win. Use it strategically:

  • Good candidates: About pages, blog posts, documentation, pricing, terms, features
  • Poor candidates: User profiles, dashboards, search results, dynamic product listings

CSR Only: Client-Side Rendering for Apps

Some routes don't need server-side rendering at all. A complex dashboard, an interactive configuration tool, or a collaborative canvas can be fully client-side. When the server sends back a minimal HTML shell and JavaScript, the browser fetches data via API and renders it.

Disable SSR with:

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

export function load({ fetch }) {
  return { fetch };
}

Now, when a request arrives, SvelteKit renders a skeleton HTML page—no page-specific content. JavaScript loads, your Svelte component initializes, and onMount or load functions fetch data and render the UI.

The benefit: you move rendering work to the client, reducing server CPU. The browser can cache assets aggressively. For single-page applications (SPAs) or heavily interactive experiences, this is appropriate.

The downside: the initial HTML is nearly empty. Users see a blank page until JavaScript loads and runs. Search engines see no content—your dashboard won't rank. Time to first meaningful paint increases. First-time visitors on slow connections feel the delay.

Use CSR only for authenticated or app-like experiences where SEO doesn't matter:

  • Good candidates: Admin dashboards, user settings, collaborative tools, real-time apps
  • Poor candidates: Marketing pages, blog posts, public product pages
typescript
// src/routes/dashboard/+page.svelte
<script>
  import { onMount } from 'svelte';
  let data = null;

  onMount(async () => {
    const res = await fetch('/api/dashboard');
    data = await res.json();
  });
</script>

{#if data}
  <h1>Dashboard</h1>
  <p>Revenue: {data.revenue}</p>
{:else}
  <p>Loading...</p>
{/if}

Mix and Match Per Route

The power of SvelteKit lies in mixing these strategies within a single app. Your marketing site might use SSR and prerendering. Your admin dashboard uses CSR. Your blog posts prerender, but your search results use SSR.

Configure per route by setting prerender or ssr in the appropriate +page.ts or +layout.ts file:

typescript
// src/routes/blog/[slug]/+page.ts
export const prerender = true;

export async function load({ params }) {
  const post = await getPost(params.slug);
  return { post };
}
typescript
// src/routes/search/+page.ts
// SSR enabled by default; no export needed

export async function load({ url }) {
  const query = url.searchParams.get('q');
  const results = await search(query);
  return { results };
}
typescript
// src/routes/app/settings/+page.ts
export const ssr = false;

export function load({ fetch }) {
  return { fetch };
}

You can also set defaults in svelte.config.js and override per route:

typescript
// svelte.config.js
export default {
  kit: {
    prerender: {
      entries: ['/sitemap.xml', '/rss.xml'],
      handleHttpError: 'warn'
    }
  }
};

This approach scales elegantly. You optimize each route for its actual use case without compromising others.

Common Mistakes

Prerendering dynamic routes without fallback: If you prerender a blog post route but don't tell SvelteKit which slugs exist, it only builds the routes you specify. New posts won't appear until the next build. Use dynamic routes carefully or disable prerendering for them.

typescript
// This won't prerender all blog posts automatically
export const prerender = true;

export async function load({ params }) {
  const post = await getPost(params.slug);
  return { post };
}

// Instead, export prerender entries:
export const entries = async () => {
  const posts = await getAllPosts();
  return posts.map(p => ({ slug: p.slug }));
};

Forgetting hydration mismatch with CSR: If you disable SSR and render content in onMount, the initial HTML and the rendered page differ. Browsers hydration may fail silently. Keep initial HTML consistent or use skeleton loaders.

Over-prerendering: Prerendering hundreds of pages increases build time and bundle size. It's not a cure-all. If your data changes frequently, prerendering is the wrong tool.

Disabling SSR when you need SEO: Setting ssr = false on a public page you want to rank removes all HTML content from the initial response. Search engines won't index it well. Reserve CSR for private or authenticated routes.

Key Takeaways

  • SSR is the SvelteKit default and the right choice for most public pages—fast FCP, SEO-friendly, secure server-side logic.
  • Prerender static pages at build time to remove server load and serve from CDN edges.
  • Use CSR only for authenticated or app-like experiences where SEO doesn't matter and client-side rendering improves user experience.
  • Configure rendering mode per route based on content freshness, user authentication, and SEO requirements.
  • Avoid common pitfalls: prerendering dynamic routes without entries, hydration mismatches, over-prerendering, and disabling SSR on public content.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles