Migrating my sites from SvelteKit to F#

Date: 2023-12-18 | create | tech | shares | sveltekit | fsharp |

For the past several months I've been researching and experimenting with server-side rendering paradigms in F#. The recent server-side rendering trend (enhanced with things like Alpine and HTMX) has been interesting to me as it feels like a step forward for the already amazing Monolithic architecture - this time allowing you to reasonably move from 2 monoliths down to 1 for most applications.

Some recent adventures in server-side rendering with F#:

In the last few weeks I decided that I liked this new approach and my experiments at least proved that this wasn't an absolutely terrible idea (though ofc time will tell). So I migrated my site (this one!) over to server-side rendering on an F# / Giraffe server.

In this post we'll explore some of the results from moving from my old stack (SvelteKit) to my new stack (F# / Giraffe).

Q: What were the results of moving from SvelteKit to F# / Giraffe?

Answer

My F# site is shiny and new and over-engineered and thus shows significant performance improvements of ~3x in both dev and prod with payloads ~75% smaller than the same pages rendered in SvelteKit.

In terms of Devx I think F# has an edge if mostly due to its smaller number of components / packages to wrangle and stricter type safety leading to less worry that I missed an edge case somewhere.

In the rest of this post we'll compare my new F# site with my old SvelteKit site from several dimensions:

Code

Overall, the codebase size as measured in Lines of Code (LoC) from SvelteKit to F# remained roughly similar. That said the composition of these lines changed significantly with F# boasting fewer files and comments.

Are comments a proxy of complexity? Maybe. Perhaps also a lack of expressiveness.

I'm not surprised by this result as I actually admire both technologies for being simple and straightforward compared to their peers which IME leads to less code bloat overall. This is one of the big reasons SvelteKit's been my frontend framework of choice for the past few years. If I was switching from say a React app I'd expect a lot more savings in terms of LoC.

Okay, okay that might not be fair to React but I am a bit of a React hater.

SvelteKit

  • Lots of JS configuration files -> More code
  • Routing done in file system rather than handlers -> Less code, more files
  • Layouts built via file system -> Less code, more files

F#

  • Stricter type system -> Less code, less files (longer files split by module is easier to handle than in other languages)
  • Routing done manually -> More code, less files
  • Layouts done manually -> More code, less files

SvelteKit vs F# - Code Composition

SvelteKit vs F# - Code Composition

SvelteKit vs F# - Lines of Code

SvelteKit vs F# - Lines of Code

  • SvelteKit - 50 files, 1940 LoC
  • F# - 20 files, 1844 LoC

Page Performance

In this section we'll compare my SvelteKit and F# sites in terms of page performance.

SvelteKit is often lauded as a fast JS framework. And it is quite fast.

But JS fast and F# fast are often two different beasts - even with your plethora of newly-minted, Rust-backed prototypes. They will likely one day catch up but that day is not this day.

I ran some naive benchmarks across several pages I felt were representative of the kinds of pages on my site - both to sanity check that my new site wouldn't keel over as soon as I launched it and to check if the migration work had the perf upsides I'd predicted.

In my naive benchmarks, my F# site loaded far faster than my SvelteKit site even though they were presenting essentially the same site (it looks the same to me w same styles and page content). F# seems to be rendering its pages faster and sending down much smaller payloads (in much fewer requests) than SvelteKit.

  • Page Load Speed: F# - 2-5x faster
  • Page Load Size: F# - often 75% smaller (tho SvelteKit cached smaller sometimes)

Caveat: All benchmarks have asterisks so don't put too much weight into these numbers.

Benchmark Setup

  • Load various pages on my site locally in an incognito browser
    • NoCache - Use CTRL+SHIFT+R to force reload of all assets
    • Cache - Normal CTRL+R
  • Measuring - Chrome dev tools for full page load and payload transferred
  • N <= 5 - For the most part I did a single measurement but if something looked weird, I took a few and took the ~median. Read: SMALL N STUDY

hamy.xyz - Project Page

Benchmark - Projects Page - https://hamy.xyz/projects

F#

  • NoCache
    • Load speed - 77ms (2.4x faster than SvelteKit)
    • Page size / Download size (network, total) - 100kb (75% smaller than SvelteKit)
  • Cache
    • Load speed - 33ms (5.7x faster than SvelteKit)
    • Page size / Download size (network, total) - 35kb (15x LARGER than SvelteKit)

SvelteKit

  • NoCache
    • Load speed - 188ms
    • Page size / Download size (network, total) - 399kb
  • Cache
    • Load speed - 131ms
    • Page size / Download size (network, total) - 2.3kb

My Projects page is essentially a large HTML table. I was curious to test this one because it's one of the few pages on my site which consists of a large blob of hand-written HTML (as opposed to mostly dynamic content in the form of my blog posts etc).

For F# the size of this HTML table was ~35kb. Other assets (mostly css) make up the other 65kb.

Idk why SvelteKit is sending 400kb for this - that seems like a lot. But I am impressed that on reloads it was able to limit it to ~2kb. This means it must be using some good caching mechanisms to make up for the initial load time - which I guess is what SPAs are supposed to be good at. The long load speed is puzzling but I think this has something to do with the large number of requests SvelteKit emits to get its chunks which takes a long time to resolve even though the actual transferred payload is quite small.

-> This makes me think there may be some opportunity for me to more aggressively cache some of my pages client-side since they don't change very often.

hamy.xyz list page

Benchmark - List Page - https://hamy.xyz/labs

