Lithe 4: On the Shoulders of Giants


Starting from characters and parsing that into an AST that could render a Svelte program could definitely open open possibilities for some low-level optimizations, but even just parsing a normal HTML document is not a small project, and Svelte includes JavaScript, TypeScript, Handlebars-esque blocks, CSS, and SCSS, too. Doing all that at once would be the mother of all slogs. But, luckily, I don't have to! There exist many, many pre-build alternatives. The Svelte compiler itself even uses a couple.

So here's the new plan:

  1. Create a basic Rust lib and make sure I can get communication working to/from JavaScript via FFI in Node
  2. Send a svelte file as a string through a DOM parser to get a basic AST
  3. Convert that AST to our own custom one
    • This is where we should do any conversions we might want to the original AST, like squashing child Text nodes, splitting nodes or inserting nodes, etc.
  4. Render that custom AST to a string
    • We should eventually keep track of other things, warnings, CSS, etc., but for now we're simply trying to replicate the JavaScript string
    • We should keep in mind that whatever we do here we'll need to do something very similar when rendering for SSR, so we should do as much AST manipulation as possible before this step. This should be a simple "let's render a string"

But before we really get started let's see what kind of performance we're getting out of the original compiler. I'm only looking to test execution times, so to ignore however long it takes to load everything into Node's runtime we'll simply do two runs per test: once to ensure everything's loaded, and the other we'll actually time. How long does Svelte take to compile this simple program?

<span>Hello world!</span>

On my machine this is about 5ms. That seems... high? I think? I haven't written a compiler before, but computers are fast and this program is absolutely tiny. Maybe there's some cost to compile anything, regardless of program size? Let's see how it scales. What if we do 10x?

<span>Hello world!</span>
<span>Hello world!</span>
<span>Hello world!</span>
<span>Hello world!</span>
<span>Hello world!</span>
<span>Hello world!</span>
<span>Hello world!</span>
<span>Hello world!</span>
<span>Hello world!</span>
<span>Hello world!</span>

I should note this renders extra calls to a space() function by default (I assume so the resultant DOM looks nice), so to avoid this I remove all newlines during tests. Anyway, with newlines removed this takes 16ms. Hmmm.... let's do a few more:

Number of spansTime to render
15ms
1016ms
10062ms
1000432ms

Okay, so half a second for 1000 elements isn't super great, but it's still probably okay. How often are you writing components this big anyhow? And any half-decent system would only recompile whenever you make changes. Still, fast compiles make for happy devs, so this is the mark we're trying to beat.

The only other order of business for now is the name. I did what I always do and looked up some synonyms for similar projects, in this case Svelte. After some browsing I landed on lithe, so that's what I'll use for now until I find some other JS project already picked that name.

Click for spoilers

Lithe is far from done, but it can easily render the example programs above. In release mode, 1000 spans takes just 5ms to run (!!!), most of which is simply the initial DOM parsing provided by an external library. Time is lost initializing FFI and shuttling strings to/from Node, but even so it turns out Rust is really really fast (who knew?). As we add more features lithe can only slow down, but for now I'll take this as a sign that this work might actually be useful.


Recommended reading

Lithe 3: A Rewrite

What if I just copied the Svelte compiler, written in TypeScript, changed all of the file extensions from .ts to .rs, and fixed all the bugs? It'd be a slog, sure, but at the end of the day I'd have a compiler that was very nearly the same, but presumably more performant.

This attempt I gave a real try, spending maybe three nights just chugging away. What I got was just more and more errors, which was fairly disheartening. I also realized just how different Rust is, and that a 1-to-1 rewrite would result in something that wouldn't be as ideal as it could be. For example, take this very simple TypeScript class…

Lithe 5: Optimize for the minifier

halfnelson did a lovely investigation of how large Svelte projects scale. I'm not looking to rehash that per se, but I am interested in how well the current Svelte compiler's output gets minified.

For this investigation I'll be pulling as many .svelte files as I can. Let's use the Copilot strategy and pull from GitHub repos with permissive enough licenses.