When Rust is bad (and the "problem" with async)
I recently wanted to update my website, as one does. I wanted to make it in Rust. I thought it was a good fit. I used SvelteKit previously, which I really enjoy, but I don't want to change my website much and I want it to keep running with low overhead.
So I started coding. I am familiar with the SvelteKit way of doing things so I tried to do that in Rust. From the "frontend frameworks" type of thing, the most familiar and best seemed like Leptos, so I used it. I started making my website and it was... fine. But after a while it stopped being that fine. There was a lot of overhead, a lot of dependencies, took sooo long to compile and it was like 10GB in my directory, so much ugly error handling and nesting HTML tags everywhere, macros that didn't show me compiler errors for 3 seconds at a time and it just felt so... bad.
...is Rust actually bad for some things? 😱🤯
I started thinking about it more. I realized, I'm making a sub-optimal website. The whole point of using Rust for me is to be able to make everything nice and efficient and lean, but Leptos uses WASM, which is generally bigger and slower than just using javascript in the browser. It definitely isn't a silver lining. If I'm going to make something sub-optimal, might as well use any Javascript framework, right? In fact, I'm pretty sure using SvelteKit would result in a better website and a better authoring experience.
Why I wanted to use Rust
In my first pass using Rust I was able to do a couple of things that I really enjoyed. Firstly, it was much nicer to parse and modify markdown into HTML. I wanted to do some modification to show math stuff and I was able to just... use KaTeX and to the things I needed to make it work. I didn't need any plugins or any weird support, and it's all type safe and I'm confident it will work practically forever.
I also had ambitions of making something cool. I wanted to host the site on my own server, where I have the plaintext of my blog synced with Syncthing, so my idea was that the server would always serve the latest version of the blog post. I wanted to have SSR and in fact I wanted to prerender everything but update the rendering dynammically when the blog contents changed. And this seemed possible in Leptos!
The flexibility of being able to implement these two things really intrigued me. I'm sure it's ultimately possible with the JS frameworks but tbh it seems way harder. I did give it a fair go with SvelteKit! But it seemed like a pain. In Rust, you can "just do it". But I realized that you can't "just do it" in Leptos. And, wait, why am I using Leptos again?
Spelling out the problem
I realized that my problem, of course, wasn't Rust. It wasn't even Leptos. It was a sneaky mutant of premature optimization. I didn't need all the things that Leptos provides. I just carried my knowledge from the JS world but I forgot that Rust is different. Since Rust generally makes you handle the full complexity of what you're doing, you shouldn't take something big and heavy and dumb it down, you should take exactly what you need. Rust also makes it easy to comparmentalize and have small "components" that only care about what they need to care which can be composed into the big thing.
Leptos could be useful for the things people use that stuff for. That is, big interactive websites. I didn't want that. I wanted simple templates and a static site. I also don't need this constant updating, really. I don't need everything to be "automatic and magical". I can just have a command that rebuilds the site. I can make the "incremental" part myself.
So let's try to see what we need and take exactly that and only that.
The solution
Something I realized about myself is that I really enjoy doing things "from scratch" as much as possible, which kind of came across on the previous paragraph. But yeah, I don't want to use frameworks if possible. I don't think it would be a terrible idea, but I want to hand roll out my own thing. So, what I need is:
- A way to generate HTML by hand and programmatically
- A way to parse markdown
- And... uhhh... and...
And that's it? I guess saving files, but that's something the Rust standard library does.
See, that's what I mean. I didn't need much, which can be seen in the full list of dependencies I've ended up with:
maud
for HTML templates.comrak
,gray_matter
andserde
for Markdown.katex
for rendering mathjiff
for dates of blog posts and RSS feedstracing
andtracing-subscriber
for logging,color-eyre
for error handling.
And that's it! I'm sure I'll add more as time passes, but this is all I need for now, so this is all I have. The program compiles pretty fast and actually you don't need to recompile to generate new pages for the blog posts, which generate almost instantly.
And the thing I like so much about this setup is that I can add anything I want because I have full control of everything. So, for example, I can add my own caching that checks if a page has been generated already. I can add a file server with hot-reloading to see live changes. I don't need to wait a million years to compile nor to learn how to do what I want with some framework or some set of imaginary constraints I don't actually have. I really like not having that!
And, for interactivity, I'm just writing Javascript. Just vanilla motherfucking js. Guess what: that is the most efficient option. It's funny that web assembly is not really the assembly of the web, because that clearly is vanilla js. But yeah, for the one interactive element I have, it is fine.
Finally, instead of having a server that rebuilds dynamically and does SSR (which doesn't even make sense in this context anymore) and all that stuff, I just build the static site and deploy to Cloudflare Pages with a simple CLI command. It's so much simpler, it will load faster in places far away thanks to Cloudflare's CDN and it won't have any downtime if my server isn't working.
When programming this simpler version it did feel like Rust again and it was very fun end enjoyable as I expected :)
This is why people dislike async
in Rust
And when realizing this I also realized: this is the reason why people dislike async
in Rust. I think. In most languages, you can take this big thing and make the language handle a lot of it for you. In most languages, the async
executor is part of the language runtime and you don't get a say in how it works or any of the lower level details. Rust exposes all of this. People want to use async
because it makes their code in other language simpler and sometimes more efficient since it doesn't block. People try to use async
in Rust and realize it's way more complicated than they thought.
Here's my theory. A lot of people that complain about async
and argue it's too complicated probably just don't need async
in the first place. You can do a lot with threads and channels in Rust, and sometimes that is really what you want. async
is necessary for the "general case", when you want your thing to be able to work with any number of other things in any context, and sometimes you really need that! But often, you don't. async
should feel convenient and simpler than not using it, like how I hear people working in embedded talk about async
, not like it is working against you.
In short: keep it simple, stupid.