[PR #388] [MERGED] perf: lazy-init mutable Headers copy in headersContextFromRequest #531

Closed
opened 2026-05-06 13:08:36 +02:00 by BreizhHardware · 0 comments

📋 Pull Request Information

Original PR: https://github.com/cloudflare/vinext/pull/388
Author: @james-elicx
Created: 3/9/2026
Status: Merged
Merged: 3/9/2026
Merged by: @james-elicx

Base: mainHead: perf/lazy-headers-context


📝 Commits (2)

  • 8182de5 perf: lazy-init mutable Headers copy in headersContextFromRequest
  • 6f2c766 perf: hoist MUTATING_METHODS set to module scope

📊 Changes

2 files changed (+146 additions, -13 deletions)

View changed files

📝 packages/vinext/src/shims/headers.ts (+82 -13)
📝 tests/shims.test.ts (+64 -0)

📄 Description

Problem

headersContextFromRequest was profiled at 815ms self-time per request with no children — all time spent in its own body. The two culprits:

  1. new Headers(request.headers) — in Workerd, this copies the entire header map across the V8/C++ boundary. On requests with many headers (e.g. a browser sending lots of cookies), this is very slow.
  2. Cookie parsing — splitting the cookie header on ; and = on every request, even when cookies() is never called.

Both operations happened unconditionally on every request entry, even for routes that never read cookies or mutate headers.

Fix

Lazy mutable Headers proxy

Replace new Headers(request.headers) with a Proxy around the original immutable request.headers:

  • Reads (get, has, entries, forEach, …) are forwarded directly to request.headers — zero copy cost.
  • First write (set, delete, or append) materialises new Headers(request.headers) once and caches it. All subsequent operations use the copy.

The copy is only needed when middleware calls NextResponse.next({ request: { headers } }) and applyMiddlewareRequestHeaders() sets headers on the context. That path is uncommon. Pure read requests (the vast majority) now pay zero V8/C++ boundary crossing for headers.

Cookie parsing is moved behind a lazy getter on the cookies property. The cookie header string is not split until cookies(), draftMode(), or applyMiddlewareRequestHeaders() is first invoked for a request. Requests that don't touch cookies skip the parse entirely.

Tests

Four new test cases added to tests/shims.test.ts:

  • defers new Headers() copy until first write — regression guard: reads must work before any write
  • defers cookie parsing until first access — cookies map is not populated until accessed
  • cookie getter reflects middleware-modified cookie header — verifies applyMiddlewareRequestHeaders correctly invalidates and rebuilds the cookie map after middleware changes the cookie header
  • Existing mutability and correctness tests all pass unchanged (581 tests, 0 failures)

Behavioural compatibility

The HeadersContext interface is unchanged. setHeadersContext({ headers, cookies }) (used by tests and legacy callers) continues to accept plain Headers/Map objects without change.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/cloudflare/vinext/pull/388 **Author:** [@james-elicx](https://github.com/james-elicx) **Created:** 3/9/2026 **Status:** ✅ Merged **Merged:** 3/9/2026 **Merged by:** [@james-elicx](https://github.com/james-elicx) **Base:** `main` ← **Head:** `perf/lazy-headers-context` --- ### 📝 Commits (2) - [`8182de5`](https://github.com/cloudflare/vinext/commit/8182de549931cdcc95d76b2e6f010fda9b169223) perf: lazy-init mutable Headers copy in headersContextFromRequest - [`6f2c766`](https://github.com/cloudflare/vinext/commit/6f2c76620d31d0f313ffccc88e635cb43e611a3b) perf: hoist MUTATING_METHODS set to module scope ### 📊 Changes **2 files changed** (+146 additions, -13 deletions) <details> <summary>View changed files</summary> 📝 `packages/vinext/src/shims/headers.ts` (+82 -13) 📝 `tests/shims.test.ts` (+64 -0) </details> ### 📄 Description ## Problem `headersContextFromRequest` was profiled at **815ms self-time** per request with no children — all time spent in its own body. The two culprits: 1. **`new Headers(request.headers)`** — in Workerd, this copies the entire header map across the V8/C++ boundary. On requests with many headers (e.g. a browser sending lots of cookies), this is very slow. 2. **Cookie parsing** — splitting the `cookie` header on `;` and `=` on every request, even when `cookies()` is never called. Both operations happened unconditionally on every request entry, even for routes that never read cookies or mutate headers. ## Fix ### Lazy mutable Headers proxy Replace `new Headers(request.headers)` with a `Proxy` around the original immutable `request.headers`: - **Reads** (`get`, `has`, `entries`, `forEach`, …) are forwarded directly to `request.headers` — zero copy cost. - **First write** (`set`, `delete`, or `append`) materialises `new Headers(request.headers)` once and caches it. All subsequent operations use the copy. The copy is only needed when middleware calls `NextResponse.next({ request: { headers } })` and `applyMiddlewareRequestHeaders()` sets headers on the context. That path is uncommon. Pure read requests (the vast majority) now pay **zero** V8/C++ boundary crossing for headers. ### Lazy cookie parsing Cookie parsing is moved behind a lazy getter on the `cookies` property. The `cookie` header string is not split until `cookies()`, `draftMode()`, or `applyMiddlewareRequestHeaders()` is first invoked for a request. Requests that don't touch cookies skip the parse entirely. ## Tests Four new test cases added to `tests/shims.test.ts`: - `defers new Headers() copy until first write` — regression guard: reads must work before any write - `defers cookie parsing until first access` — cookies map is not populated until accessed - `cookie getter reflects middleware-modified cookie header` — verifies `applyMiddlewareRequestHeaders` correctly invalidates and rebuilds the cookie map after middleware changes the `cookie` header - Existing mutability and correctness tests all pass unchanged (581 tests, 0 failures) ## Behavioural compatibility The `HeadersContext` interface is unchanged. `setHeadersContext({ headers, cookies })` (used by tests and legacy callers) continues to accept plain `Headers`/`Map` objects without change. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
BreizhHardware 2026-05-06 13:08:36 +02:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
starred/vinext#531
No description provided.