Streaming in SvelteKit without JavaScript
Tags: sveltekit, svelte, javascript, tutorial
Published: 2024-03-23 22:38:23

SvelteKit has a great feature. We can [stream](https://developer.mozilla.org/en-US/docs/Web/API/Streams_API) promises to the browser as they resolve. So your webpage will load in the browser as soon as possible and the data from promise will be loaded later on when they have been processed in the backend.

Streaming is useful especcially when your backend is sending enormous amount of data but you want to show the page to the user ASAP.  

The [SvelteKit streaming](https://kit.svelte.dev/docs/load#streaming-with-promises) is very simple and intuitive. 

Let me walk you through short tutorial including some edge cases. 

Fire up a new SvelteKit project in your terminal (using skeleton project option, no TypeScript):

```terminal
npm create svelte@latest streaming-example-app
cd streaming-example-app
npm install
```

We will use https://loripsum.net/ api to get some data. Not just some but also enormous huge chunk of Lorem Ipsum paragraphs taking really some time to load. This huge data are represented as `hugeData` which is the one to be streamed becuase of its size.

Create welcome page like this:
```svelte
<!-- src/routes/+page.svelte  -->
Go to <a href="stream">streamed data</a>
<br />
Go to <a href="nostream">not streamed data</a>
```

We will add two pages.

## Not Streamed Data

The first page will not stream data from the backend. So we can have comparission how slow this may be. The backend will send the data to the frontend when all data are ready.

```svelte
<!-- src/routes/nostream/+page.svelte -->
<script>
	export let data;
</script>

<a href="/">Home</a>

<h2>Data not streamed</h2>

<h2>minimalData</h2>
{@html data.minimalData}

<h2>biggerData</h2>
{@html data.biggerData}

<h2>hugeData</h2>
{@html data.hugeData}
```
The backend is very simple. We are loading some data, waiting for all of them and then sending the data to the frontend.

`minimalData` is one short paragraph `https://loripsum.net/generate.php?p=1&l=short`.

`biggerData` is one medium paragraph `https://loripsum.net/generate.php?p=1&l=medium`.

`hugeData` is 5000 long paragraphs `https://loripsum.net/generate.php?p=5000&l=long`.

The only trick we are using in the backend is `Promise.all` method. So the backend is fetching all the data in paralel. Even so the page will load only when all the data are ready. This takes quite some time. Not so great user experience. 

```js
// src/routes/nostream/+page.server.js
export async function load({ fetch }) {
    async function minimalRespponse() {
        let resp = await fetch(`https://loripsum.net/generate.php?p=1&l=short`)
        return await resp.text()
    }

    async function biggerRespponse() {
        let resp = await fetch(`https://loripsum.net/generate.php?p=1&l=medium`)
        return await resp.text()
    }

    async function hugeResponse() {
        let resp = await fetch(`https://loripsum.net/generate.php?p=5000&l=long`)
        return await resp.text()
    }

    const [minimalData, biggerData, hugeData] =
    await Promise.all([
        await minimalRespponse(),
        await biggerRespponse(), 
        await hugeResponse()
    ]);

    return {
        minimalData,
        biggerData,
        hugeData
    }
}
```

## Streamed Data

The second page will use streaming of data from the backend.

minimalData and biggerData are sent to the frontend when they are available. But the hugeData are not send directly, we are sending the respective promise instead. Initially the frontend shows "_Loading ..._" in `#await` block. The hugeData are received and shown in `:then` sub-block later on as soon as they are processed and streamed from the backend and thus the promise gets resolved. 

The page does not wait for all the data and can be rendered quite fast.

```svelte
<!-- src/routes/stream/+page.svelte -->
<script>
	export let data;
</script>

<a href="/">Home</a>

<h2>Data streamed</h2>

<h2>minimalData</h2>
{@html data.minimalData}

<h2>biggerData</h2>
{@html data.biggerData}

<h2>hugeData</h2>
{#await data.hugeData}
	Loading ...
{:then hugeData}
	{@html hugeData}
{/await}
```
In the backend you can see that we are not awaiting the hugeData. The backend returns the promise in matter instead as `hugeData: hugeResponse()` and lets the frontend to await the hugeData.

Make sure the data awaited in the backend (i.e. minimalData and biggerData  which are not streamed) are sent at the end of `return { ... }` block, otherwise we can't start loading hugeData until we've loaded the minimalData and biggerData.

We are also using the trick of `Promise.all` as has been already mentioned above.

