Commit 6a6e09e8344 for nodejs
commit 6a6e09e834433d7c8f75ba45ad79fea9e76473c6
Author: Yagiz Nizipli <yagiz@nizipli.com>
Date: Sat Sep 26 16:34:53 2026 -0400
querystring: speed up default parse and unescape
Skip the %XX walk in unescapeBuffer when the input has no '%'.
Add a dedicated '&'/'=' scanner for the default parse path so
it does not build separator code arrays or run the multi-char
state machine.
Official benchmark/querystring/querystring-parse.js:
encodemany is about 38% faster, manyblankpairs about 17%,
encodelast about 10%, noencode about 8%.
Official querystring-unescapebuffer.js with no escapes is
about 36% faster.
Assisted-by: a closed-source coding agent
Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
Co-authored-by: Yagiz Nizipli <anonrig@users.noreply.github.com>
PR-URL: https://github.com/nodejs/node/pull/66175
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Gürgün Dayıoğlu <hey@gurgun.day>
diff --git a/lib/querystring.js b/lib/querystring.js
index 30e159d647b..6aca65dee5b 100644
--- a/lib/querystring.js
+++ b/lib/querystring.js
@@ -32,6 +32,7 @@ const {
ObjectKeys,
String,
StringPrototypeCharCodeAt,
+ StringPrototypeIndexOf,
StringPrototypeSlice,
decodeURIComponent,
} = primordials;
@@ -82,6 +83,11 @@ const unhexTable = new Int8Array([
* @returns {string}
*/
function unescapeBuffer(s, decodeSpaces) {
+ // No encoded bytes: latin1-copy the string without walking %XX.
+ if (StringPrototypeIndexOf(s, '%') === -1) {
+ if (!decodeSpaces || StringPrototypeIndexOf(s, '+') === -1)
+ return Buffer.from(s, 'latin1');
+ }
const out = Buffer.allocUnsafe(s.length);
let index = 0;
let outIndex = 0;
@@ -303,6 +309,120 @@ function addKeyVal(obj, key, value, keyEncoded, valEncoded, decode) {
}
}
+/**
+ * Default querystring parser: separator '&', equals '=', native unescape.
+ * Single-character separators avoid the sep/eq index state machine.
+ * @param {string} qs
+ * @param {Record<string, string | string[]>} obj
+ * @returns {Record<string, string | string[]>}
+ */
+function parseAmpEq(qs, obj) {
+ let pairs = 1000;
+ let lastPos = 0;
+ let eqPos = -1;
+ let key = '';
+ let value = '';
+ let keyEncoded = false;
+ let valEncoded = false;
+ let encodeCheck = 0;
+ const plusChar = ' ';
+ const len = qs.length;
+
+ for (let i = 0; i < len; ++i) {
+ const code = StringPrototypeCharCodeAt(qs, i);
+
+ if (code === 38) { // '&'
+ const end = i;
+ if (eqPos === -1) {
+ if (lastPos < end)
+ key += StringPrototypeSlice(qs, lastPos, end);
+ else if (key.length === 0) {
+ if (--pairs === 0)
+ return obj;
+ lastPos = i + 1;
+ encodeCheck = 0;
+ continue;
+ }
+ } else if (lastPos < end) {
+ value += StringPrototypeSlice(qs, lastPos, end);
+ }
+
+ addKeyVal(obj, key, value, keyEncoded, valEncoded, qsUnescape);
+
+ if (--pairs === 0)
+ return obj;
+ keyEncoded = false;
+ valEncoded = false;
+ key = '';
+ value = '';
+ encodeCheck = 0;
+ lastPos = i + 1;
+ eqPos = -1;
+ continue;
+ }
+
+ if (eqPos === -1) {
+ if (code === 61) { // '='
+ if (lastPos < i)
+ key += StringPrototypeSlice(qs, lastPos, i);
+ encodeCheck = 0;
+ lastPos = i + 1;
+ eqPos = i;
+ continue;
+ }
+ if (!keyEncoded) {
+ if (code === 37) {
+ encodeCheck = 1;
+ } else if (encodeCheck > 0) {
+ if (isHexTable[code] === 1) {
+ if (++encodeCheck === 3)
+ keyEncoded = true;
+ } else {
+ encodeCheck = 0;
+ }
+ }
+ }
+ if (code === 43) {
+ if (lastPos < i)
+ key += StringPrototypeSlice(qs, lastPos, i);
+ key += plusChar;
+ lastPos = i + 1;
+ }
+ continue;
+ }
+
+ if (code === 43) {
+ if (lastPos < i)
+ value += StringPrototypeSlice(qs, lastPos, i);
+ value += plusChar;
+ lastPos = i + 1;
+ } else if (!valEncoded) {
+ if (code === 37) {
+ encodeCheck = 1;
+ } else if (encodeCheck > 0) {
+ if (isHexTable[code] === 1) {
+ if (++encodeCheck === 3)
+ valEncoded = true;
+ } else {
+ encodeCheck = 0;
+ }
+ }
+ }
+ }
+
+ if (lastPos < len) {
+ if (eqPos === -1)
+ key += StringPrototypeSlice(qs, lastPos);
+ else
+ value += StringPrototypeSlice(qs, lastPos);
+ } else if (eqPos === -1 && key.length === 0) {
+ return obj;
+ }
+
+ addKeyVal(obj, key, value, keyEncoded, valEncoded, qsUnescape);
+ return obj;
+}
+
/**
* Parse a key/val string.
* @param {string} qs
@@ -321,6 +441,16 @@ function parse(qs, sep, eq, options) {
return obj;
}
+ // Default '&' / '=' / unescape: single-char scanner, no sep/eq arrays.
+ if ((sep === undefined || sep === '&') &&
+ (eq === undefined || eq === '=') &&
+ QueryString.unescape === qsUnescape &&
+ (!options ||
+ (options.maxKeys === undefined &&
+ options.decodeURIComponent === undefined))) {
+ return parseAmpEq(qs, obj);
+ }
+
const sepCodes = (!sep ? defSepCodes : charCodes(String(sep)));
const eqCodes = (!eq ? defEqCodes : charCodes(String(eq)));
const sepLen = sepCodes.length;