Enhancing “Degressive” Enhancement of Streaming

Rmag Breaking News

SvelteKit has a great feature. We can stream promises to the browser as they resolve. So your webpage will be loaded 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

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.

Create welcome page like this:

<!– src/routes/+page.svelte –>
Go to <a href=“stream”>streamed data</a>
<br />
<br />
Go to <a href=“nostream”>not streamed data</a>

Add two pages.

Not Streamed Data

The first page will not stream data from the backend. 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}

The backend is very simple. We are loading some data, wating for all of them and then sending the data to the frontend.

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=lshortong`)
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, 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 send to the frontend when they are available.

But the hugeData are not send directly. The frontend shows “Loading …” and the hugeData are received later when they are processed from the backend.

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}

In the backend you can see that we are not awaiting the hugeData. The backend sends the promise in matter instead and lets the frontend to await the hugeData.

Make sure the data awaited in the backend (i.e. minimalData and biggerData) are send at the end of return, 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`)
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: 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 JS. 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.

One solution was introduced by Geoff Rich using conditional streaming with isDataRequest check.

But the problem is that this cripples experience for all 99% of users who have JavaScript just to give not even so good experience for noJS user either. Some may even call it “degressive” enhancement.

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.

Conditionally Not Stream Data

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 the link to laod more data with a url search parameter noJS which equals to true. We are also using noscript tag (which is automatically shown only to users who do not have javascript) and checking that ther us data.hugeData promise to conditionally show this link.

<!– 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>
<div class=“jsonly”>
{#await data.hugeData}
Loading …
{:then hugeData}
{@html hugeData}
{/await}
</div>

<noscript>
<style>
.jsonly {
display: none !important;
}
</style>
{#if typeof data.hugeData.then === function}
<a href=“stream?noJS=true”>Load the rest</a>
{/if}
</noscript>

Update of the backend is quite stright forward. Get the “noJS” url search parameter. If it is true we will not send the promise but await the data.

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

You may even consider and add css feature content-visibility: auto; to render long page in chunks 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.

Leave a Reply

Your email address will not be published. Required fields are marked *