โ† back to journal

In Node, += beats join until the string gets huge

2026-10-15 ยท Kit Wickham

The folk rule is "build strings with an array and join, not +=". In Node 26.12 the rule is backwards for most sizes. Appending a 10-character piece in a loop with += was as fast as or faster than push-then-join up to about a million pieces (a 10 MB string). Past roughly three million pieces, join was about 2x faster.

The test

Append the same 10-character piece N times, then (for the "+= total" column) read one character so the result is flattened. Median of 7 runs per cell, in a container with 2 CPUs and 3 GB. Times in milliseconds.

pieces+= (build)+= (build + first read)push + jointemplate `${s}${p}`
100,0000.540.861.00.26
1,000,0003.24.28.24.0
2,000,00030382342
3,000,00069904578
5,000,00014615669150

Why

V8 doesn't copy on +=. It makes a "cons string", a small node pointing at the two halves, so each append is a constant-size allocation and the characters are copied once, later, when something needs them flat. That last step was cheap here (14 ms to flatten 50 MB). What grows is the pile of nodes: five million of them is a lot for the garbage collector to trace, and that is where the build time jumps between 1 and 2 million pieces. join pays for an array of pointers instead, then one allocation and one copy.

So what

For typical strings (log lines, HTML, CSV of a few thousand rows) just use += or a template; it's fastest and clearest. If you are assembling tens of megabytes from millions of tiny pieces, collect them and join, or write to a stream. The numbers are noisy near the crossover (the 4M join cell came out faster than the 3M one), so read the crossover as "somewhere between 1 and 3 million pieces", not a fixed point.

Method: one machine, a 20-line script, Node 26.12 in a container. One piece length and one piece shape; a program that also holds lots of other live objects will cross over sooner.