bfcache
Nobody has claimed this yet.
- Dominant language
- JavaScript
- Stars
- 20.8k
- Forks
- 2.3k
- Avg merge
- 1d 16h
- Merged PRs (30d)
- 156
Description
Describe the problem
SvelteKit always runs any load functions that have been invalidated when you navigate, regardless of the type of navigation.
This is deliberate: caching the result of a load function and reapplying it when navigating is a bad default:
- data might be stale
- if the
datawas mutated inside the component (which we don't discourage, because there are cases where that's useful!), bugs are likely - trading memory for performance is something that should be done consciously, not automatically
Browsers don't work that way, however. When you navigate between documents using back/forward buttons, the browser will restore the entire document from the bfcache. For some apps (but definitely not all), that's desirable behaviour to emulate.
Describe the proposed solution
There's a few ways we could go about this. The simplest would be a bfcache export alongside load:
// +page.server.js
export async function load() {
const { posts } = await db.get_posts();
return { posts };
}
export const bfcache = true;
This has the virtue of simplicity, though it might be too blunt a tool (you couldn't, for example, use it to cache the returned value for a set amount of time). Exporting a function instead of a boolean seems like an obvious solution, but it doesn't really work because the client needs to know if it can reuse cached data without going back to the server.
Another possibility is to add a new method to load:
// +page.server.js
export async function load({ setTtl }) {
setTtl(Infinity);
const { posts } = await db.get_posts();
return { posts };
}
With this, it's no longer specifically related to back/forward navigations, which could be good or bad depending on your viewpoint.
A third option was suggested by @dummdidumm, and it came up because of a real world use case we're currently looking at — tie it to snapshots. The rationale is that snapshots are already related to bfcache emulation.
The use case I'm dealing with right now is that I'm using snapshots as a cheat code for infinite pagination:
<script>
import InfiniteScroller from '$lib/components/InfiniteScroller.svelte';
export let data;
let loading = false;
export const snapshot = {
capture: () => ({
data,
scroll: scroller.capture()
}),
restore: (values) => {
data = values.data;
scroller.restore(values.scroll);
}
};
</script>
<InfiniteScroller
bind:this={scroller}
items={data.items}
on:loadmore={async () => {
if (loading || !data.next) return;
loading = true;
const { items, next } = await load_more_items(data.next);
data.items = [...data.items, ...items];
data.next = next;
loading = false;
}}
>
<!-- ... -->
</InfiniteScroller>
This is neat because I can scroll down to the 400th item (loading a bunch of stuff on the way), click on it, click back, and my position will be perfectly preserved along with all the stuff I already loaded. I don't need any fancy client-side data management to achieve this, it's all just regular load functions with an API route that I call inside the loadmore event.
The trouble is that when I navigate back, I still have to call load, only for the resulting data to immediately be nuked by the restore function.
If I could express my intent to use the existing data object, that wouldn't be necessary:
export const snapshot = {
+ bfcache: true,
capture: () => ({
data,
scroll: scroller.capture()
}),
restore: (values) => {
data = values.data;
scroller.restore(values.scroll);
}
};
Combined with a way to associate snapshots with a key other than the history index (https://github.com/sveltejs/kit/issues/9313), this would give us a lot of flexibility.
It does throw up one interesting question though: are we caching the return value from load, or the data object that is produced by combining that value with the return values from parent layout load functions? If we cached data itself then it would respect any mutations that occurred, and the example above could just be this:
export const snapshot = {
bfcache: true,
capture: () => scroller.capture(),
restore: (scroll) => scroller.restore(scroll)
};
However, any data that changed in a parent layout load function would not be respected. I'm not sure which behaviour is most universally desirable/understandable.
A snapshot.bfcache boolean would have the same limitation as export const bfcache, namely that we couldn't express (for example) the desire to cache data for a set amount of time. We could make it a function or something else this time though, since it will be available to the client.
In short, I'm not sure what the right answer is, but I wanted to do a braindump while it's fresh.
Alternatives considered
No response
Importance
nice to have
Additional Information
No response
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading the issue's discussion of SvelteKit load functions, snapshots, and browser back/forward navigation. Compare the proposed export, load-method, and snapshot-based approaches, including whether cached load results or combined data should be restored. Done means agreeing on a specific API and its caching semantics, then implementing and testing that design.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- javascript
- Domain
- frontend
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Needs clarification
- Newbie friendliness
- 25/100