Hi gang!
My name is Mathias, and I work on Chrome.
Today, I’d like to highlight a handy little JavaScript performance trick that’s a little bit surprising,
and somewhat counterintuitive.
JavaScript applications, and in particular web apps, commonly use large objects to represent
state or other data.
The example we’re looking at right now is very simple and small,
…but on the web it’s surprisingly common to see objects of several kilobytes in size, especially
for web apps built using frameworks such as React or Redux.
Unfortunately, web apps often depend on this data for their initial render.
In such cases, this large JavaScript object ends up on the critical path,
and so, users end up staring at a blank screen until all of this data is
loaded, parsed, compiled, and executed by the JavaScript engine.
How can web developers make this faster?
One approach is to use server-side rendering: you serve plain HTML that already contains
the processed form of the data, and therefore no JavaScript is required for the initial load of your app.
This way, you can avoid the need for the large JavaScript objects entirely.
But, what if you can’t use server-side rendering?
Is there anything else we can do to improve performance?
It depends.
In many cases, this critical data doesn’t contain anything that cannot be unambiguously
represented in JSON: there’s no Date objects, no BigInts, no Maps, no Sets, and so on.
And in those cases, instead of having an object literal in the JavaScript source code, you
can serialize the object as JSON, turn it into a JavaScript string literal, and then pass
that to the built-in JSON.parse function.
This technique produces an equivalent object.
Now which of these two techniques do you think is faster?
As it turns out, the JSON approach is significantly faster!
This is a little surprising and counter-intuitive: the JavaScript object literal feels like the
more direct approach, whereas JSON.parse feels like an additional layer of indirection.
Yet somehow, JSON.parse is much faster. Why is that?
Part of the reason is that for JavaScript engines, the JSON example is super simple
to scan and parse.
To the JavaScript parser, this code snippet is little more than a CallExpression with
a single argument.
The large blob of data is just a single StringLiteral token!
On the other hand, the equivalent object literal consists of many more tokens:
each property name is an Identifier token or a string-like literal,
and in this case each of the values is a NumericLiteral, but they could really be anything.
They could be nested objects or arrays, with properties and values of their own, in which case there’s even more tokens.
So compared to the JSON.parse example, the JavaScript parser needs to work harder
just to tokenize this script correctly.
Another reason why JavaScript object literals are harder to scan and parse is because
you don’t know ahead of time that they’re object literals!
Let’s unpack that a little bit.
Pretend you’re a parser for a minute, and you’re looking at source code character by character.
In JSON, if you see an opening brace, there’s only two possible options:
either this is the start of an object, or this is invalid JSON.
Two options. That’s it.
In JavaScript however, there’s many more possibilities.
An opening brace could be the start of an object literal, but it could also be a number of other things!
Let’s walk through some examples.
Here, we have an opening brace on the second line.
What do you think, is this an object literal or not?
And what does the x on the second line refer to?
Does it refer to the binding on the first line, or is it something else?
Turns out, it’s not even possible to answer these questions without looking at the rest of the code.
In this case, the second line creates an object literal.
x refers to the binding on the first line.
But in this case, it’s an object destructuring, and the x doesn’t refer to the first line at all.
And then there’s this scenario, where the second line contains an arrow function with
a destructuring parameter named x.
In that case, the x also does not refer to the first line.
The point I’m trying to make here is that parsing JavaScript is tricky because its grammar is context-sensitive.
JSON doesn’t have that problem, and so parsing JSON is much simpler.
And that’s why, especially for large objects, it’s faster to use JSON.parse than to use a JavaScript object literal.
At this point you’re probably wondering…
how much faster is it exactly?
We measured this for an 8-MB payload on cold loads (with no caching) and found that in V8 and Chrome,
the JSON.parse technique is 1.7 times as fast as the object literal.
And the speed-up applies to other JavaScript engines and browsers as well!
In Safari and JavaScriptCore, the difference is even greater: there, JSON.parse is twice as fast!
Ok, you might say, that’s just a synthetic benchmark with a very large payload.
How does this affect real-world web apps?
Well, Henrik Joreteg applied this optimization to a Redux app he was working on,
and published a case study about the results.
There was an 18% improvement in Time to Interactive, and the app’s Lighthouse performance score
went up 8 points as well.
That’s not bad for applying a single optimization!
How can you start making use of this optimization?
I wouldn’t recommend doing this manually.
In source code, you’d still want to use object literals since they’re more readable.
Instead, tooling can help you automatically transform large object literals into JSON.parse,
as a build-time optimization.
webpack already applies the JSON.parse technique if your code base uses JSON modules.
There’s also a Babel plugin that can apply the transformation on other code.
For more JavaScript performance tips, check out “The Cost of JavaScript” on the V8 blog.
And that’s it — thanks for watching, everyone!
