Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Performance #35

Closed
maciejhirsz opened this issue Aug 24, 2016 · 24 comments
Closed

Performance #35

maciejhirsz opened this issue Aug 24, 2016 · 24 comments

Comments

@maciejhirsz
Copy link
Contributor

maciejhirsz commented Aug 24, 2016

Related to #34

I really, really like this library! People tend to forget that simplicity is how you make elegant code, not abstraction.

That said looking through the source code I've found some potential bottle necks. There are few places where a happy-path optimiziation for most-common patterns can save few hundred nanos.

I've made a very rough implementation with a limited feature set (name very temporary), and this is what I'm getting:

Benching DOOM create a <b> tag with text via extend
858ns per iteration (1165528 ops/sec)

Benching FRZR create a <b> tag with text via extend
4874ns per iteration (205150 ops/sec)

Numbers for FRZR don't have the el.extend from the PR. It's going to be hard to retain all functionality with that level of performance, but it should be easily possible to go into < 1500ns area.

I'm aware nanosecond benchmarking in JavaScript is a pretty exotic sport, but it only takes 200 DOM elements (and let's be honest, most complex apps create much more) to block the thread for a full millisecond with FRZR, and that's without any attributes or child element mounting etc.

@pakastin
Copy link
Owner

Hi! Glad you like FRZR and welcome aboard! 👍

Yeah, I'm aware of some bottlenecks, but I've ranked simplicity higher than micro-optimizations. There's no end of adding this and that here and there. Suddenly you have a big bloated framework, where all the abstractions are actually what make it slow.

Having said that, I agree with you, that el.extend could be more efficient - although I actually quite rarely use it myself. I don't actually even use new View(), but el(View) instead. Then it's usage is identical to the list(View) 😎

I check out your PR in a moment - I just got back from paragliding and have to take a little rest 😁

@pakastin
Copy link
Owner

Btw. have you seen my presentation about the concepts behind FRZR: https://youtube.com/watch?v=0nh2EK1xveg 😉

There's of course some special details not mentioned, for example frzr.svg(tagName, ...) works a bit differently etc..

@maciejhirsz
Copy link
Contributor Author

maciejhirsz commented Aug 24, 2016

I was watching it yesterday, that was what got me to mess around here :). I'm looking for a view library that's as tiny as possible for one project of mine where I'm already packing a 700KB of Pixi.js for canvas rendering, and the actual DOM UI I've planned is not enough to pack Vue or React on top of that (plus I think both of those, are bloated monsters already).

@pakastin
Copy link
Owner

I agree with you. With FRZR your bundle can actually quickly become smaller than even with vanillajs, if you think about how many createElement/setAttribute/etc you don't have to type 😄 And I still have tried to abstract as little as possible. So it's more futureproof also in a sense..

But comparing frameworks / libraries is not that useful - each one serves or have served it's purpose.

@pakastin
Copy link
Owner

If you have any questions about FRZR, don't hesitate to ask - I'm happy to help and it's good to gain insight which things are easy to use use and which not.. 😎 Documentations suck right now and there's not enough examples, I should enhance that. If only there would be more hours in a day 🤔

@maciejhirsz
Copy link
Contributor Author

I think I can get my way around it by just reading through the source code 🤓.

Just goofing around, with some specialisation you can drop numbers quite a lot:

Benching FRZR create a <b> tag with text via extend
883ns per iteration (1132131 ops/sec)

This is using the assumption that the first child of the el can be a String (el('p', 'Foo') or el('p', {}, 'Foo')) and if so applies it's content to textContent, same thing I did in my own implementation. Otherwise mount itself takes almost 2000ns.

@pakastin
Copy link
Owner

One big question with microbenchmarks is, that does the compiler have a chance to optimize the code or not.. Usually it will take some time to get an app to run at full speed. After the optimizations things can get faster without the extra tweaks. For example I just read an article that in most cases caching an array length is slower after optimizations, because compiler can't optimize it as far as without.

And the better the compilers get, less the need for micro-optimizations..

@pakastin
Copy link
Owner

Also when you have long execution chains, having an extra code can hit the overall performance as well..

@pakastin
Copy link
Owner

I'm no expert of the compilers though, but I know FRZR's rate of performance/simplicity is already pretty good.. I'm not making the fastest library around, but the most minimalistic and all-around performant as possible.

@pakastin
Copy link
Owner

But I will check out your PR in a moment and of course there's room for lots of further improvements 😎

