From 5edfffdf1110858c7ee4a19437505a749db499d9 Mon Sep 17 00:00:00 2001 From: Taku Amano Date: Sat, 17 May 2025 07:58:14 +0900 Subject: [PATCH 1/4] perf: keep using lightweight Response object when retrieving headers --- src/listener.ts | 5 ++++- src/response.ts | 19 ++++++++++++------- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/listener.ts b/src/listener.ts index 8e92635..f0433c1 100644 --- a/src/listener.ts +++ b/src/listener.ts @@ -49,7 +49,10 @@ const responseViaCache = ( outgoing: ServerResponse | Http2ServerResponse ): undefined | Promise => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [status, body, header] = (res as any)[cacheKey] + let [status, body, header] = (res as any)[cacheKey] + if (header instanceof Headers) { + header = buildOutgoingHttpHeaders(header) + } if (typeof body === 'string') { header['Content-Length'] = Buffer.byteLength(body) outgoing.writeHead(status, header) diff --git a/src/response.ts b/src/response.ts index cbd629c..0f8fe92 100644 --- a/src/response.ts +++ b/src/response.ts @@ -2,7 +2,6 @@ // Define lightweight pseudo Response class and replace global.Response with it. import type { OutgoingHttpHeaders } from 'node:http' -import { buildOutgoingHttpHeaders } from './utils' import { getResponseState } from './utils/internal' import type { InternalBody } from './utils/internal' @@ -37,22 +36,28 @@ export class Response { } if (typeof body === 'string' || typeof (body as ReadableStream)?.getReader !== 'undefined') { - let headers = (init?.headers || { 'content-type': 'text/plain; charset=UTF-8' }) as + const headers = (init?.headers || { 'content-type': 'text/plain; charset=UTF-8' }) as | Record | Headers | OutgoingHttpHeaders - if (headers instanceof Headers) { - headers = buildOutgoingHttpHeaders(headers) - } - ;(this as any)[cacheKey] = [init?.status || 200, body, headers] } } + + get headers(): Headers { + const cache = (this as any)[cacheKey] + if (cache) { + if (!(cache[2] instanceof Headers)) { + cache[2] = new Headers(cache[2] as HeadersInit) + } + return cache[2] + } + return this[getResponseCache]().headers + } } ;[ 'body', 'bodyUsed', - 'headers', 'ok', 'redirected', 'status', From ebe262874fdf28caee037e678a775a249660b43a Mon Sep 17 00:00:00 2001 From: Taku Amano Date: Sat, 17 May 2025 08:12:40 +0900 Subject: [PATCH 2/4] refactor: organize internal cache data type. --- src/listener.ts | 6 +++--- src/response.ts | 16 +++++++++++++--- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/listener.ts b/src/listener.ts index f0433c1..7f6e4c4 100644 --- a/src/listener.ts +++ b/src/listener.ts @@ -7,6 +7,7 @@ import { toRequestError, } from './request' import { cacheKey, getInternalBody, Response as LightweightResponse } from './response' +import type { InternalCache } from './response' import type { CustomErrorHandler, FetchCallback, HttpBindings } from './types' import { writeFromReadableStream, buildOutgoingHttpHeaders } from './utils' import { X_ALREADY_SENT } from './utils/response/constants' @@ -49,7 +50,7 @@ const responseViaCache = ( outgoing: ServerResponse | Http2ServerResponse ): undefined | Promise => { // eslint-disable-next-line @typescript-eslint/no-explicit-any - let [status, body, header] = (res as any)[cacheKey] + let [status, body, header] = (res as any)[cacheKey] as InternalCache if (header instanceof Headers) { header = buildOutgoingHttpHeaders(header) } @@ -92,8 +93,7 @@ const responseViaResponseObject = async ( const resHeaderRecord: OutgoingHttpHeaders = buildOutgoingHttpHeaders(res.headers) - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const internalBody = getInternalBody(res as any) + const internalBody = getInternalBody(res as Response) if (internalBody) { const { length, source, stream } = internalBody if (source instanceof Uint8Array && source.byteLength !== length) { diff --git a/src/response.ts b/src/response.ts index 0f8fe92..afb7ec8 100644 --- a/src/response.ts +++ b/src/response.ts @@ -9,14 +9,24 @@ const responseCache = Symbol('responseCache') const getResponseCache = Symbol('getResponseCache') export const cacheKey = Symbol('cache') +export type InternalCache = [ + number, + string | ReadableStream, + Record | Headers | OutgoingHttpHeaders, +] +interface LiteResponse { + [responseCache]?: globalThis.Response + [cacheKey]?: InternalCache +} + export const GlobalResponse = global.Response export class Response { #body?: BodyInit | null #init?: ResponseInit; [getResponseCache](): globalThis.Response { - delete (this as any)[cacheKey] - return ((this as any)[responseCache] ||= new GlobalResponse(this.#body, this.#init)) + delete (this as LiteResponse)[cacheKey] + return ((this as LiteResponse)[responseCache] ||= new GlobalResponse(this.#body, this.#init)) } constructor(body?: BodyInit | null, init?: ResponseInit) { @@ -45,7 +55,7 @@ export class Response { } get headers(): Headers { - const cache = (this as any)[cacheKey] + const cache = (this as LiteResponse)[cacheKey] as InternalCache if (cache) { if (!(cache[2] instanceof Headers)) { cache[2] = new Headers(cache[2] as HeadersInit) From 89b5838f39511b3ed955e0729f2154859d4679de Mon Sep 17 00:00:00 2001 From: Taku Amano Date: Sat, 17 May 2025 08:28:57 +0900 Subject: [PATCH 3/4] perf: status and ok should also be completed in cache only, if possible, to improve performance --- src/response.ts | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/src/response.ts b/src/response.ts index afb7ec8..0dd0435 100644 --- a/src/response.ts +++ b/src/response.ts @@ -64,18 +64,20 @@ export class Response { } return this[getResponseCache]().headers } + + get status() { + return ( + ((this as LiteResponse)[cacheKey] as InternalCache | undefined)?.[0] ?? + this[getResponseCache]().status + ) + } + + get ok() { + const status = this.status + return status >= 200 && status < 300 + } } -;[ - 'body', - 'bodyUsed', - 'ok', - 'redirected', - 'status', - 'statusText', - 'trailers', - 'type', - 'url', -].forEach((k) => { +;['body', 'bodyUsed', 'redirected', 'statusText', 'trailers', 'type', 'url'].forEach((k) => { Object.defineProperty(Response.prototype, k, { get() { return this[getResponseCache]()[k] From 8618d005cd4b8df4cd40f77b2d554a6a33e56e29 Mon Sep 17 00:00:00 2001 From: Taku Amano Date: Sat, 17 May 2025 10:20:45 +0900 Subject: [PATCH 4/4] fix: clone headers to avoid sharing the same object between parent and child --- src/response.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/response.ts b/src/response.ts index 0dd0435..fe727ed 100644 --- a/src/response.ts +++ b/src/response.ts @@ -30,6 +30,7 @@ export class Response { } constructor(body?: BodyInit | null, init?: ResponseInit) { + let headers: HeadersInit this.#body = body if (init instanceof Response) { const cachedGlobalResponse = (init as any)[responseCache] @@ -40,16 +41,15 @@ export class Response { return } else { this.#init = init.#init + // clone headers to avoid sharing the same object between parent and child + headers = new Headers((init.#init as ResponseInit).headers) } } else { this.#init = init } if (typeof body === 'string' || typeof (body as ReadableStream)?.getReader !== 'undefined') { - const headers = (init?.headers || { 'content-type': 'text/plain; charset=UTF-8' }) as - | Record - | Headers - | OutgoingHttpHeaders + headers ||= init?.headers || { 'content-type': 'text/plain; charset=UTF-8' } ;(this as any)[cacheKey] = [init?.status || 200, body, headers] } }