SvelteKit Patterns

I’d be thrilled if these pattern numbers became part of the SvelteKit vocabulary. Some terms like "SPA mode" are a little murky — is that talking about client-only rendering, or client-side navigation, or both? — and it would be nice to not have to re-explain some of the more exotic patterns over and over.

  1. SSR, data fetched server-side, client-side navigation
  2. SSR, data fetched server-side, traditional navigation
  3. Classic SPA, client rendered, single entry point HTML, client-side transitions, data fetched client side
  4. Pre-rendered shell SPA. Each route is pre-rendered as much as it can be, with data fetched client-side. Client-side navigation.
  5. Pre-rendered shell SPA with no client-side navigation
  6. Fully pre-rendered SPA, client navigation. SEO.
  7. Fully pre-rendered SPA, traditional navigation. No client JS.

I’m sure there’s other patterns and this list might grow a bit, but the existing numbers will never change. These come from a combination of variables:

And certainly there are hybrid patterns. You might be using pattern I but one of your data sources is a slow outlier that you want to defer to a client-side fetch. This is pattern I with a bit of pattern III thrown in. Or you might be fetching your data client-side with pattern IV, but some rarely changing data you want to bake into the pre-rendered HTML, making for a IV-VI hybrid.

One.

The default pattern. What you get if you change nothing in svelte.config.js and use no $app/env guards. First hit is primarily fetched and rendered server-side, subsequent hits go SPA style with full client rendering and client-side emulated navigation.

Two.

Above but with client navigation turned off. Why would you do this? Maybe you’re an anti-SPA religious crusader. Metrics and analytics are easier with this pattern since a top level request comes in for each page. The load experience is consistent whether you’re hitting page 1 or page N. Maybe you hate “fake” stuff like fake navigation. Perhaps you’re worried about module scope state accumulation on the client-side and want a clean slate for each page.

Example: I am working on an upcoming web app that I’m not quite ready to share yet, but it will be using this pattern with SvelteKit.

Three.

The industry default from when SPAs were new and no one had coined the word “isomorphic” yet in reference to web apps. All the server has to do is send an empty DIV and a SCRIPT tag, and rewrite all paths to point to a fallback index.html file, so it’s easy operationally. Cons of course are you have to wait for your web app to “boot up.”

Examples: Gmail, Facebook, etc. (I actually really like Facebook’s approach to loading, instead of a spinner it has an understated blank gray page.) Also anything using sveltejs/template with the --single option.

Four.

Similar to above, data is primarily fetched and rendering primarily happens client-side, but instead of a fallback page, each route gets its own pre-rendered HTML page. These pages are largely just stubs, but they can include static parts of the page like a header bar and side nav, including “current page” highlight state, and these outer parts of the page will appear instantly even on low end devices. This goes a long way to improving the experience over pattern III.

To achieve this pattern in SvelteKit is kind of a complex topic that deserves its own writeup. You’d use adapter-static and a web server that can strip off the .html extension when looking for assets, such as using the cleanUrls option in Vercel. But the real challenge is dealing with parameterized paths.

Advantages are similar to pattern III, you can serve this from any web server environment and it doesn’t have to be Node. Also this is really great for incremental adoption of Svelte. If you already have a web server, maybe it’s Java servlets and it serves a React SPA, you can include a build step that builds your SvelteKit and copies the /build folder over to your static asset directory. This way you can do one or two pages in Svelte alongside your existing app.

Example: I’m building an internal web app at Apple that’s using this pattern, in the Apple Media Products division.

Five.

This is just pattern IV with no client routing.

Story time: My most recent stint at Google I was on a frontend team that owned some of the pages in the Google Cloud Console. That thing was a truly enormous Angular app that was collapsing under its own weight, so myself and a couple other Googlers were secretly prototyping a version of it in SvelteKit. We used pattern V for this and the main reason is, gigantic and super horizontally wide apps just can’t thrive under the SPA pattern. In computer science we divide and conquer. In Service Oriented Architecture we have independent pieces connected together over the wire, each with an autonomous owner. The same organization can be applied to web apps, simply divide them on URL lines. Harry owns /foo/bar/* and Sally owns /foo/baz, and each is free to implement their pages however they like. This wouldn’t have been practical in the pattern III days because app spin-up time was prohibitive, but with Svelte and its < 1MB Javascript bundles (most pages would probably be under 300kb actually), "full page reload" isn't really a bad word anymore.

A secondary reason was that Node was banned in production at Google (at least it was in 2021), ruling out pattern II. Ironic I know since Node is based on V8.

Few people would actually need pattern V, as few people are working on a web app with millions of lines of code and hundreds of distinct monthly active code contributors. (For anyone currently at Google, I can give you some go/ links that are probably still up!)

Examples: “Project Panther” at Google

Six.

The fully pre-rendered site, as one might use for static “brochure” style sites. Maybe like Gatsby? This is pattern II but the fetching and rendering happens at build time instead of at server-side runtime. I think people call this pattern SSG.

Much like pattern III you can host this on any static web server, but like pattern I this is also good for SEO. The feel comes across as instantaneous, almost like you’re clicking between pages that live on your local hard drive. Or like you’re browsing one of the old CD-ROM based apps.

Seven.

Above but with client-side navigation turned off. This pattern requires zero JS on the client, but to be fair, pattern VI works just fine with JS turned off as well. Only with this pattern you don't even attempt to send JS to anybody, making for the lightest possible experience and leanest possible resource usage. (And fewest possible HTTP connections.)

This one primarily comes down to “feel.” If you want your site to feel like a native iOS app, pattern VI all the way. If you want it to feel like a traditional website, like say a Craigslist or an MDN, pattern VII might be a good choice.

If you do use this pattern, I’d recommend the conditional hydration trick used on the About page of the demo app, so you get HMR in development. You can also do the same thing globally with config.kit.browser.hydrate: process.env.NODE_ENV == 'development' in your svelte.config.js.

Examples: The site you’re reading now, royalbarrel.com.

Comparison table

Static parts
fetch/render
Dynamic parts
fetch/render
Navigation Fallback Adapter
I Server runtime Server runtime Client n/a adapter‑node
II Server runtime Server runtime Native n/a adapter‑node
III Client runtime Client runtime Client Yes adapter‑static
IV Build time Client runtime Client No adapter‑static
V Build time Client runtime Native Yes adapter‑static
VI Build time Build time Client Yes adapter‑static
VII Build time Build time Native No adapter‑static

Next steps

I plan to make a sample app and implement it seven ways, demonstrating each pattern in code. It'll be a real world app though, with auth, talking to a real backend, so patterns VI and VII aren't necessariy applicable. All I could do there is the static marketing-type pages.