maverick-js / maverick

Build and ship strongly typed and fast UI component libraries.
MIT License
70 stars 3 forks source link

Latest Performance Review (0.22.0) #2

Closed mihar-22 closed 1 year ago

mihar-22 commented 1 year ago

I've been working on improving/fixing Maverick over the past week and thought I'd share some of the learnings/discoveries here.

I benched across cellx, reactively, and the JS framework bench so I can get a well-rounded view of what's happening both in terms of raw perf and mem. These are the best bencharmks I know of at the moment but would love to find more and build some if I have time.

Signals

I tried everything from various data structures (linked lists, k-ary graphs, arrays), primitives (strings/symbols/prototypes/functions), and heaps of other stuff. The solution I landed on was:

Complete solution can be viewed here

Compiler

The compiler was mostly in good shape.

Runtime

SSR

No exciting changes here at the moment - I still need to find a good benchmark. I think Marko has one.

Benchmarks

Intro

I'm only comparing libraries that are of similar color apples. Comparing to libraries such as Preact Signals or some of the others makes no sense. They're all amazing libs but no point disingenuously comparing them when they don't account for scopes, context, error handling, nested effects, large dynamic graphs, etc.

I had shared some results previously on Twitter which some of you may have seen. I found out later when running JS framework bench that it wasn't a complete solution since it had mem leaks and other major issues so I disregarded those numbers.

With these recent changes I was able to achieve similar perf results whilst removing all issues and supporting a slightly bigger feature set with things like proper effect scopes and deeply nesting them.

CellX Bench (Signals)

The Cellx benchmark saw a massive speed jump but this bench is mostly useless since it doesn't account for scope disposal and other dynamic stuff. I found that it's essentially only testing two vectors in a single and very long compute subtree: how eagerly operations are queued and how aggressively its cached. You can essentially BS this benchmark and learn nothing new about your lib.

image

Reactively Bench (Signals)

The reactively bench saw a huge jump and considering Maverick is well tested which now supports a slightly wider-feature set with things like effect roots and disposal I was really happy to see these numbers. However, these numbers don't translate into the DOM (more info next section). I'd also guess that most the gains here were specifically from the source/observer tracking scheme created by Modderme.

I need to get a better picure of memory and GC times but I can just say for now they dropped by ~60% or so from the numbers I was scanning over time. You can ignore this until I have better proof.

Big wins here are amazing. It might not translate into improvements in our DOM runtime but it will for everything non-DOM related - raw number crunching and side-effects. This is something I personally need for the Vidstack Player analytics library I'll be creating.

image

JS Framework Bench (Signals + DOM)

Tested locally but I've submitted a PR.

This is started out really bad and took me some time to weed the issues out. I was mostly concerned about memory as the rest is not super important just yet. Either way, everything has been resolved and we're in the green with Solid-like performance.

Screen Shot 2022-12-27 at 7 17 41 pm Screen Shot 2022-12-27 at 7 17 56 pm

Review

This is where things get tricky because it's mostly looking at the DOM runtime. I tested with both the Maverick compiler and the Babel DOM Expressions compiler Solid uses and found results to be either the same or ever-so slightly slower than Solid.

I think the problem here is that Solid is hyper-optimized for that benchmark :sweat_smile: I copied maybe 90% of the same optimizations but couldn't beat Solid in any meaningful way. I'll wait to see what the official results end up showing.

Memory was especially difficult to beat Solid which is weird because I thought the linked list solution for scope tracking which doesn't require creating any arrays would be a huge win. I think the answer here is that V8 can optimize arrays really well, and the Solid runtime is more efficient but slightly bigger in size.

Obvious finding: Improving performance across compute, memory, and library bundle size is very fucking hard. They're generally at odds with each other and I think Ryan has done a phenomenal job optimizing Solid. I think at this stage it would be about revising compilation and the DOM runtime if we want to see big wins. I think most micro-optimizations have been discovered and I can't see us going much further from here. I guess the last part would be finding more ways to do less or no work at all until absolutely required. Service workers could be an interesting exploration.

On a side note for anyone interested I weeded out a lot of memory issues by:

Final Notes