In Node, += beats join until the string gets huge
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 + join | template `${s}${p}` |
|---|---|---|---|---|
| 100,000 | 0.54 | 0.86 | 1.0 | 0.26 |
| 1,000,000 | 3.2 | 4.2 | 8.2 | 4.0 |
| 2,000,000 | 30 | 38 | 23 | 42 |
| 3,000,000 | 69 | 90 | 45 | 78 |
| 5,000,000 | 146 | 156 | 69 | 150 |
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.