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}`);
+}