A literal is a textual representation (notation) of a value as it is written in source code. Many people generally associate the concept of literals with performance “cheapness”: A literal always seems to consume very little resource. Is this true? This post discusses the hidden performance cost of literals in JavaScript.
Distinction Between Object and Primitive Literals
Literals can be divided into two categories: Literals that represent objects and literals that represent primitives.
An Example of an Object Literal
Consider the following pair of code snippets:
const a = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
for (let i = 0; i < 10000; ++i) {
a.sort();
}
for (let i = 0; i < 10000; ++i) {
[1, 2, 3, 4, 5, 6, 7, 8, 9, 10].sort();
}
The first code snippet is expected to run faster, because the second code snippet constructs the
array 10000 times, while the first code snippet only constructs it once. The first code snippet
looks up the value of a
10000 times, which is faster than constructing the array 10000 times. Our
experiments with jsbench.me and Chrome 126 showed that the second code snippet was about 20.78%
slower.
An Example of a Primitive Literal
Now we modify the code snippets above slightly to the following pair of code snippets:
const b = 1;
for (let i = 0; i < 10000; ++i) {
b + 3;
}
for (let i = 0; i < 10000; ++i) {
1 + 3;
}
They are expected to take similar time for a JavaScript engine to execute. Although the second code
snippet constructs 1
10000 times, creating 1
is still faster than looking up the value of b
in
the first code snippet. Our experiments with jsbench.me and Chrome 126 showed that they were
similarly fast with less than 1% difference in their running time.
Comparison
Although in both pairs of code snippets [1, ...]
and 1
are literals, the former is an object
while the latter is a primitive. Generally speaking, constructing an object is relatively more
expensive than looking up the value of a variable. Therefore, the second pair of code snippets has
performance difference while the first pair does not. We now conclude: An object literal is not
cheap in terms of performance. If a line with an object literal is expected to be executed multiple
times, Performance-wise it is usually a good idea to consider assigning it to a variable beforehand
and using this variable instead.
Common Performance Trap: No-op Functions
One literal regarding which people tend to forget this point is no-op functions, such as () => {}
.
When a placeholder callback is needed, people often throw in () => {}
without much thought.
However, a JavaScript engine always creates a new object () => {}
in the process. For example, our
experiments with jsbench.me and Chrome 127 showed that the second code snippet below was about
85.06% slower than the first code snippet.
const noop = () => {};
for (let i = 0; i < 10000; ++i) {
noop;
}
for (let i = 0; i < 10000; ++i) {
() => {};
}
For better performance, consider a global no-op function in your program, or one from a utility
library that you are already using, such as lodash’s noop
function.
A Tricky Object Literal: Regular Expressions
Consider the following two code snippets. Which one is faster? Or, are they similarly fast?
const b = /a*b/;
for (let i = 0; i < 10000; ++i) {
b.exec("aab");
}
for (let i = 0; i < 10000; ++i) {
/a*b/.exec("aab");
}
The answer is: The first code snippet is faster. Our experiments with jsbench.me and Chrome 126 showed that the second code snippet was about 7.19% slower.
If you find this answer counterintuitive, chances are you have conflated the timing of regular expression compilation versus regular expression object creation.
Two Ways to Create a Regular Expression
JavaScript permits creating regular expressions using literals. For example,
const re = /a*b/;
Alternatively, one can write:
const re = new RegExp("a*b");
According to MDN, /a*b/
is more efficient than new RegExp("a*b")
because:
Regular expression literals provide compilation of the regular expression when the script is loaded… Using the constructor function provides runtime compilation of the regular expression.
In other words, an optimized JavaScript engine compiles the regular expression with /a*b/
when
loading the script while it only compiles the regular expression with new RegExp("a*b")
when
executing that line of code. No text mentions the timing of creating a regular expression
(RegExp
) object—Compiling the regular expression is only one intermediate step in creating
a regular expression object.
The Real Performance Cost of a Regular Expression Literal
The ECMA Standard says:
A regular expression literal is an input element that is converted to a RegExp object each time the literal is evaluated.
In other words, when a JavaScript engine encounters a regular expression literal during runtime, it
constructs a RegExp
object. This is not so different from other objects such as the [1, ...]
array in the previous section, except that the JavaScript engine has done some work ahead of time,
i.e., compiling the regular expression.
Now let’s revisit the two code snippets at the beginning of the section. The first code snippet is
faster at the beginning of this section because it only constructs the RegExp
object once, while
the second code snippet constructs the RegExp
object 10000 times.
Non-Fundamental Primitive Types
Which of the following code snippets is faster? Or, are they similarly fast?
const s = "aaaaaaaaaaaaaaaaaaaa";
for (let i = 0; i < 10000; ++i) {
s + "b";
}
for (let i = 0; i < 10000; ++i) {
"aaaaaaaaaaaaaaaaaaaa" + "b";
}
Our experiments with Chrome 126 and Firefox 128 on jsbench.me showed that they were similarly fast with less than 1% difference in their running time.
If you expect the first code snippet to be faster, the following may be what you may have thought:
A Mistakened Analysis
String is not fundamental to mainstream architectures of modern computers. This means that
constructing a long string object is likely slower than looking up a variable. Since the second code
snippet constructs the "a..."
string 10000 times but the first code snippet only constructs it
once, the first code snippet is faster.
Why Are They Similar in Running Time?
The mistake in the analysis above is that it fails to account for the immutability of JavaScript
primitives. Since a string is a
JavaScript primitive, a string is never modified. Hence, when an optimized JavaScript engine
executes the second code snippet, it is able to cache the constructed "a..."
string and reuse it
in subsequent iterations. This is not necessarily true if the "a..."
string were an object. In
this case, to optimize, the JavaScript engine must be smart enough to figure out that the object is
never modified, which may or may not be the case.
Verification With V8
We can loosely verify this caching behavior with V8. Execute the following in shell:
node --allow-natives-syntax <<EOF
for (let i = 0; i < 3; ++ i) {
%DebugPrint("aaaaaaaaaaaaaaaaaaaa")
}
EOF
--allow-natives-syntax
enables the V8 intrinsic
%DebugPrint
(implemented
here),
which prints the address of the
variable
among other things. The output repeats the following line 3 times:
DebugPrint: 0x3b6cc0a94429: [String] in OldSpace: #aaaaaaaaaaaaaaaaaaaa
The hexadecimal number after DebugPrint:
is the address of the "a..."
string and is likely
different every time you run the script. The line above is repeated 3 times, which implies that V8
has cached the "a..."
string and reused it.
Conclusion
A literal representing an object literal is usually more expensive than looking up a variable in terms of performance. If a line with an object literal is expected to be executed multiple times, Performance-wise it is usually a good idea to consider assigning it to a variable beforehand and using this variable instead. A common situation in which people tend to forget this point is when using no-op functions, such as
() => {}
.Regular expression (
RegExp
) literals also represent objects. Hence, Item 1 applies. If this seems counterintuitive to you, chances are you have conflated the timing of regular expression compilation versus regular expression object creation.Literals of primitive types can be non-fundamental to the CPU architecture, such as String and BigInt. Hence, such a literal is usually more expensive than looking up a variable. However, Item 1 does not apply because an optimized JavaScript engine is able to cache such literals due to their immutability.