Auroratide

Coding + Storytelling

Server Side Rendering a Random Number

Web Dev

Topics
  • svelte
  • ssr
  • javascript
  • html
  • code

Have you ever tried rendering a random number with Svelte before? Here's a rather quick implementation:

<script>
	const n = Math.random()
</script>

<p>{n}</p>

Seems like nothing can go wrong, right? It even works if you plug it into the REPL. But as soon you plug it into SvelteKit, Svelte's website framework, oh dear what's happening?

When 'Reload' is clicked, two random numbers flash on the screen instead of one.
When the page is refreshed, a number shows briefly before being replaced

The footage here is slowed down, but for some reason two random numbers get shown when the page loads, the second one rapidly replacing the first 😥. What's going on, and how do we fix this?

A similar issue occurs when using NextJS.

You won't get two flashing numbers. Instead, the console displays a nasty red error stating, "Warning: Text content did not match." This is definitely better since the site visitor won't see that, but it's still an issue, and it can lead to further problems if not addressed.

Essentially, the random number is being generated twice: once on the server, and once on the client computer. This is because some frontend frameworks, SvelteKit included, practice something called server-side rendering (SSR):

  1. The server runs the framework's Javascript code to generate nice juicy HTML, and the result is sent to the browser to render.
  2. The browser renders the result, and begins downloading the Javascript code.
  3. Once downloaded, the framework hydrates the existing HTML with the Javascript, making the page interactive.
Step 1: JS on server becomes HTML; Step 2: HTML goes to client; Step 3: JS hydrates on client
Flow diagram illustrating server-side rendering

The hydration step is key to understanding the problem with randomness.

Imagine for a moment that you just purchased a new flashlight. Dang, it sure is nice that you don't have to build one yourself! Unfortunately, the flashlight does not come with batteries, so you have to install those before you can light up your day. To help you out, the shipment provides instructions on where the batteries should go:

A flashlight on the left contrasted with a different kind of flashlight on the right, the right demonstrating where the battery goes
What you got does not match the provided instructions

What gives?! The instructions don't match the flashlight, so where am I supposed to put the batteries? How do I hydrate my flashlight?

</analogy>

When HTML is rendered server-side, it is not yet interactive; it only becomes interactive once Javascript has been applied. The process of applying interaction to server-rendered content is called hydration, and it relies on one key assumption:

The results of rendering on the server and the client should match.

In our flashlight analogy, when the instructions don't match the product, we get confused. Similarly, if client-side Svelte expects there to be a button on which to attach an event, and the server failed to render that button, then Svelte has to make a decision.

In the case of our random number, Svelte decides to replace whatever's there with what it thinks should be there. Since hydration takes time to do, we get a flash of two numbers.

Step 1: JS on server converts a random number 2 into HTML; Step 2: HTML goes to client; Step 3: JS on client replaces HTML with a newly random number 7
How the random number scenario results from SSR

If the problem is that the server and client are generating different numbers, then we need a way for them to generate the same numbers instead. In other words:

The server must send enough info for the client to generate the same numbers.

In particular, we can take advantage of the fact that random number generators are not actually random. In fact, they are pseudo-random, relying on well-defined algorithms to generate a pattern that is so chaotic that it's generally good enough.

Many such pseudo-random number generators take a seed as an input and use that seed to deterministically create an entire sequence of numbers. The key insight is that using the same seed always results in the same sequence of numbers.

So step by step:

  1. The server generates a single random seed and uses that to generate HTML
  2. That seed is sent to the client along with the HTML
  3. The client initializes its own random number generator with the seed, ensuring the exact same sequence of random numbers are generated
  4. 💰💰💰

Step 2 differs in implementation from framework to framework, but let's see an example in SvelteKit (since that's what I was using when I ran into this issue).

In order to send the seed to the client consistently, we can rely on one of the properties of SvelteKit's special fetch function:

It makes a copy of the response when you use it, and then sends it embedded in the initial page load for hydration

In other words, when Svelte makes a fetch on the server, rather than forcing the client to do the same fetch, it caches the result and sends it to the client, saving a lot of time.

Which means, if we have an endpoint that generates a random seed, we can guarantee the client sees the same seed!

// random-seeds.js
export const get = async () => ({
	body: {
		// use your favorite algorithm for this
		seed: generateRandomString(),
	},
})

We want the seed to be available on all pages, so we can fetch it within the primary layout.

<!-- __layout.svelte -->
<script context="module">
	export const load = async ({ fetch }) => {
		// fetch's result will be cached for the client
		const seed = await fetch('/random-seeds')
			.then(res => res.json())
			.then(json => json.seed)
			.catch(() => '') // this shouldn't break the app

		return {
			props: {
				seed,
			},
		}
	}
</script>

Interestingly, Javascript's native random() function cannot be seeded. Therefore, we have to find or roll out our own random number generator which can be seeded.

The seedrandom library by David Bau is very good for this, or if you want more control over what's bundled, feel free to choose from this list of pseudorandom number generators.

// lib/random.js
import seedrandom from 'seedrandom'

export const seeded = (seed) => seedrandom(seed)

export const usingMath = () => Math.random

We want the random number generator to be available everywhere in the app that it's needed without having to send it several layers deep via props. Svelte's context api is very useful here, since the __layout.svelte is the root of every page.

// also lib/random.js
import { getContext, setContext } from 'svelte'

const key = Symbol()
export const generator = () => (getContext(key) ?? usingMath())()
export const setGenerator = (generator) => setContext(key, generator)

We can now initialize the generator in the layout where we had previously fetched the seed to use.

<!-- __layout.svelte -->
<script>
	import { setGenerator, seeded, usingMath } from '$lib/random'

	export let seed = ''

	setGenerator(seed.length > 0 ? seeded(seed) : usingMath())
</script>

<slot></slot>

And finally, we can use all this infrastructure to get a random number that is the same on both the server and client!

<!-- index.svelte -->
<script>
	import { nextRandom } from '$lib/random'

	const n = nextRandom()
</script>

<p>{n}</p>

The approach I took above was perfect for my use case in which the random numbers were used solely for aesthetics. If, however, you need random numbers for something security-related, definitely double check whether this approach fits within your constraints!

Additionally, the solution presented here works iff the server and client generate content in the same order. You could imagine that if the client built the page backward, the same sequence of random numbers would be generated but they'd be applied to different pieces of the page. I'm not sure why this would happen, but if this is a significant factor for you, then it requires a more nuanced approach.

As usual, here's a repo to explore the raw solution. Hopefully it is helpful!

SvelteKit SSR Repo

  • If you are using a framework with server-side rendering, thoroughly examine and test places where it is possible for the server and client results to differ, such as with random numbers or login state.
  • Where consistency is desired, the server must send enough info for the client to replicate the results exactly.

And a bonus tip:

  • When you get stuck, ask for help! I was scratching my head for a while before asking the folks at Stack Overflow, and as a result I learned something new about how SvelteKit works.