The Lab · Development
String Concatenation vs Array Join: I Timed the Old Advice
I timed string concatenation against array push and join across four loop sizes, and the old quadratic-cost advice did not hold.
Each line jumps to its section
- I timed string += against array push then join at four loop sizes in Node 22
- At 1 million iterations, += finished in 339ms against 437ms for push and join
- The two methods tied at 10,000 and 100,000 iterations, within a few percent either way
- The old advice to always use array join for loops no longer matches what I measured
For years I've written loops that build a string with array.push() followed by .join() instead of plain +=, because that's the advice everyone repeats: string concatenation in a loop is quadratic, arrays are the safe choice. I finally timed both, at sizes from a thousand iterations to a million, and the advice didn't hold up.
I'd never actually run the numbers myself. I'd just inherited the rule, the way most of us inherit most performance advice, from a blog post or a code review comment that sounded confident enough not to question.
The Advice I Was Following
The reasoning behind "always use join" goes back to how strings used to work in JavaScript engines. A string was an immutable block of memory, so every += had to allocate a new block and copy the old contents into it, then throw the old block away. Do that inside a loop and you're copying more data on every pass, which is the textbook definition of quadratic behavior.
Array push is O(1) on average. Join happens once, at the end, over the whole array. That's genuinely a different algorithmic shape, and it's why the advice became a reflex: avoid the loop that copies growing amounts of data on every iteration, do the expensive step once at the end instead.
I never questioned it. It's logical, it's been repeated in enough style guides and code reviews that it stopped feeling like a claim and started feeling like a fact.
A fact I'd never checked myself, which is exactly the kind of fact worth checking.
The reasoning isn't wrong about the engines it was written for. Older JavaScript engines, and plenty of languages that still work this way today, genuinely do pay a copy cost on every += inside a loop, and the array-and-join pattern genuinely does avoid it there. The question I hadn't asked was whether that description still matched the engine sitting on my own machine.
What I Actually Measured
I wrote two functions. One built a string with a loop and +=, appending a short piece of text on each pass. The other pushed the same pieces into an array and called .join() once at the end. I ran both at four sizes, 1,000, 10,000, 100,000, and 1,000,000 iterations, warmed each function up first so the engine's JIT compiler had already optimized the hot path, then timed five trials per size with process.hrtime.bigint() and averaged them.
At 1,000 iterations, += finished in about 0.18 milliseconds against 0.33 for push and join, concatenation clearly faster. At 10,000, the two were close enough to call a tie, 0.84 against 0.78 milliseconds, join edging ahead by a hair. At 100,000, another near tie, 14.4 against 14.8 milliseconds. At 1,000,000 iterations, the size where quadratic behavior should show up clearly if it still existed, += finished in 339 milliseconds against 437 for push and join.
Concatenation won or tied at every single size I tested. It never lost by a meaningful margin, and at the largest size, the one place quadratic growth would be impossible to hide, it won by close to 30 percent.
A million iterations is where a quadratic algorithm would have fallen apart. It didn't. That one result told me more than the other three combined.
I ran the whole set a second time to make sure the result wasn't a fluke of whatever else was running on the machine that moment. The ranking held, concatenation ahead or tied at every size, though the exact percentages shifted a few points each run, the usual noise you get from timing anything on a shared machine.
Why the Old Advice Stopped Being True
Modern JavaScript engines, V8 included, don't store a string as one flat block of memory once you start building it with repeated concatenation. They use a structure usually called a rope, or a cons-string: instead of copying both sides into a new block immediately, the engine just records "this string is the first part followed by the second part" and only flattens the whole thing into one block when something actually needs to read it as a single string.
That one change removes the quadratic cost almost entirely. Each += becomes a cheap operation that links two pieces together instead of an expensive one that copies everything that came before. The flattening still has to happen eventually, but it happens once, not once per append, which is exactly the same shape of savings the array-and-join pattern was designed to get.
Nobody announced this loudly. It's the kind of internal engine change that ships quietly across several versions and never gets a release note of its own, which is exactly how a piece of advice can stay technically true for one JavaScript engine from a decade ago and false for the one actually running your code today.
I didn't expect to find that out from a four-line benchmark. I expected to confirm what I already believed.
I didn't.
That's the part I'd flag to anyone repeating performance advice secondhand. The advice usually describes a real mechanism, rope strings versus flat buffers, O(1) push versus O(n) copy, and the mechanism is real. What goes stale is the assumption that the mechanism still applies to the specific engine running the specific code in front of you, years after someone first wrote the advice down.
What I'd Actually Write Now
For a loop that builds a string, I'd reach for plain += first. It reads simpler, there's no array to manage and no .join() call to remember at the end, and based on what I measured, it's never meaningfully slower than the alternative and often faster.
I'd still reach for array.push() and .join() in one specific case: when I already have the pieces as an array for some other reason, iterating over existing data rather than generating new pieces one at a time. In that situation .join() is the natural operation, not a performance workaround, and there's no reason to rewrite it into a loop just to use +=. That's a readability choice, not a speed one, and it's the right reason to pick a method either way.
What I wouldn't do anymore is treat "array and join" as the automatically correct choice for every string-building loop, the way a style guide or a code review comment sometimes insists on it. That advice was true once. I measured it and it isn't anymore, at least not on the Node version I'm running today, and a rule that quietly stopped being true is worse than no rule, because it still sounds authoritative when someone repeats it.
I'll probably still get a code review comment about this at some point, pointing me back to the array-and-join pattern as the "correct" one. I'd rather show the numbers than argue the theory. A screenshot of a benchmark ends that conversation faster than any explanation of cons-strings ever will.
I keep coming back to the same habit on this blog: measure the thing before you repeat the advice about the thing, the same instinct that had me counting debounce calls instead of trusting the usual explanation of when to use it. Engines change. Advice about engines doesn't always catch up.
Bottom Line
If you're building a string in a loop in modern Node, don't assume array-and-join is faster without checking. On every size I tested, from a thousand iterations to a million, plain += matched or beat it, and the old quadratic-cost argument doesn't hold on the V8 build shipping in Node 22.
Use whichever one reads clearer in the code you're writing, since the speed difference won't be the thing that decides it anymore. If you're optimizing a loop that runs millions of times and every millisecond counts, benchmark your actual case rather than trusting either side of this, mine or the old advice both.
Engines keep changing underneath code that doesn't. The rule worth keeping isn't "use arrays" or "use concatenation." It's run the loop yourself before you repeat someone else's answer about it, because the answer has an expiration date nobody prints on it.