Hyperapp: Performance and Laziness

Created on 9 Feb 2017  路  32Comments  路  Source: jorgebucaran/hyperapp

It appears there's no laziness at all in hyperapp -- no diff of props:

https://github.com/hyperapp/hyperapp/blob/f29647bf9da4bc281078abcacd116031f9fb0c3e/src/app.js#L159-L163

Have you tried this out in a large application with 1000s of nodes?

Feature Website

Most helpful comment

All 32 comments

There's also no batching for vnodes at the moment, but there used to be. There are more than a couple things we could do to optimize our virtual-dom implementation.

@ccorcos there's no laziness at all

Can you help with that?

See also:

@jbucaran please consider implementing https://github.com/krausest/js-framework-benchmark, it is much more representative of real-world performance.

the only thing dbmonster tests is initial creation and .className and .textContent (or .nodeValue) replacement; it doesnt test complex reconciliation cases, track-by-key bindings or dom mutations that cause reflow/restyle.

@leeoniya Thanks! That's next on my list.

Can you help with that?

Sure! Let me see what I can do.

oh wait, I just realized there's no composition of components here...

@ccorcos What do you mean?

@jbucaran maybe #2, haha. @ccorcos ?

yes, every similar. there needs to be some way of combining components together. Hyper isnt so scalable in terms of breaking up components and reusing them by combining them together. For example:

const TwoCounters = app({
    model: {
        one: Counter.model,
        two: Counter.model,
    },
    update: {
        one: ({one, two}) => ({two, one: Counter.update(one)})
        two: ({one, two}) => ({one, two: Counter.update(two)})
    },
    view: ({one, two}, actions) =>
        <div>
            {Counter.view(one, actions.one)}
            {Counter.view(one, actions.two)}
        </div>
})

Then laziness could be introduced where Counter one doesnt need to be rerendered when Counter two changes...

@ccorcos This is now supported since https://github.com/hyperapp/hyperapp/pull/77.

<MyComponent propts=...>children</MyComponent>

We are almost like Angular, it's not okey. But yea.

Can you add choo? So we can see how we are compared to it which is similar and uses real dom. Because switching to nanomorph is absolutely easy and i've tried it.

from that screenshot, there's not a single recent version lib there; the fastest impl (Mithril) is using an ancient 0.1.21.

take my word for it, just bench this: https://github.com/krausest/js-framework-benchmark, don't waste your time with anything else.

@tunnckoCore You know, this is time consuming, so I can't promise I'll add it right away, but I'll try 馃憤

@leeoniya I just forked lhorie's implementation of the benchmark and added mine, I wasn't really paying attention to the version of the other libraries.

Do you have any idea how to get started with the krausest/js-framework-benchmark benchmarks? The README does an incredible good job at not explaining that and from all the other benchmarks I've seen, this one looks more challenging.

Do you have any idea how to get started with the krausest/js-framework-benchmark benchmarks?

Yes. The implementation itself is easy, though actually running the bench is less easy - i usually leave the latter to @krausest since he does this for the majority of pull requests and merges.

There are two versions of the bench, and you can implement one or both: -keyed and -non-keyed. Here are examples of my impls with domvm [1][2]. Your package.json should have a build-prod script [3] that bundles and compiles your app as needed to run in the browser.

Other than that, all you have to make sure is that it behaves correctly in the browser, like vanillajs and vanillajs-keyed. You can get an idea of the expected perf from the console timers that are called from your impl, but the actual bench extracts the numbers from Chrome's timeline in a very stable/reproducible way between runs.

I would start by pulling the repo, running build-prod in the vanillajs-keyed dir, and opening its index.html in a browser.

[1] https://github.com/krausest/js-framework-benchmark/tree/master/domvm-v2.0.1-keyed/src
[2] https://github.com/krausest/js-framework-benchmark/tree/master/domvm-v2.0.1-non-keyed/src
[3] https://github.com/krausest/js-framework-benchmark/blob/master/domvm-v2.0.1-keyed/package.json#L7

@leeoniya Thanks, this is at least a start. By the way, can you elaborate on:

don't waste your time with anything else.

You clearly are aware of something I am not.

Also, I'm not sure what it means, but FWIW HyperApp was actually the _fastest_ in the async version of the benchmark (mithril was not even close, I wonder what that is about). 馃槃

You clearly are aware of something I am not.

