Skip to content

Performance: dropped frames on reactive graph updates #14721

@gyzerok

Description

@gyzerok

Describe the bug

We are developing a highly interactive application that displays a lot of interconnected data on the screen simultaneously. That's why our reactive graph is somewhat deep. Mostly Svelte is working amazingly for us and performance is great! However we've found one case where updating the graph taking so long, that our app start dropping frames.

All the following flamecharts are taken on a 4x CPU slowdown. That's to emulate more realistic user device as well as to make problems more visible. However without throttling there are occasional frame drops as well.

Here is a pick on how our flamechart does look like. The whole task here takes almost 78ms.
image

If we zoom in a bit, we can see lots of check_dirtiness calls.
image

This first 2 screens also got me wondering if it'd be possible to split process_deferred calls into multiple frames inside Svelte? That'd make for better scalability even on the big graphs.

And here is part of another task also taking 70ms+.
image

When zoomed it also should a lot of check_dirtiness calls.
image

Reproduction

Unfortunately making a reproduction seems to be really tricky and I am still working on it. Meanwhile I am looking for any help or suggestions on how to arrive at it. I know a high quality report would be much better and I'm looking to improve this one hopefully with your help.

Simplified our code looks somewhat like the following. We have a sorted list of items which we derive based on several fields values. Of course it's not the most efficient way to allocate an array and sort it on every change here, but it keeps a lot of things very simple (basically for the same reasons deriveds are advised over effects in docs). And I'd expect it to be performance bottleneck with large lists of items - like thousands or even tens of thousands (maybe I am wrong here). In our case frames seems to be dropped even when size is less than a hundred.

import { SvelteMap } from 'svelte/reactivity';

export class Store {
	cache: SvelteMap<number, Model> = new SvelteMap();

	constructor() {
		for (let i = 1; i <= 500; i++) {
			const model = new Model(i);
			this.cache.set(i, model);
		}
	}

	readonly all: Model[] = $derived.by(() => {
		return Array.from(this.cache.values()).sort((a, b) => b.maxTime - a.maxTime);
	});

	readonly even: Model[] = $derived.by(() => {
		return this.all.filter((model) => model.id % 2 === 0);
	});
}

export class Model {
	time: number = $state(0);

	updateTime: number | null = $state(null);

	constructor(readonly id: number) {
		this.time = Date.now();
	}

	moveUp() {
		this.updateTime = Date.now();
	}

	maxTime = $derived.by(() => {
		return Math.max(this.time, this.updateTime || 0);
	});
}
<script lang="ts">
	import type { Store } from '$lib/Store.svelte';
	import { getContext } from 'svelte';
	import { flip } from 'svelte/animate';

	const store: Store = getContext('store');
</script>

<div>
	<div>Total: {store.all.length}, Even: {store.even.length}</div>
	{#each store.all as model, i (model.id)}
		{@const next = store.all[i + 1]}
		<div animate:flip>
			{model.id.toString().padStart(4, '0')}{' '}<button onclick={() => model.moveUp()}
				>click</button
			>

			{#if !next}
				<div>that's it</div>
			{/if}
		</div>
	{/each}
</div>

Logs

No response

System Info

MacBook Pro M1 2020, 16GB RAM
Chrome 131 with 4x CPU throttling enabled

Severity

annoyance

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions