Stanislav Khromov

Streaming in SvelteKit is a powerful feature that allows you to load data progressively. In a nutshell, streaming allows your SvelteKit app to send an initial content response to the browser quickly, while fetching and sending additional data as it becomes available. This can make your app feel more responsive, especially when dealing with slow data sources. While streaming data loads you can easily show loading spinners.

What is streaming?

In a traditional SvelteKit application without streaming, the server waits until data from all load functions has finished loading before sending the complete response to the browser. This means users have to wait for the slowest data fetch to complete before seeing anything. This is the case for both the initial SSR load, as well as for future client-side navigations using the built-in Kit router.

With streaming, SvelteKit instead:

  1. Sends the initial HTML (SSR) or JSON data (client-side navigation) immediately
  2. Keeps the connection to the server open
  3. Streams in additional data as it becomes available by appending data to the existing response
  4. Finally closes the connection when everything is loaded

SvelteKit streaming is a server feature. It should only be used in +page.server.ts files.

Let’s look at a practical example. We’ll start with a basic load function in a +page.server.ts that doesn’t use streaming:

import type { PageServerLoad } from './$types';

export const load: PageServerLoad = () => {
    const quote = "The only way to do great work is to love what you do. - Steve Jobs";
    return {
        quote,
    };
};

It’s also common to have asynchronous data sources like external API:s, and that looks almost the same! The difference is that you make the load function async, and await the result of any asynchronous calls before returning them:

import type { PageServerLoad } from './$types';

const getQuote = async () => {
  // you can use await here to call API:s!
  return "The only way to do great work is to love what you do. - Steve Jobs";
};

export const load: PageServerLoad = async () => {
    return {
        quote: await getQuote()
    };
};

Regardless of whether you have a sync or async data source, it looks the same in the +page.svelte component:

<script>
    let { data } = $props();
</script>

{data.quote}

Now it’s time to enable streaming, and it’s quite simple! We need to:

  1. Make our load functions return a promise instead of just returning our data directly or awaiting it in the load function
  2. Use Svelte’s {#await} syntax to handle the promises

Let’s improve our previous async load function and add a little bit of artificial delay so we simulate what would happen when you contact an external API. Imporantly, we will no longer await the call to getQuote()

import type { PageServerLoad } from "./$types";

const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));

export const load: PageServerLoad = async ({ setHeaders }) => {
  const getQuote = async () => {
    await delay(1000);
    return "The only way to do great work is to love what you do. - Steve Jobs";
  };


  return {
    quote: getQuote(),
  };
};

Now we’ll add the {#await} block in the +page.svelte component to handle the streaming promise:

<script>
    let { data } = $props();
</script>

{#await data.quote}
  <p class="quote placeholder">Loading inspiring quote<span class="loading-dots"></span></p>
{:then quote}
  <p class="quote">{quote}</p>
{:catch error}
  <p class="quote error">Failed to load quote: {error.message}</p>
{/await}

As we see in the example code, SvelteKit also provides us with a simple way to handle errors using {:catch}.

How does streaming work technically?

When you make a request to a streaming endpoint, the response comes in multiple parts:

In the SSR (initial load) case

  1. First, you get the initial HTML with loading states you defined in your {#await} block
  2. Then, as data becomes available when the promises resolve on the server, SvelteKit sends additional data by appending <script> tags to the HTML response
  3. These scripts contain the actual data and update the loading states with real content

The actual HTML response will look like this

<!doctype html>
<html lang="en">
  ...the entire SSR html...
</html>
<script>__sveltekit_dev.resolve({id:1,data:"The only way to do great work is to love what you do. - Steve Jobs",error:void 0})</script>

In the SPA navigation case (navigations after initial load)

  1. First, you get the data in the __data.json call the Kit SPA router makes
  2. Then, as data becomes available, additional JSON objects are appended to the response, separated by new lines (essentially making this the JSON Lines format)
{"type":"data","nodes":[{"type":"skip"},{"type":"data","data":[{"quote":1,"menuItems":3,"isOpen"....
{"type":"chunk","id":1,"data":["The only way to do great work is to love what you do. - Steve Jobs"]}

Error handling

In your load function, you can indicate than an error occured using SvelteKit’s error() helper, and then catch and display these errors gracefully in your component using Svelte’s {:catch} block in the await syntax. This provides a smooth way to show friendly error messages to users when your streamed data fails to load. Here’s a basic example:

import type { PageServerLoad } from "./$types";

export const load: PageServerLoad = async ({ setHeaders }) => {
  const getQuote = async () => {
    error(500, "Oh no!");
  };
};

Now let’s cache it in +page.svelte

<script>
  let { data } = $props();
</script>

<div class="quote-container">
  {#await data.quote}
      <p class="quote">Loading inspiring quote</p>
  {:then quote}
      <p class="quote">{quote}</p>
  {:catch error}
      <p class="quote error">Failed to load quote: {error.message}</p>
  {/await}
</div>

Considerations when using streaming with promises

Before implementing streaming, there are two major considerations to keep in mind:

JavaScript Requirement
Streaming relies on JavaScript to work. If you disable JavaScript, your users will be stuck seeing the loading states forever. This makes streaming generally unsuitable for scenarios where you need to support users without JavaScript.

SEO Impact
Search engines like Google use a two-phase indexing approach. The initial crawl is SSR-only, and so Google only sees the loading states. While Google will eventually execute the JavaScript and see the full content, this can take hours or even weeks.

However, as we will learn a bit later in this blog post, there is a clever workaround we can use to get around this issue.

When to Use Streaming

As a general guideline, streaming works best for:

  • Non-critical sidebar content
  • Comment sections
  • Social media feeds
  • Any content where immediate SEO indexing isn’t crucial

The main benefit is improved perceived performance – users see the page structure immediately and content fills in progressively rather than waiting for everything to load at once.

Conditional streaming – the best of both worlds

We have established that streaming is not suitable for content where SEO is important, but what if there was actually a way to get the best of both worlds – both excellent SEO and streaming for users who are navigating using the client-side router. By using conditional streaming, you can use the isDataRequest property to detect whether a request comes from the initial page load or client-side navigation, letting you serve full data initially for SEO and non-JavaScript users while enabling fast streaming updates during navigation.

Here is a basic example:

export async function load({ isDataRequest }) {
  const slowData = getSlowData();
  return {
    data: isDataRequest ? slowData : await slowData
  };
}

Platform Support

Streaming works on most major platforms like Vercel and the Node.js adapter. However, some serverless platforms might have limitations. Verify streaming support with your hosting provider before implementing it extensively.

Fin

Thank you for reading to the end! Have you implemented streaming in your SvelteKit applications? Let me know about your experiences in the comments below! You are also welcome to check out this demo repository I made that showcases the techniques presented in the blog post.

Want more content on SvelteKit streaming? Watch my video with practical examples of SvelteKit streaming below.

Share photo by Brian Botos on Unsplash

🇸🇪 Full-stack impostor syndrome sufferer & Software Engineer at Schibsted Media Group

View Comments

  • RuneRune

    Author Reply

    Hi,
    Thanks for the great post.

    I have am using your suggestions above, however I have one issue.
    Often I like to have a client variable like “let loading = $state(false)”, that enables certain features on the page when the data has finalised loading.
    How do I update the state of this variable when the async page.server.ts has successfully loaded?
    Regards,
    Rune


    • Hi Rune. Await has a loading state “built in” so I haven’t had much of a need for this, but you should be able to do something like:


      let { data } = $props();
      let loaded = $state(false);
      $effect(() => {
      data.promise.finally(() => loaded = true; );
      });


Next Post