I've done impls of a lot of benchmarks. This one is by far the most representative of expected perf and highly stable between runs due to its methodology. Basically, this bench tends to accurately reflect the performance results of all other benches and covers enough surface area not to only test a couple specific cases (like dbmon). There's nothing TodoMVC bench will tell you that you won't find out more thoroughly and with better feedback from js-framework-benchmark.

There are other good ones, like uibench and vdom-benchmark but they don't have the most up-to-date impls of other libs, so it's difficult to compare vs most recent lib versions. They also dont have great output for assessing relative perf in an intuitively-colored scale.

Also, I'm not sure what it means, but FWIW HyperApp was actually the fastest in the async version of the benchmark. 馃槃

If your implementation or lib is async, rather than relying on the console timers, you should open up chrome's timeline view and record the perf between actions. Then look at "Scripting time", etc in the pie chart.

I added some hints how to start your implementation in the README of js-framework-benchmark at the bottom. Just let me know if that helps. If you have specific questions don't hesitate to ask.

@jbucaran I bet the bigger the app, the worse hyper will perform. try that benchmark with a website that has 1000s of components

@ccorcos of course, absolutely, because each "app" can sucks more than the framework with which it is built. No matter which is the "framework". 2c :D

well my point is if you wanted to build a custom visualization or some kind of dashboard, the lack of laziness is going to become a problem. i dont mean to be a put-down -- I like what you guys are building here

Related question: does hyperapp currently re-use old DOM nodes when rendering? (especially when unchanged)

tl;dr

  • Optimized engines like Inferno and Mithril are faster, but there is no extraordinary advantage.
  • HyperApp has excellent performance and it's on the same range as Cycle and React; it's faster than Riot, Ractive, Choo, Stem.
  • Choo has the poorest performance of all tested frameworks.

I followed @leeoniya's suggestion and implemented the js-framework-benchmarks.

What benchmarks were used?

Results

screen shot 2017-02-27 at 20 29 32

Hardware

Notes

  • Compare with @krausest's results here, that used similar hardware.

  • What's to do next? Improve HyperApp's vdom engine. ~Focus on the first detected weakness: deleting items.~ Done.

To me the most interesting result is create 1,000 rows where hyperapp is twice as fast as all the others whereas create 10,000 is in the range of others. I think you managed to trick my benchmark runner in this case I'm afraid I wouldn't take that result as granted...

Yep, that was my mistake when adding the frame, it's fixed already. 馃檱

Just curious, what led you to implement another view renderer instead of using an existing one? File size?

@nichoth 馃 I can think of two reasons: one is file size; I am willing to trade some performance for less bytes. If you look at the git history you'll see we went from virtual-dom, to morphodom, then Snabbdom and finally the current custom engine.

After https://github.com/hyperapp/hyperapp/pull/124 lands we'll be 1869 bytes, <=1500 without the Router. So, 1kb is more of a stretch now, but it's still in the same range.

Also, speed will have improved significantly. See the js-framework-benchmarks above 馃敟.

The other reason is: I wanted the entire source of truth in a single place.

Whenever I try to understand how some library X works, I'm often disappointed that I have to go through dozens of modules, also with deps themselves, and sometimes not written by the same person, which usually translates to quite different coding styles.

Should we consider using switch statements for perf improvement? They create lookup tables and are optimized for string evaluation especially.

Only a few flat if trees would benefit from this however

@selfup 馃 If it makes things any faster, then why not, I guess. Won't switch statements take more space than if else?

@jbucaran they have low entropy. With gzip it shouldn't matter.

I think we can close here and continue to track future optimization efforts in #183.

Since 0.8.0, and with the introduction of keys, our patch/diff algorithm became more complex (but also more powerful and useful). Performance is now around 1.34~1.37, which is still fine.

Summary

  • V2 (via minor feature bump posterior to the official release) will introduce a feature equally powerful to React's shouldComponentUpdate comparable to Elm's Html.lazy function (#721)
  • V2 will have Superfine's "blazing fast" VDOM built-in (#499)

    • The VDOM will be specifically adapted for Hyperapp, _not_ used as a dependency. (#641)

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jbrodriguez picture jbrodriguez  路  4Comments

jscriptcoder picture jscriptcoder  路  4Comments

jamen picture jamen  路  4Comments

icylace picture icylace  路  3Comments

dmitrykurmanov picture dmitrykurmanov  路  3Comments