@maciejhirsz
Copy link
Contributor Author

maciejhirsz commented Aug 24, 2016

V8 is a hell of a VM, but it's not magic. You usually get the full benefits after few iterations (< 10), I'm benchmarking everything with a 1,000,000, if only to get proper granuality to measure nanoseconds (performance.now() is nowhere near as precise as process.hrtime() in Node.js, and even that has some overhead).

My general experience is that people in JavaScript world assume too much and benchmark too little 🙂. A recent example - I've seen a claim that creating a Uint8Array on an existing ArrayBuffer is free - it seems like it should be nearly free, it's just a view to access already existing data right? But it turns out it's anything but free, I've even filed a bug report for Firefox about it.

On that note, V8 can do absolutely nothing about WebAPIs (so, basically the entire DOM), it doesn't even know they exist. To V8 they are just some FFI C bindings it has to call and wait for. Calling element.textContent = 'Foo' will be always faster than element.appendChild(document.createTextNode('Foo')), no matter how good the actual JavaScript compiler gets. It's up to the programmer to pick the right API.

Making the execution chain longer can stop V8 from inlining functions, and when that happens, a microbenchmark will pick it up. It can also be remedied by putting the expensive but rare control path behind a different function call :). But I digress...

Also I don't want this whole thing to seem like I'm picking on FRZR being slow, it's very, very decent! I wouldn't want to go super crazy over this and start breaking everything down into specialized if / else hell to squeeze up every single nanosecond possible, that is a kind of madness of it's own, simplicity has to prevail! There are usually few low hanging fruits that are easy to kick over for a big boost. The expand is a good example, I was reading through release notes and saw your note about it having a performance cost, then looked at the code and went "I know that from somewhere".

Ok, this ended up being way too long! Need to cook some food.

@pakastin
Copy link
Owner

Yeah, I've seen it also, just always thought it's ugly :) But let's go with it!

@pakastin
Copy link
Owner

Btw. why did you choose six "cached" arguments? :D

@maciejhirsz
Copy link
Contributor Author

maciejhirsz commented Aug 24, 2016

No particular reason other than that I like "counting" to F. I've been dealing with too many hexadecimal numbers lately, and 0123456789ABCDEF is now permanently burned into my brain 🤓.

@maciejhirsz
Copy link
Contributor Author

With that last PR reversing 10000 elements with FRZR now benchmarks at ~75ms, while React is at ~120ms on my hardware 😎.

I reckon you have a better idea what the numbers were before.

@pakastin
Copy link
Owner

That's great! I made a pull request to update the dbmon as well. It's at least as fast as it was (was already quite old version, which was probably simplier)..
mathieuancelin/js-repaint-perfs#82

@pakastin
Copy link
Owner

I'm so glad you came aboard – such great pull requests coming through! 👍

@maciejhirsz
Copy link
Contributor Author

Ok, I think I just broke the internet.

I've been goofing around with different kinds of proof of concept implementation of syntax similar to FRZR. Tests:

bench('Vanilla JS', function() {
    var div = document.createElement('div');

    var h1 = document.createElement('h1');
    h1.className = 'V_JS';
    h1.textContent = 'Hello ';

    var b = document.createElement('b');
    b.textContent = 'V_JS';

    h1.appendChild(b);
    h1.appendChild(document.createTextNode('!'));

    var p = document.createElement('p');
    p.textContent = 'Bacon ipsum dolor amet meatloaf meatball shank porchetta \
            picanha bresaola short loin short ribs capicola fatback beef \
            ribs corned beef ham hock.';

    div.appendChild(h1);
    div.appendChild(p);

    return div;
});

bench('DOOM <div> with multiple child nodes', function() {
    div(
        h1(setClass('doom'), 'Hello ', b('DOOM'), '!'),
        p('Bacon ipsum dolor amet meatloaf meatball shank porchetta \
            picanha bresaola short loin short ribs capicola fatback beef \
            ribs corned beef ham hock.')
    );
});

bench('FRZR <div> with multiple child nodes', function() {
    frzr_div(
        frzr_h1({ className: 'frzr' }, 'Hello ', frzr_b('FRZR'), '!'),
        frzr_p('Bacon ipsum dolor amet meatloaf meatball shank porchetta \
            picanha bresaola short loin short ribs capicola fatback beef \
            ribs corned beef ham hock.')
    );
});

Benchmark results on Chrome Canary:

Benching Vanilla JS
5426ns per iteration (184285 ops/sec)

