kit icon indicating copy to clipboard operation
kit copied to clipboard

Snapshot restore is slower than child component functions

Open half-metal opened this issue 3 months ago • 3 comments

Describe the problem

Restoring snapshots from the parent page of a component has a race condition. A remote function in a child component may run before the snapshot restore occurs. Unfortunately, snapshot.restore does not have a .ready property like remote functions do, which means a remote function in a child component can run before the snapshot.restore happens.

I created this project with async and remote flags turned on to demonstrate https://github.com/half-metal/restore-with-async

You'll see something like this in the console log PARENT PAGE - capture posts (6) [{…}, {…}, {…}, {…}, {…}, {…}] CHILD - running getPosts() in child component PARENT PAGE +page.svelte:15 restore value {posts: Array(6)}

I can manually create a property like 'isReady' and if posts is zero or do a timeout, but it seems hacky, and I notice some edge cases where I need a timeout. I can also do a global state to track restoring for certain page, but that gets ugly too. I tried setting restore to an async function and even had await, but that did nothing.

Snapshot restore is crucial to modern websites, and avoid wasteful API calls that you already have data for when navigating away and back and easily scrolling to where the user was at. This gets messy when you have infinite scrolling, cursor pagination, and if the snapshot restore function is too slow.

Describe the proposed solution

You could have a snapshot.restore.ready property, but maybe a better option is some setting on the restore function that indicates not to load the child component until it's ready.

I'm also confused why snapshot.restore wouldn't be instant like the first thing to happen. Again I do have kit set to async because my app has remote functions.

Importance

would make my life easier

half-metal avatar Oct 13 '25 19:10 half-metal

Also, using tick().then seems to be the best solution I've been able to figure out to ensure the restore function happens before attempting to scroll. To me though, that is not intuitive and not obvious in the documentation this would be needed. As an example of a snapshot with restore:

	export const snapshot = {
		capture: () => {
			return {
				tour: $state.snapshot(tour),
				scrollY: scrollY,
				tourId: tourId  // Store tourId to verify on restore
			};
		},
		restore: (value) => {
			console.log('🔄 Tour view snapshot restore - value:', value, 'current tourId:', tourId);

			// Only restore if snapshot is for the same tour
			if (value && value.tourId === tourId && value.tour?.id) {
				console.log('✅ Restoring tour view snapshot for:', tourId);

				// Restore scroll position
				scrollY = value.scrollY || 0;

				// Restore tour data
				tour = value.tour;

				// Mark as ready since we restored from snapshot
				isReady = true;
			} else {
				console.log('❌ No valid snapshot for this tour');
			}
		}
	};

You could then use tick since queueMicrotask is too early

tick().then(async () => {
		if (!isReady && !tour) {
			// No snapshot was restored, fetch fresh data
			console.log('📡 Fetching fresh tour data (no snapshot restored)...');
			try {
				tour = await getTour(tourId);
				console.log('✅ Tour fetched:', tour);
				error = null;
			} catch (err) {
				error = err;
				tour = null;
			}
			isReady = true;
		}
	});

Then having some scroll effect

// Watch for scrollY changes from snapshot restore
	$effect(() => {
		if (scrollY > 0 && !hasRestoredScroll) {
			console.log('🔄 Tour scroll restoration triggered, scrollY:', scrollY);
			hasRestoredScroll = true;
			// Use requestAnimationFrame to ensure DOM is ready
			requestAnimationFrame(() => {
				window.scrollTo({ top: scrollY, behavior: 'instant' });
			});
		}
	});

And if you are changing items in the child component then you either need to bind those properties or bind the component. I think having a Svelte snapshot with capture, restore, some additional property/function that can handle this, would make less boilerplate for a very common scenario - restoring scroll.

half-metal avatar Oct 14 '25 15:10 half-metal

I recently ran into this with a paginated list where we snapshot the current list of items, the current page, total count, and loading flags. When navigating back, snapshot.restore correctly restores the state, but onMount still fires first and triggers a fresh fetch with currentPage === 0. That fetch later resolves and overwrites the restored state.

So the issue isn’t only that onMount runs before restore, but that async work kicked off in onMount can complete after restore and overwrite the restored data. This makes it tricky to reliably restore paginated UI state without guarding against stale async requests.

Would be helpful if the docs clarified this race condition, since it affects any component that triggers async loads on mount while also using snapshots for data retention, not just scroll / DOM state.

AlexVialaBellander avatar Nov 27 '25 10:11 AlexVialaBellander

Yeah, onMount can happen and overwrite the snapshot restore.

Also, based on recent Sveltekit updates - you can't even reliably use tick() anymore. I can use afterNavigate but it's visibly slower than tick() - I suspect it's due to one of these PR

https://github.com/sveltejs/kit/pull/14800 https://github.com/sveltejs/kit/pull/14644

I tried settled().then(() => { but that doesn't work. So I have reverted from Sveltekit "@sveltejs/kit": "^2.49.0" to "@sveltejs/kit": "2.47.3"

Seems like a bug now with async.

half-metal avatar Dec 01 '25 23:12 half-metal