The Architecture Behind jacksonsalopek.com
All of the source code is available on GitHub.
I've been on a bit of a soulsearching journey for the past few months, figuring out which language I wanted to learn to write future webservers in, as I believe that each language has its performance benefits and tradeoffs (oh so controversial). However, any language is capable of writing a decent webserver, each with their quirks. Anecdotally, the best requests per second I measured came from a Zig + H2O executable, compiled and run on my M1 Macbook Air. Performance may have been great, but the developer experience was poor due to having to manage memory and generally write more code than other languages given that there are virtually no solutions for templating so one must be handrolled as well as having to interface H2O's C with Zig. Zap for Zig is a good alternative, reducing the need for C header calls (as there is no H2O to interface with) but with a slight performance hit. I didn't feel like relearning Rust for the sake of using Leptos, which seems like a pleasant DX if you enjoy writing Rust.
I write Java and TypeScript for my dayjob at Spotify so I didn't want to write as much of those as I would usually. Avoiding a JavaScript framework like React or SolidJS was one of my primary goals -- I had recently explored htmx as a part of my work on Skintracker and absolutely loved it. It made me feel like I was back in the days of jQuery and Bootstrap was barely even a thing yet. Sometimes, it feels like adding massive overcomplexity to use all that JavaScript to make what's actually a very simple application. I had to setup webservers using IIS back in the day which served static HTML files, and this felt significantly better than that while still hearkening to its simplicity.
Finally, I settled on Scala as my language of choice, as I like its approach to functional programming. A website like this, which consists of static assets, a home page featuring a Three.js animation, and a blog, is not very complex in comparison to other production code which I've written before. Given the low complexity, I figured that whatever allowed me to write the least amount of code would be best. There are other functional programming languages like OCaml, but I found that Dream's performance on my machine to be lacking in comparison to the host of other frameworks that I've demoed. OCaml feels a bit too illegible to me, and I wanted the code to be readable in say a year's time when I decide to update this site. I also enjoyed the ease of interop between Java and Scala, as I really wanted to use ActiveJ over a similar HTTP platform like Spring, since the benchmarks were quite enticing.
Java has seen significant performance improvements with Java 21, as the concurrency and threading model has evolved significantly. You might be thinking, "Why would you need threading for this?" Well, you don't but I wanted to flex.
Setting up a toolchain that allowed for:
- Scala 3.x
- Java 21 on GraalVM
- SCSS/TS transpilation
- HTML templating
- Markdown -> HTML rendering
- HTMX integration
... was already becoming a seemingly immense challenge -- why?
I had never written Scala before.
Despite this, I found SBT to be very easy to work with (in contrast with Bazel), and setting up the toolchain and dependencies was very straightforward. I also found that one can execute shell commands as tasks, which allowed me to setup a shell script to precompile/transpile (using Bun and grass) my web-based languages (such as SCSS and TypeScript) to their respective CSS and JS equivalents as a part of my application build process with SBT. I use Neovim as my primary editor, and nvim-metals provides incredible LSP features which made development all that much easier.
After poking around various repositories such as Maven Central and reading a ton of Medium articles, I had found Play's Twirl templating engine to be a very good match for me as well as spray-json for reading the blog's manifest into a case class. The combination of the two (seemingly) allowed for a very easy time when creating a template for something like the blog page. As for the posts, I wanted to write Markdown and render that into HTML since it needed to be displayed on the web. For this task, I chose txtmark due to its incredible benchmarks, even if it may be old. Upgrading to the latest and greatest isn't always worth it. After having chosen all of my dependencies, I decided to run more performance tests against the stack locally which yielded RPS of >135k, even including HTML templating! For reference, Skintracker templates using JSX and runs on Bun + Elysia, achieving roughly 75k RPS, which is not bad but gets smoked by this stack.
Everything was going amazingly!
Then, I tried to containerize the app.
And that's where it seemed like it was all going to fall apart. The scala-sbt
Docker image was dynamically linked and Alpine was expecting statically-linked
musl C, but that was impossible to do since the scala-sbt image did not have
musl since it was based on Oracle Linux nor did it have a package manager to
install utilities like curl to get musl. To fix this, I switched to a
debian:stable-slim
base and gave up on the dream of a micro-image on Alpine
(for now).
This worked for a bit, but broke when attempting to generate a fallback image.
Adding the --no-fallback
flag to native-image
resolved this, and also
exposed a ton of issues with the build. Reflection configuration was straightforward
to resolve, but I had a bit of trouble with attempting to get the application to
run properly as a binary when visiting /blog. The culprit was spray-json, as
its build was not compatible with native-image
, but was swiftly fixed by switching
to jsoniter (which also reduced manifest parsing code complexity immensely).
Finally, resources were not compiled as part of the binary properly -- adding
a resource configuration to the compiler flags solved this. @eduard-vasinskyi
was super helpful in giving me guidance when it came to resolving the prior
issue as well as assisting with configuring Logback with native-image
.
Final Stack, Tooling & Dependencies:
- Scala 3
- Java 21 GraalVM
- SBT
- ActiveJ
- txtmark
- jsoniter
Front-end:
- playframework/twirl
- htmx
- grass (SCSS -> CSS transpilation)
- Bun (TS -> JS transpilation)
I'm pretty pleased with this stack and am curious how far I can push it in the future. I also really want to get this built for Alpine, so look forward to a follow-up!
Thanks for reading!
-- Jackson