JavaScript offers several ways to concatenate strings. Let’s learn how to concatenate strings and which one is the best performance.
Plus Operator and Template Strings/Literal (backtick/backquote)
Using the plus operator to concatenate strings is supported by many languages. We can put variables into a statement without plus operator by using “`”. It is called Template Literal which was called Template String before.
const str1 = "Hello";
const str2 = "I'm";
const str3 = "Yuto";
const msg = str1 + "," + str2 + " " + str3;
const msg2 = `${str1}, ${str2} ${str3}`;
const msg3 = str1.concat(str2, str3);
console.log(msg);
// Hello,I'm Yuto
console.log(msg2);
// Hello, I'm Yuto
Template Literal is not readable
Template Literal is useful because we can put everything in the same statement. However, it gets a bit unreadable when the variable names are long.
const longKeywordVariable = "key";
const longKeywordNumber = 12
const longKeywordValue = "value";
const msg = `${longKeywordVariable}-${longKeywordNumber}-${longKeywordValue}`;
console.log(msg);
// key-12-value
We may need to scroll to see what values are used. The first improvement is the following.
const msg2 = `${longKeywordVariable}-
${longKeywordNumber}-
${longKeywordValue}`;
It’s more readable than before but it doesn’t work as expected. The result is the following.
// key-
// 12-
// value
Since it is the same statement and each variable is written on a new line. It needs to be separated as follows.
const msg3 = `${longKeywordVariable}-`
+ `${longKeywordNumber}-`
+ `${longKeywordValue}`;
console.log(msg3);
// key-12-value
Furthermore, using Array.prototype.join
function is better when the variables are concatenated with the same string.
const msg4 = [
longKeywordVariable,
longKeywordNumber,
longKeywordValue,
].join("-");
console.log(msg4);
// key-12-value
Performance comparison concat vs Template Literal vs Array.join
I measured the performance difference. The common function is the following.
import { performance } from "perf_hooks";
function doLoop(cb: () => void) {
for (let i = 0; i < 1000000; i++) {
cb();
}
}
type TestDataType = { startStr: string, copiedStr: string };
const copiedStr = "x".repeat(100);
function measure(title: string, cb: (args: TestDataType) => void) {
const start = performance.now();
const args = { startStr: "", copiedStr };
cb(args);
const result = performance.now() - start;
console.log(`result(${title}): ${Math.round(result)} ms`);
}
What we need to do is to define the callback function and set it to cb. I defined the following functions.
const runConcat = () => measure("concat with", (args: TestDataType) => {
const cb = () => {
args.startStr = args.startStr.concat(args.copiedStr);
}
doLoop(cb);
});
const runPlus = () => measure("+ operator", (args: TestDataType) => {
const cb = () => { args.startStr += args.copiedStr };
doLoop(cb);
});
const runLiteral = () => measure("Template Literal", (args: TestDataType) => {
const cb = () => {
args.startStr = `${args.startStr}${args.copiedStr}`;
};
doLoop(cb);
});
const runJoin = () => measure("Array.join with loop", () => {
const result: string[] = [];
doLoop(() => {
result.push(copiedStr);
});
result.join("");
});
const runReduce = () => measure("Array.reduce with loop", () => {
const result: string[] = [];
doLoop(() => {
result.push(copiedStr);
});
result.reduce((pre, cur) => pre + cur);
});
// ------ without loop -------
const array: string[] = [];
doLoop(() => {
array.push(copiedStr);
});
const runJoinWithout = () => measure("Array.join without loop", () => array.join(""));
const runReduceWithout = () => measure("Array.reduce without loop", () => {
array.reduce((pre, cur) => pre + cur);
});
for (let i = 0; i < 10; i++) {
runConcat();
// runLiteral();
// runPlus();
// runJoin();
// runJoinWithout();
// runReduce();
// runReduceWithout();
}
The last for loop is the statement for the main process. I executed only one function at a time so that I could measure without side effects. This is the result including for-loop.
N/A | concat | join | Template Literal | reduce | + operator |
---|---|---|---|---|---|
– | 89 | 226 | 89 | 135 | 88 |
– | 245 | 238 | 247 | 183 | 253 |
– | 163 | 234 | 188 | 156 | 170 |
– | 162 | 259 | 168 | 156 | 156 |
– | 154 | 376 | 166 | 166 | 172 |
– | 165 | 324 | 168 | 162 | 155 |
– | 161 | 258 | 176 | 156 | 163 |
– | 159 | 235 | 167 | 164 | 152 |
– | 176 | 228 | 174 | 191 | 98 |
– | 153 | 253 | 182 | 176 | 172 |
Ave | 162.7 | 263.1 | 172.5 | 164.5 | 157.9 |
This result doesn’t include for-loop which means using the array directly.
N/A | join without | reduce without |
---|---|---|
– | 197 | 88 |
– | 154 | 94 |
– | 193 | 176 |
– | 159 | 95 |
– | 171 | 89 |
– | 156 | 94 |
– | 174 | 97 |
– | 175 | 90 |
– | 169 | 89 |
– | 220 | 96 |
Ave | 176.8 | 100.8 |
The compiler couldn’t build the following code because the number of arrays is too big to put the argument.
const runConcatWithout = () => {
measure("concat without loop", (args: TestDataType) => {
args.startStr.concat(...array);
});
}
// RangeError: Maximum call stack size exceeded
By the way, the versions when I measured this is the following.
$ tsc -v
Version 4.0.3
$ node -v
v14.13.0
Summary
Array.prototype.join
function is the slowest to concatenate string. Others are almost the same performance. In most cases, we don’t have to take care of which one to use. Its difference is very small even though the string is long.
Comments
The readability concern with long variable names is unwarranted. The following is perfectly legal, thanks to JavaScript’s handling of white-space.
const msg2 = `${
longKeywordVariable
}-${
longKeywordNumber
}-${
longKeywordValue
}`;
Thank you for posting your idea. It’s also a nice way.