I was debugging a strange problem recently in our SvelteKit application – users would get stuck in a state where the page kept refreshing itself over and over in a loop. Closing the browser and/or clearing the cache was the only way to fix the problem.
After some time I finally managed to reproduce the issue. It turns out that this happens after you deploy a new version of your application while a user has an old version open in their browser.
When you deploy a new version all of your asset paths (CSS & JavaScript) will change due to the way Vite and Rollup work. The file names of bundled assets have a unique hash in their names that represent the content of the file.
If a user has the old version open and tries to navigate to a new page this will trigger a client side navigation that usually requires the loading of new JavaScript assets from the server. But since you deployed a new version the old assets are no longer present and will return a 404 error. There is a built in functionality in SvelteKit where it will detect that the asset failed to load and perform a hard refresh of the page.
This is where our issue start. If the HTML response of the page itself is cached in the browser, it will still refer to the now-old asset paths that no longer exist. This triggers a refresh of the page, and the loop has started.
This happens when we don’t pass any cache headers from our application to the browser. The browser will the pick an arbitrary time to cache our content. This is usually not desired behavior.
The solution
We need to set an explicit no-cache
header for responses to HTML files coming from your app. You can do this for most adapters in hooks.server.ts
using this code:
import type { Handle } from '@sveltejs/kit';
export const handle = (async ({ event, resolve }) => {
const response = await resolve(event);
response.headers.set('cache-control', 'no-cache');
return response;
}) satisfies Handle;
In addition to this, we also need to make sure that our +layout.ts
in src/routes
file contains the following line:
export const prerender = false;
This is because SvelteKit by default will attempt to prerender the routes and if they are prerendered then they will be considered completely static and won’t even run through our hooks.server.ts
logic! This is documented but somewhat odd behavior in the context of adapter-node
.