From bcfe0f4540a7ed68161123fecb62a5576fb47674 Mon Sep 17 00:00:00 2001 From: Roberto Bianchi Date: Thu, 15 Oct 2026 12:28:45 +0200 Subject: [PATCH] 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 --- benchmark/url/percent-encode.js | 50 +++++++++++++++++++ lib/internal/querystring.js | 10 +++- .../test-querystring-escape-two-byte.js | 13 +++++ 3 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 benchmark/url/percent-encode.js create mode 100644 test/parallel/test-querystring-escape-two-byte.js diff --git a/benchmark/url/percent-encode.js b/benchmark/url/percent-encode.js new file mode 100644 index 000000000000..1752f1d0909c --- /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'], + 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 46fdf564dbcc..c05c1d57ff3b 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 000000000000..f6ac02661c83 --- /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}`); +}