SSR, hydration, boundaries and async

Svelte · Expert · 15 min

Server rendering and async UI have the trickiest edge cases: mismatches, leaked state and loading coordination.

Server rendering: render(App, { props }) from svelte/server returns { head, body } HTML. Only component scripts and markup run; $effect, onMount and attachments do not, so browser-only work belongs there. On the client, `hydrate(App, { target, props })` adopts the existing DOM instead of recreating it. SvelteKit does both for you.

Hydration expects the client's first render to produce the same output. Non-deterministic values (random ids, Date.now(), window checks during render) cause mismatches; use $props.id() for ids and compute browser-only values in effects. Remember that module-level state is shared across all server requests.

`<svelte:boundary>` (5.3+) catches errors thrown while rendering or in effects of its children: provide a failed snippet, which receives (error, reset), and an onerror callback for logging. This keeps one broken widget from blanking the page.

Experimental async (5.36+): with the experimental.async compiler option you can await directly in <script>, in $derived expressions and in markup. A boundary's pending snippet shows while its contents first load; later updates keep the old UI until new data is ready, and $effect.pending() reports in-flight work. Independent awaits in the same template run in parallel, and updates are coordinated so the UI never shows a mix of old and new data. Because the API can still change, check the docs before using it in production.

Example

<!-- svelte.config.js: export default { compilerOptions: { experimental: { async: true } } } -->
<script>
  import Chart from './Chart.svelte';
  let id = $state(1);
  async function getUser(id) {
    const res = await fetch(`/api/users/${id}`);
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  }
</script>

<button onclick={() => id++}>next</button>
{#if $effect.pending()}<span>updating...</span>{/if}

<svelte:boundary onerror={(e) => console.error(e)}>
  <h2>{(await getUser(id)).name}</h2>      <!-- await in markup (experimental) -->
  <Chart userId={id} />

  {#snippet pending()}<p>Loading...</p>{/snippet}
  {#snippet failed(error, reset)}
    <p>Error: {error.message}</p><button onclick={reset}>retry</button>
  {/snippet}
</svelte:boundary>

Remember

  • render (server) and hydrate (client); effects never run on the server
  • Avoid non-deterministic render output; use $props.id() for ids
  • <svelte:boundary> with failed and pending snippets isolates errors and loading
  • await in markup and deriveds is experimental behind experimental.async

Watch out for

  • Reading window or localStorage at the top level of a component script, which crashes during SSR.

Check yourself

  1. What does a `failed` snippet on `<svelte:boundary>` receive?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: svelte.dev/docs/svelte/svelte-boundary

Builds on

Unlocks

Nothing builds on this yet.