From 24cb5f23ed65b1a7c8fdb87df6b407d581017e02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=BCrg=C3=BCn=20Day=C4=B1o=C4=9Flu?= Date: Tue, 10 Nov 2026 21:02:57 +0100 Subject: [PATCH] http: keep request option copies in fast mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The options of every client request are copied several times: once in ClientRequest to merge the URL and the options, once more to build the options handed to the agent, and again in Agent#addRequest (on every request, including keep-alive reuse) and createSocket to merge in the agent options. Each copy was a `{ __proto__: null, ...}` literal or an Object.assign() onto `{ __proto__: null }`. V8 creates such an object in dictionary mode, and copying from a dictionary-mode source takes the generic slow path, so every copy in the chain was slow and fed a slow source into the next one. Build each copy as an ordinary object literal and set its prototype to null afterwards, which leaves a fast-mode object with a null prototype and the same own properties in the same order. The same is done for the copy of the options in the http and https Agent constructors, which is the source of the agent options merged into every request. On a keep-alive client against a raw TCP responder (10 runs each, medians, busy machine) this goes from 68.7k to 83.1k requests per second and from 8.7 to 6.4 us of user CPU per request. Assisted-by: claude:opus-5.5 Signed-off-by: Gürgün Dayıoğlu --- lib/https.js | 2 +- lib/internal/http/agent.js | 6 +++--- lib/internal/http/client.js | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/https.js b/lib/https.js index 6be488aa753f..dcf02e4ca208 100644 --- a/lib/https.js +++ b/lib/https.js @@ -489,7 +489,7 @@ function Agent(options) { if (!(this instanceof Agent)) return new Agent(options); - options = { __proto__: null, ...options }; + options = ObjectSetPrototypeOf({ ...options }, null); validateOpenSSLEngineOptions(options); options.defaultPort ??= 443; options.protocol ??= 'https:'; diff --git a/lib/internal/http/agent.js b/lib/internal/http/agent.js index 3871c5b56a2f..7260b6c9d308 100644 --- a/lib/internal/http/agent.js +++ b/lib/internal/http/agent.js @@ -158,7 +158,7 @@ function Agent(options) { EventEmitter.call(this); - this.options = { __proto__: null, ...options }; + this.options = ObjectSetPrototypeOf({ ...options }, null); this.defaultPort = this.options.defaultPort || 80; this.protocol = this.options.protocol || 'http:'; @@ -375,7 +375,7 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */, } // Here the agent options will override per-request options. - options = { __proto__: null, ...options, ...this.options }; + options = ObjectSetPrototypeOf({ ...options, ...this.options }, null); if (options.socketPath) options.path = options.socketPath; @@ -445,7 +445,7 @@ Agent.prototype.addRequest = function addRequest(req, options, port/* legacy */, Agent.prototype.createSocket = function createSocket(req, options, cb) { // Here the agent options will override per-request options. - options = { __proto__: null, ...options, ...this.options }; + options = ObjectSetPrototypeOf({ ...options, ...this.options }, null); if (options.socketPath) options.path = options.socketPath; diff --git a/lib/internal/http/client.js b/lib/internal/http/client.js index 1952e7b69afb..27f00cf9b3f5 100644 --- a/lib/internal/http/client.js +++ b/lib/internal/http/client.js @@ -26,7 +26,6 @@ const { Boolean, Error, NumberIsFinite, - ObjectAssign, ObjectDefineProperty, ObjectHasOwn, ObjectKeys, @@ -363,7 +362,8 @@ function ClientRequest(input, options, cb) { pathIsFromURL = false; } - options = ObjectAssign({ __proto__: null }, input, options); + // Avoid creating a dictionary-mode object. + options = ObjectSetPrototypeOf({ ...input, ...options }, null); } // Propagate the user's highWaterMark to OutgoingMessage so that @@ -411,7 +411,7 @@ function ClientRequest(input, options, cb) { const defaultPort = options.defaultPort || (this.agent?.defaultPort); - const optsWithoutSignal = { __proto__: null, ...options }; + const optsWithoutSignal = ObjectSetPrototypeOf({ ...options }, null); const port = optsWithoutSignal.port = options.port || defaultPort || 80; const host = optsWithoutSignal.host = validateHost(options.hostname, 'hostname') ||