Skip to content

perf(ext/http): optimize Deno.serve() hot path#32736

Open
bartlomieju wants to merge 6 commits intomainfrom
perf/serve-hot-path
Open

perf(ext/http): optimize Deno.serve() hot path#32736
bartlomieju wants to merge 6 commits intomainfrom
perf/serve-hot-path

Conversation

@bartlomieju
Copy link
Copy Markdown
Member

Summary

Optimizes the Deno.serve() hot path by reducing per-request object allocations, eliminating unnecessary function calls, and adding fast paths for the most common response patterns.

Benchmark results (wrk, 2 threads, 10 connections, 10s, macOS ARM64)

Benchmark Before After Improvement
new Response("Hello, World!") ~213,000 RPS ~219,000 RPS +3%
new Response(JSON.stringify(...), {headers}) ~170,000 RPS ~179,000 RPS +5-6%

Changes

ext/http/00_serve.ts — Serve handler hot path

  • Sync fast path: The mapped callback is no longer async. When the handler returns a Response synchronously (not a Promise), it processes it without creating a Promise or going through the microtask queue. Async handlers fall through to handleAsyncResponse.
  • Ultra-fast path for new Response(string): Responses marked with _fastResponse skip all validation (type check, bodyUsed check, prototype check) and go directly to the combined Rust op.
  • Inlined response processing: For the non-tracing path, respondWith and toInnerResponse are inlined to eliminate function call overhead and redundant property accesses.
  • Direct bodyUsed check: Checks inner.body.streamOrStatic?.consumed directly instead of going through response.bodyUsed getter (avoids assertBranded + getter chain).
  • Skip ServeHandlerInfo allocation: Only created when handler has 2+ parameters.
  • Skip InnerRequest/Request creation for 0-arg handlers.
  • Fix asyncContextSnapshot property name: Was accessing context.asyncContext (undefined) instead of context.asyncContextSnapshot.

ext/fetch/23_response.js — Response constructor

  • Fast constructor for new Response(string): Inlines response creation, skipping webidlConvertersBodyInitDomString, webidlConvertersResponseInitFast, and extractBody. Saves ~2 object allocations.
  • Fast constructor for new Response(string, {status, headers}): Similar fast path with init.
  • Fast Response.json() path: Avoids extractBody intermediates.
  • Lazy Headers creation: Headers wrapper only created when accessed, not in constructor.
  • Fast plain-object headers: { "content-type": "application/json" } added directly to headerList without Headers wrapper.
  • Inner response prototype: url() method on shared prototype.
  • Shared urlList: User-created responses share empty urlList.
  • Direct headerList access in initializeAResponse.

ext/fetch/23_request.js — Request construction

  • Lazy Headers creation: Same pattern as Response.
  • Eliminated closure in fromInnerRequest: Stores guard directly instead of closure.

ext/http/http_next.rs — Rust ops

  • Combined op_http_set_response_body_text_with_header: Sets header + body + status in one op.
  • Small body fast path: Skip compression check for bodies < 64 bytes.
  • Skip Vary: Accept-Encoding: Only when compression is considered.
  • Skip response headers borrow: When compression is None.

Test plan

  • Deno.serve() with sync handlers
  • Deno.serve() with async handlers
  • Response.json() correctness
  • new Response(body, { headers: {...} }) headers correctness
  • new Response(body, { status: 404 }) status correctness
  • new Response(null) empty body
  • Streaming responses
  • WebSocket upgrades
  • Existing serve spec tests

🤖 Generated with Claude Code

bartlomieju and others added 6 commits March 14, 2026 19:53
Key optimizations:
- Add sync fast path in mapToCallback to avoid Promise creation when
  handler returns Response synchronously
- Make Response headers lazy - defer Headers wrapper creation until
  actually accessed
- Add fast constructor path for new Response(string) to skip webidl
  converters and extractBody intermediate objects
- Skip ServeHandlerInfo allocation when handler doesn't use info param
- Skip compression/Vary header processing when compression is None
- Avoid RefCell borrow for response headers when not compressing
- Add fast path in respondWith to send string bodies directly via
  body.source without streamOrStatic indirection
- Eliminate closure allocation in fromInnerRequest by using stored guard
- Share urlList array and use prototype for inner response url()

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Inline serve request handling in non-tracing path to reduce
  function call overhead
- Add Response.json() fast path that avoids extractBody intermediates
- Fix asyncContextSnapshot property name (was using wrong name)
- Add fast path in op_http_set_response_body_text for small bodies
  (<64 bytes) to skip compression check entirely
- Use inner response prototype to share url() method across instances

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
When the serve handler has 0 parameters (e.g., () => new Response("ok")),
skip creating InnerRequest and Request objects entirely since they won't
be used. This avoids 2 object allocations per request.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add op_http_set_response_body_text_with_header that sets header + body
  + status in a single op dispatch (avoids separate set_header call)
- Inline respondWith into the sync fast path to eliminate function call
- Check bodyUsed via inner body directly instead of going through
  response.bodyUsed getter (avoids assertBranded + getter chain)
- Access inner.type directly instead of response.type (avoids assertBranded)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Mark responses created via the fast constructor path (new Response(string))
with a _fastResponse symbol. In the serve handler, detect this marker and
skip all validation (type check, bodyUsed check, ResponsePrototype check)
going directly to the combined op.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ructor

When Response is constructed with headers as a plain object (the most
common pattern: new Response(body, { headers: { key: value } })), add
headers directly to headerList instead of going through fillHeaders
which requires creating a Headers wrapper object.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant