Streaming in SvelteKit without JavaScript

kvetoslavnovak - Mar 23 - - Dev Community

SvelteKit has a great feature. We can stream 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 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):

npm create svelte@latest streaming-example-app
cd streaming-example-app
npm install
Enter fullscreen mode Exit fullscreen mode

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:

<!-- src/routes/+page.svelte  -->
Go to <a href="stream">streamed data</a>
<br />
Go to <a href="nostream">not streamed data</a>
Enter fullscreen mode Exit fullscreen mode

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.

<!-- 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}
Enter fullscreen mode Exit fullscreen mode

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.

// 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
    }
}
Enter fullscreen mode Exit fullscreen mode

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.

<!-- 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}
Enter fullscreen mode Exit fullscreen mode

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.

// 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
    }
}
Enter fullscreen mode Exit fullscreen mode

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". It might be around 1 % of users still I guess.

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 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. 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 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.

<!-- 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}
Enter fullscreen mode Exit fullscreen mode

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(),.

// 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
    }
}
Enter fullscreen mode Exit fullscreen mode

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" 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.

. . . . . . . . . . . . . . . . . .
Terabox Video Player