Skip to content

Commit d243115

Browse files
anonrigRafaelGSS
authored andcommitted
url: improve URLSearchParams creation performance
PR-URL: #47190 Reviewed-By: Tiancheng "Timothy" Gu <[email protected]>
1 parent 760e13c commit d243115

File tree

2 files changed

+49
-25
lines changed

2 files changed

+49
-25
lines changed

lib/internal/url.js

+45-25
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
const {
44
Array,
5+
ArrayIsArray,
56
ArrayPrototypeJoin,
67
ArrayPrototypeMap,
78
ArrayPrototypePush,
@@ -156,52 +157,74 @@ function isURLSearchParams(self) {
156157
}
157158

158159
class URLSearchParams {
160+
[searchParams] = [];
161+
162+
// "associated url object"
163+
[context] = null;
164+
159165
// URL Standard says the default value is '', but as undefined and '' have
160166
// the same result, undefined is used to prevent unnecessary parsing.
161167
// Default parameter is necessary to keep URLSearchParams.length === 0 in
162168
// accordance with Web IDL spec.
163169
constructor(init = undefined) {
164-
if (init === null || init === undefined) {
165-
this[searchParams] = [];
170+
if (init == null) {
171+
// Do nothing
166172
} else if (typeof init === 'object' || typeof init === 'function') {
167173
const method = init[SymbolIterator];
168174
if (method === this[SymbolIterator]) {
169175
// While the spec does not have this branch, we can use it as a
170176
// shortcut to avoid having to go through the costly generic iterator.
171177
const childParams = init[searchParams];
172178
this[searchParams] = childParams.slice();
173-
} else if (method !== null && method !== undefined) {
179+
} else if (method != null) {
180+
// Sequence<sequence<USVString>>
174181
if (typeof method !== 'function') {
175182
throw new ERR_ARG_NOT_ITERABLE('Query pairs');
176183
}
177184

178-
// Sequence<sequence<USVString>>
179-
// Note: per spec we have to first exhaust the lists then process them
180-
const pairs = [];
185+
// The following implementationd differs from the URL specification:
186+
// Sequences must first be converted from ECMAScript objects before
187+
// and operations are done on them, and the operation of converting
188+
// the sequences would first exhaust the iterators. If the iterator
189+
// returns something invalid in the middle, whether it would be called
190+
// after that would be an observable change to the users.
191+
// Exhausting the iterator and later converting them to USVString comes
192+
// with a significant cost (~40-80%). In order optimize URLSearchParams
193+
// creation duration, Node.js merges the iteration and converting
194+
// iterations into a single iteration.
181195
for (const pair of init) {
182-
if ((typeof pair !== 'object' && typeof pair !== 'function') ||
183-
pair === null ||
184-
typeof pair[SymbolIterator] !== 'function') {
196+
if (pair == null) {
185197
throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]');
186-
}
187-
const convertedPair = [];
188-
for (const element of pair)
189-
ArrayPrototypePush(convertedPair, toUSVString(element));
190-
ArrayPrototypePush(pairs, convertedPair);
191-
}
198+
} else if (ArrayIsArray(pair)) {
199+
// If innerSequence's size is not 2, then throw a TypeError.
200+
if (pair.length !== 2) {
201+
throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]');
202+
}
203+
// Append (innerSequence[0], innerSequence[1]) to querys list.
204+
ArrayPrototypePush(this[searchParams], toUSVString(pair[0]), toUSVString(pair[1]));
205+
} else {
206+
if (((typeof pair !== 'object' && typeof pair !== 'function') ||
207+
typeof pair[SymbolIterator] !== 'function')) {
208+
throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]');
209+
}
192210

193-
this[searchParams] = [];
194-
for (const pair of pairs) {
195-
if (pair.length !== 2) {
196-
throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]');
211+
let length = 0;
212+
213+
for (const element of pair) {
214+
length++;
215+
ArrayPrototypePush(this[searchParams], toUSVString(element));
216+
}
217+
218+
// If innerSequence's size is not 2, then throw a TypeError.
219+
if (length !== 2) {
220+
throw new ERR_INVALID_TUPLE('Each query pair', '[name, value]');
221+
}
197222
}
198-
ArrayPrototypePush(this[searchParams], pair[0], pair[1]);
199223
}
200224
} else {
201225
// Record<USVString, USVString>
202226
// Need to use reflection APIs for full spec compliance.
203227
const visited = {};
204-
this[searchParams] = [];
205228
const keys = ReflectOwnKeys(init);
206229
for (let i = 0; i < keys.length; i++) {
207230
const key = keys[i];
@@ -223,13 +246,10 @@ class URLSearchParams {
223246
}
224247
}
225248
} else {
226-
// USVString
249+
// https://url.spec.whatwg.org/#dom-urlsearchparams-urlsearchparams
227250
init = toUSVString(init);
228251
this[searchParams] = init ? parseParams(init) : [];
229252
}
230-
231-
// "associated url object"
232-
this[context] = null;
233253
}
234254

235255
[inspect.custom](recurseTimes, ctx) {

test/parallel/test-whatwg-url-custom-searchparams-constructor.js

+4
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,10 @@ function makeIterableFunc(array) {
4747
assert.throws(() => new URLSearchParams([null]), tupleError);
4848
assert.throws(() => new URLSearchParams([{ [Symbol.iterator]: 42 }]),
4949
tupleError);
50+
51+
assert.throws(() => new URLSearchParams(
52+
makeIterableFunc([['key', 'val', 'val2']])
53+
), tupleError);
5054
}
5155

5256
{

0 commit comments

Comments
 (0)