From 6bb6c670e3348e4fce77cff0f9610c12e8fb98a0 Mon Sep 17 00:00:00 2001 From: Matteo Collina Date: Mon, 12 Oct 2026 05:26:23 +0000 Subject: [PATCH] perf: skip per-character loop for safe long strings --- lib/internals/querystring.js | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/lib/internals/querystring.js b/lib/internals/querystring.js index 35e6fd0..ec5ddc5 100644 --- a/lib/internals/querystring.js +++ b/lib/internals/querystring.js @@ -24,6 +24,12 @@ const noEscape = new Int8Array([ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, // 112 - 127 ]); +const needEscape = /[^A-Za-z0-9!\-.~'()*]/; +// Below this length the tight per-character scanning loop is faster than a +// native regex pre-scan, because the regex call overhead dominates for short +// inputs. Above it, escaping only the tail is a clear win. +const SHORT_STRING_LENGTH = 16; + /** * @param {string} str * @returns {string} @@ -32,9 +38,17 @@ function encodeString(str) { const len = str.length; if (len === 0) return ""; - let out = ""; - let lastPos = 0; - let i = 0; + // Fast path: for short strings we scan every character with a tight loop. For + // longer ones we first use a native regex scan to find the first character + // that needs escaping. When the whole string is already safe (the common case + // for already-escaped values) we can return it unchanged without the loop, and + // otherwise we resume the loop right where escaping starts. + const firstEscape = len < SHORT_STRING_LENGTH ? 0 : str.search(needEscape); + if (firstEscape === -1) return str; + + let out = len < SHORT_STRING_LENGTH ? "" : str.slice(0, firstEscape); + let lastPos = firstEscape; + let i = firstEscape; outer: for (; i < len; i++) { let c = str.charCodeAt(i);