```js
// src/routes/nostream/+page.server.js
export async function load({ fetch }) {
    async function minimalRespponse() {
        let resp = await fetch(`https://loripsum.net/generate.php?p=1&l=short`)
        return await resp.text()
    }

    async function biggerRespponse() {
        let resp = await fetch(`https://loripsum.net/generate.php?p=1&l=medium`)
        return await resp.text()
    }

    async function hugeResponse() {
        let resp = await fetch(`https://loripsum.net/generate.php?p=5000&l=long`)
        return await resp.text()
    }

    const [minimalData, biggerData] =
    await Promise.all([
        await minimalRespponse(),
        await biggerRespponse()
    ]);

    return {
        hugeData: hugeResponse(),
        minimalData,
        biggerData
    }
}
```

If you have coded along you should see quite a huge page laod improvement already.

## No JavaScript Problem
But all this fails if the user has no JavaScript. Just go to your develper console using F12, press Ctrl+Shift+P, type "_javascript_" and disable JavaScript. The page with streaming will show "_Loading ..._" forever now.

No JavaScript case may happen more often then you would like as [this famous article "explains"](https://www.kryogenix.org/code/browser/everyonehasjs.html). It might be around 1 % of users still I [guess](https://adamsilver.io/blog/javascript-isnt-always-available-and-its-not-the-users-fault/).

There are probably three ways to handle this problem:

1. Throw noJS users overborad
This is really againts accesinility but you can use the [noscript](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/noscript) element like this on your page `<noscript>This page needs JavaScript. Enable JavaScript please.</noscript>`.

2. isDataRequest check
This solution was introduced by Geoff Rich using [conditional streaming with `isDataRequest` check](https://geoffrich.net/posts/conditionally-stream-data/). But unfortunatelly it is important to note that this does not solve the first page load. In that case users with JavaScript will have no streaming. So in my humble opinion the problem is that this cripples experience for all 99 % of users loading the page who have JavaScript just to give not even so good experience for user without JavaScript either. Some may even call it _"degressive" enhancement_.

3. Custom url search parameter check
The solution I would recommend is to have streaming in the first place and conditionally show "_Load more_" only to the users who do not have JavaScript. We can use custom url search parameter to achieve this goal.

## Conditionally Not Stream Data with Custom Url Search Parameter
So update your src/routes/nostream/+page.svelte file like is shown hereunder. 

If the user has JavaScript everything works as before. 

But if not we are showing `<noscript>` tag with a link to laod more data with an [url search parameter](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams) `noJS` which equals to `true` (you can use any other name for the parameter the name `noJS` seemed just appropriate to me). The noscript tag is automatically rendered and executed only to users who do not have JavaScript. 

```svelte
<!-- src/routes/nostream/+page.svelte -->
<script>
	export let data;
</script>

<a href="/">Home</a>

<h2>Data streamed</h2>

<h2>minimalData</h2>
{@html data.minimalData}

<h2>biggerData</h2>
{@html data.biggerData}

<h2>hugeData</h2>
{#await data.hugeData}
	<!-- showing this div only to users who have JavaScript enabled -->
	<div class="jsonly">Loading ...</div>
	<!-- showing this noscript tag only to users who do not have JavaScript -->
	<noscript>
		<style>
			.jsonly {
				display: none !important;
			}
		</style>
		<a href="/stream?noJS=true">Load the rest</a>
	</noscript>
{:then hugeData}
	{@html hugeData}
{/await}
```

Update of the backend src/routes/stream/+page.server.js file is quite straightforward. Get the `noJS` url search parameter using `let noJS  = !!url.searchParams.get("noJS")`. If `noJS` is true we will not send the promise but await the data. If there is no such url search parameter the backend will send the promise to be streamed. This is the relevant part of the code: `hugeData: noJS ? await hugeResponse() : hugeResponse(),`.

```js
// src/routes/stream/+page.server.js
export async function load({ fetch, url }) {
    let noJS  = !!url.searchParams.get("noJS")

    async function minimalRespponse() {
        let resp = await fetch(`https://loripsum.net/generate.php?p=1&l=short`)
        let respText = await resp.text()
        return respText
    }

    async function biggerRespponse() {
        let resp = await fetch(`https://loripsum.net/generate.php?p=1&l=medium`)
        let respText = await resp.text()
        return respText
    }

    async function hugeResponse() {
        let resp = await fetch(`https://loripsum.net/generate.php?p=5000&l=long`)
        let respText = await resp.text()
        return respText
    }

    const [minimalData, biggerData] =
    await Promise.all([
        await minimalRespponse(),
        await biggerRespponse()
    ]);

    return {
        hugeData: noJS ? await hugeResponse() : hugeResponse(),
        minimalData,
        biggerData
    }
}
```

## Conclusion
I hope this was usefull. 

Compared to isDataRequest check solution the users with JavaScript have smooth experience all the time. The friction of users who do not have JavaScript have to deal with "_Load more_" link. But I guess it is worth it because their experience is already worse. It seems like a good tradeoff.

This shows againg how versatile SvelteKit is, how SvelteKit adheres to web standards and even can work without JavaScript if neccessary.

You may even consider and add css feature `content-visibility: auto;` to render long page "[_on-demand_](https://developer.mozilla.org/en-US/docs/Web/CSS/content-visibility#auto)" but some users might not like a "_scrolling glich_".


SveleKit streaming is a huge booster for some of my projects where sometimes I need to show loads of data. Big thank to SvelteKit team.