Performant Reactivity with Svelte-Kit


  • This short article discusses reactive statements in Svelte-Kit.
  • A performance improvement you can add to your application, if you don’t already know it.
  • Plus, a pitfall I ran into by not understanding this reactive statement feature.

For context, if you’re new to svelte, a very basic example of reactive statements

let a = 1
let b = 1
$: c = a + b; // 2

a = a + 1 would reactively update c to 3

And without reactivity, c would still be equal to 2.

Any top-level statement can be made reactive by prefixing it with the $: JS label syntax. Reactive statements run after other script code and before the component markup is rendered, whenever the values that they depend on have changed. Source

Layering Reactive Statements

“Layering” reactive statements can change when subsequent reactive statements execute. (layering is not a technical or official term)

For example, if I want to fetch some data only when my notebook_id changes, I can layer my reactive statements like this.

export let data;
$: ({notebook_id} = data)
$: { loadData(notebook_id) } // Fires when *id* changes

This code will only execute when the notebook_id value changes. In other words, unrelated changes to data, won’t cause loadData to fire

Context: A +page.svelte file can have a sibling +page.ts that exports a load function, the return value of which is available to the page via the data prop Source

For contrast, the code below, would execute loadData every time data changes, even if notebook_id has the same value

export let data;
$: { loadData(data.notebook_id) } // Fires when *data* changes

A Store Example

Here is a similar example using stores and reactive statements

A store is simply an object with a subscribe method that allows interested parties to be notified whenever the store value changes Source

In this example, we’re calling refreshEditor whenever the value prop on notebook_store changes.

import { notebook_store } from 'notebook'

export let data;
$: ({notebook_id} = data)
$: active_notebook = $notebook_store[notebook_id];
$: current_value = active_notebook?.value
$: if (current_value) {
  // Only executes when current_value changes

For contrast, the following statement would execute when any property on notebook_store updates.

$: if ($notebook_store.current_value) {

If refreshEditor is expensive (in my real-world use-case it’s updating a CodeMirror instance) we only want to run it when we need to.

Page State Management

Not realizing this was the case when I first started using Svelte-Kit, I had littered my app with statements like this. (don’t do this)

let last_notebook_id = ''
$: {
  if (data.notebook_id && last_notebook_id != data.notebook_id) {
    last_notebook_id = data.notebook_id

Aside from being much more verbose, I think this code is more error prone

When you navigate around your application, Svelte-Kit reuses existing layout and page components. Source

i.e. Svelte-Kit will re-use the page instead of destroying and recreating it. While the example above is probably okay, I often worry I’ll introduce some hard-to-find bug when I write code like this in my pages.

+Page Reactivity (Bonus)

+page.ts load functions work in a similar way on navigation.

Svelte-Kit tracks the dependencies of each load function to avoid re-running it unnecessarily during navigation. Source

The code below will re-execute during navigation if params.notebook_id has changed.

So for example, if we navigated from notebook/123 to notebook/456 the load function would execute. Because notebook_id has changed.

But going from notebook/456 to notebook/456/child-route would not re-execute.

export const load = ({ params }) => {
    return {
        notebook_id: params.notebook_id,
        streamed: {
            notes: fetchNotes(params.notebook_id)

This works without any sort of “layering”. In other words, this executes on navigation when notebook_id changes. Not when any params property changes


More Stuff by Me

Sun May 21 2023