diff --git a/lib/_http_client.js b/lib/_http_client.js index 024834c7ecfd22..49dbd64995e4b4 100644 --- a/lib/_http_client.js +++ b/lib/_http_client.js @@ -63,7 +63,7 @@ const { const Agent = require('_http_agent'); const { Buffer } = require('buffer'); const { defaultTriggerAsyncIdScope } = require('internal/async_hooks'); -const { URL, urlToHttpOptions, searchParamsSymbol } = require('internal/url'); +const { URL, urlToHttpOptions, isURLThis } = require('internal/url'); const { kOutHeaders, kNeedDrain, @@ -133,8 +133,7 @@ function ClientRequest(input, options, cb) { if (typeof input === 'string') { const urlStr = input; input = urlToHttpOptions(new URL(urlStr)); - } else if (input && input[searchParamsSymbol] && - input[searchParamsSymbol][searchParamsSymbol]) { + } else if (isURLThis(input)) { // url.URL instance input = urlToHttpOptions(input); } else { diff --git a/lib/https.js b/lib/https.js index c3ecbc45ee41ed..fe42293a2e854c 100644 --- a/lib/https.js +++ b/lib/https.js @@ -52,7 +52,7 @@ const { ClientRequest } = require('_http_client'); let debug = require('internal/util/debuglog').debuglog('https', (fn) => { debug = fn; }); -const { URL, urlToHttpOptions, searchParamsSymbol } = require('internal/url'); +const { URL, urlToHttpOptions, isURLThis } = require('internal/url'); const { validateObject } = require('internal/validators'); function Server(opts, requestListener) { @@ -350,9 +350,7 @@ function request(...args) { if (typeof args[0] === 'string') { const urlStr = ArrayPrototypeShift(args); options = urlToHttpOptions(new URL(urlStr)); - } else if (args[0] && args[0][searchParamsSymbol] && - args[0][searchParamsSymbol][searchParamsSymbol]) { - // url.URL instance + } else if (isURLThis(args[0])) { options = urlToHttpOptions(ArrayPrototypeShift(args)); } diff --git a/lib/internal/url.js b/lib/internal/url.js index 65af71a8883332..801ee8874858d9 100644 --- a/lib/internal/url.js +++ b/lib/internal/url.js @@ -100,6 +100,7 @@ const FORWARD_SLASH = /\//g; const context = Symbol('context'); const searchParams = Symbol('query'); +const kDirty = Symbol('dirty'); const updateActions = { kProtocol: 0, @@ -224,11 +225,12 @@ class URLSearchParams { } else { // USVString init = toUSVString(init); - initSearchParams(this, init); + this[searchParams] = init ? parseParams(init) : []; } // "associated url object" this[context] = null; + this[kDirty] = false; } [inspect.custom](recurseTimes, ctx) { @@ -604,11 +606,34 @@ class URL { ctx.password = password; ctx.port = port; ctx.hash = hash; - if (!this[searchParams]) { // Invoked from URL constructor - this[searchParams] = new URLSearchParams(); + if (this[searchParams]) { + // Update `kDirty` property to recalculate searchParams on access. + // This is done to reduce the overhead of initializing the URL. + this[searchParams][kDirty] = true; + } + }; + + #onSearchUpdate = (href, origin, protocol, hostname, pathname, + search, username, password, port, hash) => { + const ctx = this[context]; + ctx.href = href; + ctx.origin = origin; + ctx.protocol = protocol; + ctx.hostname = hostname; + ctx.pathname = pathname; + ctx.search = search; + ctx.username = username; + ctx.password = password; + ctx.port = port; + ctx.hash = hash; + + if (this[searchParams] == null) { + this[searchParams] = new URLSearchParams(this[context].search); this[searchParams][context] = this; + } else { + this[searchParams][searchParams] = this[context].search ? parseParams(this[context].search) : []; + this[searchParams][kDirty] = false; } - initSearchParams(this[searchParams], ctx.search); }; toString() { @@ -729,18 +754,25 @@ class URL { return this[context].search; } - set search(search) { + set search(value) { if (!isURLThis(this)) throw new ERR_INVALID_THIS('URL'); - search = toUSVString(search); - updateUrl(this[context].href, updateActions.kSearch, search, this.#onParseComplete); - initSearchParams(this[searchParams], this[context].search); + updateUrl(this[context].href, updateActions.kSearch, toUSVString(value), this.#onSearchUpdate); } // readonly get searchParams() { if (!isURLThis(this)) throw new ERR_INVALID_THIS('URL'); + // Create URLSearchParams on demand to greatly improve the URL performance. + if (this[searchParams] == null) { + this[searchParams] = new URLSearchParams(this[context].search); + this[searchParams][context] = this; + } else if (this[searchParams][kDirty]) { + const updated = this[context].search; + this[searchParams][searchParams] = updated ? parseParams(updated) : []; + this[searchParams][kDirty] = false; + } return this[searchParams]; } @@ -815,14 +847,6 @@ ObjectDefineProperties(URL, { revokeObjectURL: kEnumerableProperty, }); -function initSearchParams(url, init) { - if (!init) { - url[searchParams] = []; - return; - } - url[searchParams] = parseParams(init); -} - // application/x-www-form-urlencoded parser // Ref: https://url.spec.whatwg.org/#concept-urlencoded-parser function parseParams(qs) { @@ -1141,8 +1165,7 @@ function domainToUnicode(domain) { function urlToHttpOptions(url) { const options = { protocol: url.protocol, - hostname: typeof url.hostname === 'string' && - StringPrototypeStartsWith(url.hostname, '[') ? + hostname: url.hostname && StringPrototypeStartsWith(url.hostname, '[') ? StringPrototypeSlice(url.hostname, 1, -1) : url.hostname, hash: url.hash, @@ -1313,6 +1336,6 @@ module.exports = { domainToASCII, domainToUnicode, urlToHttpOptions, - searchParamsSymbol: searchParams, encodeStr, + isURLThis, };