Building portable web apps with SvelteKit’s new single-file bundle strategy and hash router
Iâve been building static single page apps using adapter-static for a while now, and I love how simple the deployment story is â just upload a folder of files and youâre done! If you are unfamiliar with this adapter, it compiles down your app to a folder of HTML, CSS and JavaScript files.
A couple of months ago, the SvelteKit maintainers added two features that really caught my attention. They were actually both part of Advent of Svelte in 2024. With these new features, we can make our SvelteKit projects way more flexible. For example, you can make a project run without any web server at all, meaning you can send your whole SvelteKit app to someone as a single HTML file, have them double click it and have it all work out of the box.
Let me show you how I applied these features to an existing project of mine.
Prefer to watch a video? See this:
The new âsingleâ bundle strategy
Before we had the bundle strategy feature, there was really no way to tell SvelteKit how we wanted it to split the JavaScript bundles for our project. SvelteKit would just do its thing and create a bunch of different JavaScript files that would load as you navigated around.
Now we have three different options to choose from in our svelte.config.ts
file. The default âsplitâ option is what weâve always had â it splits the app up into multiple JavaScript and CSS files so theyâre loaded lazily as the user navigates. This is usually what you want because you donât want users loading every component in your app just because they hit one page.
Then thereâs âsingleâ, which creates just one JavaScript bundle and one CSS file, which is something people have been asking for for a long.
But thereâs also the new âinlineâ option. This one takes all your JavaScript and CSS and inlines it directly into the HTML file. I didnât see many people asking for this specifically, but it made me curious about whether it would be possible to build a completely self-contained SvelteKit app!
The new âhashâ router
The second addition was the hash router configuration. Before this, there was basically only one way to route things in SvelteKit â using normal paths like /about
or /settings
. But now we can use hash routing, which means paths like /#/about
and /#/settings
work just the same.
This might seem like a small change, but itâs actually quite impactful. The thing about hash parameters is that theyâre client-only â the server doesnât even see them. That means you can run your app without any server-side routing configuration at all. However, this also means that server-side rendering and prerendering are disabled. In fact, you canât even have any +server.ts
files in your project anymore if you use the hash router, so beware!
You configure the hash router by setting the router option to hash
in svelte.config.ts
Testing it on a real project
The project I decided to test these features on is called DeriVault. Itâs a deterministic password manager I built that doesnât require any connection to the internet. I figured it would be perfect for this experiment since itâs already designed to work completely offline.
Let me walk you through what happened when I added these features. First, I added the bundle strategy to my svelte.config.ts
:
const config = {
preprocess: vitePreprocess(),
kit: {
output: {
bundleStrategy: 'inline'
},
adapter: adapter({
fallback: 'index.html'
})
}
};
When I built the site with this configuration, instead of having all these different JavaScript files in my _app/immutable/chunks
folder, everything was instead inlined into the index.html
file!
Making the hash router work
Adding the hash router is also simple, we add this key to config.kit
in svelte.config.ts
:
router: {
type: 'hash'
}
One challenge was refactoring my navigation. I had to go through and change every call to goto
from something like `goto(â/vaultâ)` to `goto(â#/vaultâ)`.
The trickiest part was handling query parameters. When you have a URL like #/add?edit=1
, edit is technically not a query parameter anymore because everything after the hash is just a fragment. So I wrote this little utility function to handle this:
export function getHashParams(url) {
const urlObj = typeof url === 'string' ? new URL(url) : url;
let hashParams = new URLSearchParams();
if (urlObj.hash && urlObj.hash.includes('?')) {
const queryString = urlObj.hash.substring(urlObj.hash.indexOf('?') + 1);
if (queryString) {
hashParams = new URLSearchParams(queryString);
}
}
return hashParams;
}
Then in my load functions, instead of using url.searchParams.get('edit')
, I had to use getHashParams(url).get('edit')
. The big benefit was that I didnât have to change my route structure â I could keep using my filesystem routes and route parameters just like before.
Putting it all together
After making all these changes and building the project, I wanted to see if it really worked. So I deleted everything from the build folder except the `index.html` file. Then I double-clicked it.


And it worked! The whole app loaded up, I could navigate between routes, add passwords, everything worked just like it did when served from a web server. I was honestly amazed that an entire SvelteKit app could run from a single HTML file opened directly in the browser.
When would you actually use this?
Now you might be wondering when youâd actually want to use these features. For the inline bundle strategy, I can think of a few scenarios. You could build tools that need to work completely offline and can be shared as a single file. You could send someone an interactive tool as an email attachment (though watch the file size!). You could save apps on USB drives or deploy them to devices with limited file systems.
The single bundle strategy (without inlining) has its own use cases. If youâre building Capacitor apps, they traditionally havenât been able to use HTTP/2 multiplexing effectively, so having fewer files to load can improve the load performance.
The hash router is particularly useful when you canât control the web server configuration. GitHub Pages is a great example â itâs difficult to set up proper routing for a SPA there, but with hash routing you donât need to.
Some things to keep in mind
These features are really cool, but theyâre not for every project. The inline strategy means everyone downloads everything upfront, which goes against the whole idea of code splitting. You also lose server-side rendering with the hash router, which affects SEO.
But for the right use cases, these features make the DX so much better.
What do you think? Have you tried these features yet? Iâd love to hear what kind of projects youâd build with a single-file SvelteKit app!
Social image by Walls.io on Unsplash
View Comments
How to fix performance issues in your Svelte application using svelte-render-scan
If youâre working on web applications today, you might have heard that re-renders are...