From 07a4b400b5f3f3a5be47abe4e87c415cb7f79de1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrg=C3=BCn=20Day=C4=B1o=C4=9Flu?= Date: Sun, 15 Mar 2026 16:51:29 +0100 Subject: [PATCH] url: optimize URLSearchParams set/delete duplicate handling --- benchmark/url/url-searchparams-mutation.js | 43 +++++++++++++++++++ lib/internal/url.js | 49 +++++++++++++++------- 2 files changed, 78 insertions(+), 14 deletions(-) create mode 100644 benchmark/url/url-searchparams-mutation.js diff --git a/benchmark/url/url-searchparams-mutation.js b/benchmark/url/url-searchparams-mutation.js new file mode 100644 index 00000000000000..cb01793037437e --- /dev/null +++ b/benchmark/url/url-searchparams-mutation.js @@ -0,0 +1,43 @@ +'use strict'; +const common = require('../common.js'); + +const bench = common.createBenchmark(main, { + method: ['set', 'delete'], + type: ['unique', 'duplicates'], + count: [10, 1000], + n: [1e4], +}); + +function buildSeed(type, count) { + const parts = new Array(count); + + if (type === 'duplicates') { + for (let i = 0; i < count; i++) { + parts[i] = `dup=${i}`; + } + } else { + for (let i = 0; i < count; i++) { + parts[i] = `k${i}=${i}`; + } + } + + return new URLSearchParams(parts.join('&')); +} + +function main({ method, type, count, n }) { + const seed = buildSeed(type, count); + const key = type === 'duplicates' ? 'dup' : 'k0'; + const mutate = method === 'set' ? + (params) => params.set(key, 'updated') : + (params) => params.delete(key); + + for (let i = 0; i < 1e3; i++) { + mutate(new URLSearchParams(seed)); + } + + bench.start(); + for (let i = 0; i < n; i++) { + mutate(new URLSearchParams(seed)); + } + bench.end(n); +} diff --git a/lib/internal/url.js b/lib/internal/url.js index 813208f39e6622..328f8c683f7393 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -495,26 +495,37 @@ class URLSearchParams { const list = this.#searchParams; name = StringPrototypeToWellFormed(`${name}`); + const len = list.length; + let write = 0; if (value !== undefined) { value = StringPrototypeToWellFormed(`${value}`); - for (let i = 0; i < list.length;) { + for (let i = 0; i < len; i += 2) { if (list[i] === name && list[i + 1] === value) { - list.splice(i, 2); - } else { - i += 2; + continue; + } + if (write !== i) { + list[write] = list[i]; + list[write + 1] = list[i + 1]; } + write += 2; } } else { - for (let i = 0; i < list.length;) { + for (let i = 0; i < len; i += 2) { if (list[i] === name) { - list.splice(i, 2); - } else { - i += 2; + continue; + } + if (write !== i) { + list[write] = list[i]; + list[write + 1] = list[i + 1]; } + write += 2; } } + if (write !== len) + list.length = write; + if (this.#context) { setURLSearchParamsModified(this.#context); } @@ -594,24 +605,34 @@ class URLSearchParams { const list = this.#searchParams; name = StringPrototypeToWellFormed(`${name}`); value = StringPrototypeToWellFormed(`${value}`); + const len = list.length; // If there are any name-value pairs whose name is `name`, in `list`, set // the value of the first such name-value pair to `value` and remove the // others. let found = false; - for (let i = 0; i < list.length;) { + let write = 0; + for (let i = 0; i < len; i += 2) { const cur = list[i]; + let keep = true; if (cur === name) { if (!found) { - list[i + 1] = value; + list[write] = cur; + list[write + 1] = value; found = true; - i += 2; } else { - list.splice(i, 2); + keep = false; } - } else { - i += 2; + } else if (write !== i) { + list[write] = cur; + list[write + 1] = list[i + 1]; } + if (keep) + write += 2; + } + + if (found && write !== len) { + list.length = write; } // Otherwise, append a new name-value pair whose name is `name` and value