Benching DOOM <div> with multiple child nodes
5245ns per iteration (190669 ops/sec)

Benching FRZR <div> with multiple child nodes
10660ns per iteration (93806 ops/sec)

So basically as fast as vanilla implementation, sometimes faster!

Here is how it works:

All the tag functions are constructed by bindings, like so:

var b = expand.bind(el('b'));
var div = expand.bind(el('div'));
var h1 = expand.bind(el('h1'));
var p = expand.bind(el('p'));

el() there is just an alias for document.createElement(), meaning that expand() function gets contextually bound to a DOM Node ((this instanceof Node) === true).

The whole content of expand is this:

export function expand() {
    var el = this.cloneNode(false);
    var hasChildren = false;

    for (var i = 0; i < arguments.length; i++) {
        var arg = arguments[i];

        if (typeof arg === 'string') {
            if (hasChildren) {
                el.appendChild(document.createTextNode(arg))
            } else {
                el.textContent = arg;
                hasChildren = true;
            }
        } else if (arg.nodeType) {
            el.appendChild(arg);
            hasChildren = true;
        } else {
            arg(el);
            if (!hasChildren) {
                hasChildren = el.hasChildNodes();
            }
        }
    }

    return el;
}

So, instead of using document.createElement it's shallow-cloning the original DOM Node, this appears to be faster, and logically seems like it should be since we are avoiding passing and parsing strings around. Then it goes through the arguments with 3 possibilites:

  • If it's a string, set it to textContent or append as new text node, very similar to what FRZR is doing now.
  • if it's a node, append it.
  • assume it's a function otherwise, and call it with the element as argument.

Major differences compared to FRZR:

  • no null tolerance.
  • no Number tolerance, need to cast manually.
  • no components.
  • no attribute map support, but...
  • do whatever with the element by passing a function.

When constructing the header like this:

h1(setClass('doom'), 'Hello ', b('DOOM'), '!')

setClass is a higher order function, that is - it returns a function that will then modify the element and set the className to whatever value. This kind of a behaviour means that the element creation can be extended to do virtually anything without introducing additional runtime cost to the generic functionality.

Nothing FRZR related as such, it's just my own exploratory territory, but I figured you might find the figures and more FP approach interesting.

And now I'll go out and pretend I do have a life.

@pakastin
Copy link
Owner

I also like to explore: you can't believe how much FRZR have changed since 0.0.1 :) Also the whole way of how I develop projects have changed so many times I can't even count anymore..

But I've decided to keep the design of FRZR quite stable from now on, and prefer to create a new library instead if the whole design is a lot different. For example RZR was such an experiment (with virtual dom). I didn't like the virtual dom concept though, since you need to put keys everywhere all the time to prevent extra updates.

I've actually also thought about creating a bit more "pure" version of FRZR. Not with virtual dom, but quite something you showed. Let's see. Also the frzr-dom needs rethinking. Instead of faking the window and document objects, it should "inject" code to FRZR itself when it's on the server-side. I didn't know before how it's done with Rollup, but I've found a way!

It's really exciting to fiddle around with new ideas, even if they are dumb sometimes – in the end you might invent something great! ;)

@pakastin
Copy link
Owner

Too many people just say:

  • "Why don't you just use React or Angular?"
  • "Just another library / framework"
  • "Silly little library – big and popular frameworks are better!".

I like you're kind of people more, who like to try new things and love minimalism! 😎

@pakastin
Copy link
Owner

Although when the author of Mithril.js says kind words about your library, you know you're doing something right and you learn to ignore the nonsense :D

@pakastin
Copy link
Owner

Oh, now I realized what you're doing with that bind there. That's really clever! 👍

@maciejhirsz
Copy link
Contributor Author

Yep. Making a big library is not really a problem, it's making a small library that still does everything you need and not letting it bloat over time that's the challenge.

I'm using React at work now, things are easier when people jump at the code base and are familiar with the ways things are built because they have seen it before - but that's really all there is to it. The code behind it matters little.

I have a love / hate relationship with virtual dom. The nerdy side of me likes it for being all automagical, the concept as such is really really brilliant... but... DOM updates are not video game rendering on a bitmap, simple bindings solve the problem easier, faster and with much, much less code. And then you have smaller but still considerably sized libs like Vue which does the update bindings for you, but suddenly things are magical and you don't really have any control over the (leaky) abstractions.

@pakastin
Copy link
Owner

I know exactly what you mean.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants