
How JavaScript works: inside the V8 engine - zlatkov
https://blog.sessionstack.com/how-javascript-works-inside-the-v8-engine-5-tips-on-how-to-write-optimized-code-ac089e62b12e
======
Veedrac
> How to write optimized JavaScript

This is all sensible advice if you're interested in writing fast-enough code,
but I do find there's a lack of material for people who want to write _fast_
Javascript. Pretty much the only thing I've found is the post by Oz[1], though
I really don't want to have to compile Chrome.

For an example, I have a method in Javascript that does a mix of (integer)
arithmetic and typed array accesses; no object creation or other baggage. I
want it to go faster, and with effort I managed to speed it up a factor of 5.
One of the things that helped was inlining the {data, width, height} fields of
an ImageData object; just moving them to locals dropped time by ~40%.

Yet after all this effort, mostly based on educated guesses since Chrome's
tooling doesn't expose the underlying JIT code for analysis, the code is still
suboptimal. There's a pair of `if`s that if I swap their order, that part of
the code allocates. How do people deal with these issues? A large fraction of
this code is still allocating, and I haven't a clue where or why.

Perhaps I'm asking too much from a language without value types (srsly tho,
every fast language has value types), but what I want is clearly possible:
ASM.js does it! I don't _really_ want to handwrite that, though.

[1]:
[https://www.html5rocks.com/en/tutorials/performance/mystery/](https://www.html5rocks.com/en/tutorials/performance/mystery/)
(E: This link is actually written from a different perspective than the one I
read, but the content is the same.)

~~~
avaer
I have some experience with this stuff, and my go-to advice is:

    
    
      - Never construct or mutate objects in a loop or recursive call.
      - Preallocate the max of what you'll need at load time, then index into it.
      - Prefer typed arrays over objects.
      - Don't use strings. Work with numbers.
      - Remove function calls.
      - Do not call a function with differently shaped arguments.
      - If the JS engine can't prove a number is a 32 bit int, you'll take a large perf hit.
      - Break out your algo into a worker. Not only does it keep things responsive, but it might actually be faster due to less GC pressure.
      - The web stack is full of frightening gotchas that you'll discover. Recieving large data on a WebSocket can halt your DOM rendering. Leaking a Promise callback can completely blow a function into interpreter mode. And other fun times.
    

Finally: If you're hand optimizing and not getting sufficient results on your
preferred browser, accept that you're screwed. The next browser will break
your assumptions. Rethink your algorithm and data flow, or move it elsewhere,
like a worker or server.

I do agree the tooling for this stuff isn't great, but I found Chrome's line-
by-line perf counts to be excellent hints about where you could do better.
It's often surprising but obvious in retrospect.

~~~
nybblet
I agree with a lot of these bullets just as general design principles, and
emphatic +1 to not hand-optimizing.

Just noticed this comment after posting
[https://news.ycombinator.com/item?id=15069116](https://news.ycombinator.com/item?id=15069116),
which is specific to V8 but which I'd guess holds for other JS engines too.
Relevant excerpt:

""" ...in just about every case, the real answer is "it depends". That's why
V8 is ~1,000,000 lines of code, not 1,000.

Please don't try to follow some rule blindly, much less derive. More often
than not, when you try to hand-tune for the last bit of performance, you'll
actually trigger something that was introduced to streamline the other [95%
of] cases/JS devs' code, and then you've spent hours on making your code less
readable with no benefit.

There is no "Try This One Crazy Trick To Make Your JavaScript Fast!!!" (or
even ten!) """

(And for the specific case of preallocation vs. growing as you go, right now,
in most cases, the difference is negligible. In the others, it depends.)

~~~
BatFastard
What this tells me is don't bother to try to write performant code in
javascript, which is scary!

~~~
nybblet
Not what I said. I said to avoid /hand-optimizing/ code.

Trying to write performant code is good. Some examples of advice on that
front: \- use an appropriate algorithm, and make sure the logic is free of
bugs. For example, accidental out-of-bounds array accesses can really hurt
performance. \- avoid needless computations. Don't do something (complex)
twice if doing it once is enough.

If this sounds like "use common sense/generally good design principles",
that's because it is (and hence the agreement with many of parent post's
points that are simply and straightforwardly good design principles).

Let's take the example of "remove function calls". It's true that each call
costs a few instructions, but unless the function you're calling is _tiny_ ,
that overhead won't be measurable. If it makes sense for
readability/maintainability/testability to split your code into functions,
then by all means do it!

Another example is the "trick" to write "for (var i = 0, len = array.length; i
< len; i++)" instead of the simpler "for (var i = 0; i < array.length; i++)".
As [http://mrale.ph/blog/2014/12/24/array-length-
caching.html](http://mrale.ph/blog/2014/12/24/array-length-caching.html) (from
the original V8 team) explains in great detail, what seems like a no-brainer
can actually do the opposite of what you'd expect --- and at the same time,
won't make any difference whatsoever in most real code (i.e. aside from
microbenchmarks).

Yet another example is
[https://www.youtube.com/watch?v=UJPdhx5zTaw](https://www.youtube.com/watch?v=UJPdhx5zTaw).
You can spend all day on it, but at the end of the day, the best thing to do
is code correctly and sanely, not hand-optimize the bejeezus out of the thing
(mostly yourself, in many cases).

~~~
Veedrac
Only we've seen what happens when everyone follows this: you get to the state
that we're in today, where everything sucks but everyone's OK with it.

------
fdw
If you're into V8 internals, I'd recommend watching these talks by Franzsika
Hinkelmann, a V8 engineer at Google:
[https://www.youtube.com/watch?v=1kAkGWJZ6Zo](https://www.youtube.com/watch?v=1kAkGWJZ6Zo)
,
[https://www.youtube.com/watch?v=B9igDWV5ZUg](https://www.youtube.com/watch?v=B9igDWV5ZUg)
and
[https://www.youtube.com/watch?v=p-iiEDtpy6I&t=606s](https://www.youtube.com/watch?v=p-iiEDtpy6I&t=606s)

She's also recently started blogging at
[https://medium.com/@fhinkel](https://medium.com/@fhinkel)

------
cm2187
I was wondering, given that 90% of the javascript in browsers must be standard
libraries (jquery, bootstrap & co) wouldn't it make sense for google to hash
the source code for every published version of these libraries, compile those
statically using full optimisation and ship the binaries as part of their
updates to the browser, so that you only have to compile the idiosyncratic
part of the code?

~~~
sand500
Maybe ship popular libraries as web assembly?

~~~
sjrd
Right, because wasm is there to solve all our performance problems, isn't it?

And how exactly is your jquery.wasm going to access the DOM? Or expose methods
to your application, for that matter?

wasm is not a drop-in replacement for JS.

~~~
Fifer82
I really don't know much about wasm.

I just wondered though, from a birds eye view, does using wasm effectively
give you control of everything within the "frame" of your browser?

Can I get rid of the DOM(boilerplate aside), get rid of CSS?

For example HTMLCanvasElement is a flat surface. I can do what I want within
it as a scene graph renderer.

Could I do that with wasm? Could I invent my own 2D entities (x, y, width,
height). My own 2D Constraints resize like Apple Devices etc?

~~~
skybrian
It's still in the same sandbox as JavaScript. You don't get any new API's.
There's only going to be speedup on CPU-intensive things.

------
yanowitz
Interesting article--I'd love to see one just on GC.

I just downloaded the latest node.js sources and v8 still has a call to
CollectAllAvailableGarbage in a loop of 2-7 passes. It does this if a much
cheaper mark-and-sweep fails. Under production loads, that would occasionally
happen. This led to pause-the-world-gc of 3600+ms with v8, which was terrible
for our p99 latency.

The fix still feels weird -- we just commented out the fallback strategy and
saw much tighter response time variance with no increased memory footprint
(RSS).

I never submitted a patch though because although it was successful for our
workload, I wasn't sure it was generally appropriate (exposed as a runtime
flag) and I left the job before I could do a better job of running it all
down.

~~~
tracker1
I've had a similar, but differing issue on a server that ran a lot of one-off
node scripts for things ranging from ETL, or queue processing. I found that I
_wanted_ to force GC after every, or every N items, because the memory could
bloat out a lot before GC would happen and pause for several seconds... or,
potentially starving out peer processes.

Fortunately, that was already in the box, though behind a runtime flag.

------
btown
Is there a way to see what hidden class an object has? For instance, if an
array of objects is parsed from json, were all the objects assigned the same
hidden class? Alternately, can one obtain statistics about hidden class usage?
Seems like this would be very helpful for real world apps, especially given
the prevalence of data intensive Electron apps.

EDIT:
[https://www.npmjs.com/package/v8-natives](https://www.npmjs.com/package/v8-natives)
haveSameMap seems to do exactly this!

------
dlbucci
> Also, try to avoid pre-allocating large arrays. It’s better to grow as you
> go.

Is this really true? I've only heard the opposite (preallocate arrays whenever
possible) and I know that preallocation was a significant performance
improvement on older devices with older javascript engines.

~~~
ChrisSD
Hm, it might have something to do with this:
[https://groups.google.com/d/msg/v8-users/Vov_e5GUq3o/eqRT8mA...](https://groups.google.com/d/msg/v8-users/Vov_e5GUq3o/eqRT8mAePMMJ)

> For preallocated arrays, 100,000 just so happens to be the threshold where
> V8 decides to give you a slow (a.k.a. "dictionary mode") array.

~~~
nybblet
tl;dr: No, this is outdated, and please don't try to hand-optimize.

This is outdated. It was true in 2013, but changed in 2014
([https://codereview.chromium.org/416403002/](https://codereview.chromium.org/416403002/)
; specific change in elements.cc).

Two tangential cents/Unpopular Puffin: there's a lot of misinformation or half
truths that are propagated about V8 in an effort to boil all their
optimizations down to One True Set of Advice, in this post and others.

The thing is, in just about every case, the real answer is "it depends".
That's why V8 is ~1,000,000 lines of code, not 1,000.

Please don't try to follow some rule blindly, much less derive. More often
than not, when you try to hand-tune for the last bit of performance, you'll
actually trigger something that was introduced to streamline the _other_ [95%
of] cases/JS devs' code, and then you've spent hours on making your code less
readable with no benefit.

There is no "Try This One Crazy Trick To Make Your JavaScript Fast!!!" (or
even ten!)

(And in this particular case of pre-allocation vs. growing as you go? Usually
the difference is so tiny it doesn't matter, and in the other cases --- it
depends.)

Disclaimer: I'm not on the V8 team but I'm on a personal basis with some of
them, and these are my own thoughts cobbled from casual conversations.

------
kevmo314
> Now, you would assume that for both p1 and p2 the same hidden classes and
> transitions would be used. Well, not really. For “p1”, first the property
> “a” will be added and then the property “b”. For “p2”, however, first “b” is
> being assigned, followed by “a”. Thus, “p1” and “p2” end up with different
> hidden classes as a result of the different transition paths. In such cases,
> it’s much better to initialize dynamic properties in the same order so that
> the hidden classes can be reused.

Does that mean an object with n properties takes up O(n^2) memory for the
class definitions or O(n!) if the classes do not guarantee a property
initialization order?

~~~
ridiculous_fish
An object with N properties will typically require N hidden classes, forming a
chain back to the empty class. However most hidden classes only require a
constant amount of memory, so this is O(N) allocation size.

If you initialize the same properties but in a different order, you'll get a
new list of N hidden classes. In the worst case you could indeed have O(N!)
hidden classes, if you construct with every possible initialization order.

~~~
inglor
Are you sure of that? If I recall correctly from the source an object with N
properties typically requires 1 hidden class (for that object).

In order for it to require 8 it needs to add properties very dynamically. Am I
remembering this wrong?

~~~
ridiculous_fish
You can see the description of how hidden classes work on the v8 wiki [1]. The
short explanation is that the way v8 finds a hidden class is by following a
chain of transitions starting at the empty class. Intermediate classes have to
be kept around to retain the transitions.

That is, in code like:

    
    
       function Point(x, y) {
          this.x = x;
          this.y = y;
        }
    

v8 retains the hidden class for {x}, so it can find the hidden class for {x,
y}.

[1] [https://github.com/v8/v8/wiki/Design%20Elements#fast-
propert...](https://github.com/v8/v8/wiki/Design%20Elements#fast-property-
access)

------
sjrd
For a more comprehensive reference on v8 internals, there is
[http://wingolog.org/tags/v8](http://wingolog.org/tags/v8), which has been
around for a long time. It is even directly referenced from
[https://developers.google.com/v8/under_the_hood](https://developers.google.com/v8/under_the_hood).

I don't think there's anything in this post that wasn't already explained in
this reference, except the fact that now there's Ignition and TurboFan, but
that doesn't fundamentally change anything.

------
schindlabua
Does this also mean that in the function that returns an object vs. prototype
debate the former wins because object literals presumably require less class
transitions?

let Ctor = function(a, b){ this.a = a; this.b = b; }

vs

let obj = (a,b) => ({ a:a, b:b });

------
cryptozeus
Thanks for the post, great writing...