F#

  • NoCache
    • Load speed - 55ms (6.5x faster than SvelteKit)
    • Page size / Download size (network, total) - 79kb (80% smaller than SvelteKit)
  • Cache
    • Load speed - 49ms (7.7x faster than SvelteKit)
    • Page size / Download size (network, total) - 15kb (80% smaller than SvelteKit)

SvelteKit

  • NoCache
    • Load speed - 361ms
    • Page size / Download size (network, total) - 475kb
  • Cache
    • Load speed - 379ms
    • Page size / Download size (network, total) - 86kb

My List Page is how I dynamically list all my blog posts in an ordered, paginated fashion. It's a good page to test because it powers navigation for my Blog, Labs, and Tags features.

In this case again SvelteKit is sending down a very large initial payload but interestingly it also sends down a large payload on cached loads.

hamytodo - screenshot the huge amount of post requests

My hypothesis is that a lot of this is due to how I dynamically loaded my markdown posts (read: not well). My Node has never been that good and so you can imagine that my SvelteKit / Vite was worse with less time to fail and learn. It seems that the way I was generating my pages led to lots of empty requests from the client for all my blog posts - like blog posts that shouldn't show up on this page were still getting requested with empty payloads.

Idk why this happened and really should just be chalked up to me coding bad rather than SvelteKit being slow. But at the same time I built my F# site with dynamic markdown loading fine without this issue so take that as you will.

hamy.xyz - tags page

Benchmark - Tags Page - https://hamy.xyz/blog/tags/create

F#

  • NoCache
    • Load speed - 74ms (5x faster than SvelteKit)
    • Page size / Download size (network, total) - 80kb (80% smaller than SvelteKit)
  • Cache
    • Load speed - 61ms (7.5x faster than SvelteKit)
    • Page size / Download size (network, total) - 15kb (80% smaller than SvelteKit)

SvelteKit

  • NoCache
    • Load speed - 384ms
    • Page size / Download size (network, total) - 498kb
  • Cache
    • Load speed - 463ms
    • Page size / Download size (network, total) - 87kb

The Tags page is a good page to test because it's arguably the most resource-intensive operation aside from markdown parsing. Here we have to get all the posts for a given tag and present them in sorted order with pagination.

I'll note that my new F# tags engine is much more optimized than my SvelteKit one as it precomputes and caches this on startup as opposed to dynamically figuring this out on request. This I think explains the load speed differences but still we've got massively larger payloads which it doesn't really explain.

hamy.xyz - post page

Benchmark - Basic Post - https://hamy.xyz/blog/2023-09-nobody-likes-ties

F#

  • NoCache
    • Load speed - 56ms (5x faster than SvelteKit)
    • Page size / Download size (network, total) - 71kb (80% smaller than SvelteKit)
  • Cache
    • Load speed - 51ms (4.5x faster than SvelteKit)
    • Page size / Download size (network, total) - 7kb (75% LARGER than SvelteKit)

SvelteKit

  • NoCache
    • Load speed - 294ms
    • Page size / Download size (network, total) - 388kb
  • Cache
    • Load speed - 234ms
    • Page size / Download size (network, total) - 4kb

Finally we get to a post page which is important because my site is all about posts - sharing my words with others. If this page sucks then the whole rest of the site is useless. I chose this post in particular because it doesn't have any external media which would contaminate my benchmark with extraneous factors.

This page is arguably the most resource intensive in terms of markdown parsing due to its need to load the full content. As expected F# runs much faster with a smaller initial payload. But again we see that SvelteKit returns an impressively small payload on rerenders which beats out even F#'s minimal HTML.

Production Metrics

The previous benchmarks were all run locally on my machine - in dev you might say. But as we all know dev != prod (and I believe you should act like it). So in this section I wanted to record some prod metrics to get more of a feel for how these two sites perform in the real world.

These metrics were pulled from the GCloud Cloud Run dashboard where I host my sites (old and new). I've used Cloud Run serverless containers as my primary hosting method for the past several years and really liked it which is why it's part of my 2023 core tech stack.

For an idea of scale, my sites get about 5k views a month which comes out to about 166 views per day or 0.002 requests / s. So like no load really. But hey - that's real life. Despite what your Software Engineering interviews might lead you to believe, most systems don't have much load either.

Request Latencies

  • SvelteKit - p95 - 30 ms
  • F# - p95 - 10 ms (3x faster than SvelteKit)

SvelteKit vs F# - Request Latencies

CPU Utilization

  • SvelteKit - 1%
  • F# - 1%

We can see that neither SvelteKit or F# are trying very hard - cause my sites just don't get that much load.

SvelteKit vs F# - CPU Utilization

Memory Utilization

  • SvelteKit - ~28%
  • F# - ~35%

It makes sense that F# / dotnet use more memory because they're little memory hogs. But typically this comes with the tradeoff that they're faster.

SvelteKit vs F# - Memory Utilization

Startup Latency

  • SvelteKit - 3s
  • F# - 7s

F# / dotnet is just slower to startup than SvelteKit / Node. This is a common tradeoff between dynamic / compiled languages - dynamic starts faster but is slower long-term (assuming a long enough time horizon).

SvelteKit vs F# - Startup Latency

Next

Overall I'm very happy with my new F# site. It allows me to play more in a language I enjoy and I think has some sizable devx / performance advantages even though it doesn't really matter for my sites.

I've enjoyed the fullstack F# experience so much that I've also decided to migrate CloudSeed - my F# project boilerplate - over to this stack. I use CloudSeed as my base template for starting most of my projects so it makes sense for it to migrate along with me.

If you liked this post you might also like:

Want more like this?

The best / easiest way to support my work is by subscribing for future updates and sharing with your network.