Commit 544340a279d for nodejs
commit 544340a279dab67b83ad1d08913d9bce55fa4b9b
Author: Roberto Bianchi <roberto.bianchi@spendesk.com>
Date: Thu Nov 5 20:25:00 2026 +0100
querystring: precompute Latin-1 escapes
Reuse precomputed escapes for Latin-1 characters instead of joining
two byte escapes on every occurrence. This reduces repeated work
when querystring and URLSearchParams serialize these characters.
Signed-off-by: Roberto Bianchi <roberto.bianchi@spendesk.com>
PR-URL: https://github.com/nodejs/node/pull/66756
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
diff --git a/benchmark/url/percent-encode.js b/benchmark/url/percent-encode.js
new file mode 100644
index 00000000000..e81dad1356c
--- /dev/null
+++ b/benchmark/url/percent-encode.js
@@ -0,0 +1,50 @@
+'use strict';
+
+const common = require('../common');
+const assert = require('node:assert');
+const querystring = require('node:querystring');
+
+const bench = common.createBenchmark(main, {
+ n: [1e5],
+ api: ['querystring', 'URLSearchParams', 'URLSearchParams-new-instance'],
+ type: ['ascii', 'latin1', 'mixed', 'twobyte', 'bmp', 'astral'],
+ size: [16, 256],
+});
+
+function main({ n, api, type, size }) {
+ const units = {
+ ascii: 'abc/? &=',
+ latin1: '\u00e9',
+ mixed: 'abcd\u00e9',
+ twobyte: '\u03b1',
+ bmp: '\u4e2d',
+ astral: '\ud83d\ude00',
+ };
+ const unit = units[type];
+ const input = unit.repeat(Math.ceil(size / unit.length)).slice(0, size);
+ const params = new URLSearchParams([['value', input]]);
+ const expected = api === 'querystring' ? encodeURIComponent(input) :
+ `value=${encodeURIComponent(input).replaceAll('%20', '+')}`;
+ let actual;
+ let checksum = 0;
+ bench.start();
+ if (api === 'querystring') {
+ for (let index = 0; index < n; index++) {
+ actual = querystring.escape(input);
+ checksum += actual.charCodeAt(actual.length - 1);
+ }
+ } else if (api === 'URLSearchParams') {
+ for (let index = 0; index < n; index++) {
+ actual = params.toString();
+ checksum += actual.charCodeAt(actual.length - 1);
+ }
+ } else {
+ for (let index = 0; index < n; index++) {
+ actual = new URLSearchParams({ value: input }).toString();
+ checksum += actual.charCodeAt(actual.length - 1);
+ }
+ }
+ bench.end(n);
+ assert.strictEqual(actual, expected);
+ assert.strictEqual(checksum, n * expected.charCodeAt(expected.length - 1));
+}
diff --git a/lib/internal/querystring.js b/lib/internal/querystring.js
index 46fdf564dbc..c05c1d57ff3 100644
--- a/lib/internal/querystring.js
+++ b/lib/internal/querystring.js
@@ -17,6 +17,12 @@ for (let i = 0; i < 256; ++i)
StringPrototypeToUpperCase((i < 16 ? '0' : '') +
NumberPrototypeToString(i, 16));
+const latin1HexTable = new Array(128);
+for (let codePoint = 0x80; codePoint < 0x100; codePoint++) {
+ latin1HexTable[codePoint - 0x80] = hexTable[0xC0 | (codePoint >> 6)] +
+ hexTable[0x80 | (codePoint & 0x3F)];
+}
+
const isHexTable = new Int8Array([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
@@ -76,8 +82,8 @@ function encodeStr(str, noEscapeTable, hexTable) {
// Multi-byte characters ...
if (c < 0x800) {
lastPos = i + 1;
- out += hexTable[0xC0 | (c >> 6)] +
- hexTable[0x80 | (c & 0x3F)];
+ out += c < 0x100 ? latin1HexTable[c - 0x80] :
+ hexTable[0xC0 | (c >> 6)] + hexTable[0x80 | (c & 0x3F)];
continue;
}
if (c < 0xD800 || c >= 0xE000) {
diff --git a/test/parallel/test-querystring-escape-two-byte.js b/test/parallel/test-querystring-escape-two-byte.js
new file mode 100644
index 00000000000..f6ac02661c8
--- /dev/null
+++ b/test/parallel/test-querystring-escape-two-byte.js
@@ -0,0 +1,13 @@
+'use strict';
+
+require('../common');
+const assert = require('node:assert');
+const querystring = require('node:querystring');
+
+for (let codePoint = 0x80; codePoint < 0x800; codePoint++) {
+ const character = String.fromCharCode(codePoint);
+ const input = `prefix${character}${character}/suffix`;
+ const expected = encodeURIComponent(input);
+ assert.strictEqual(querystring.escape(input), expected);
+ assert.strictEqual(new URLSearchParams([[input, input]]).toString(), `${expected}=${expected}`);
+}