perf: optimize response writer for dynamic SSR#91625
perf: optimize response writer for dynamic SSR#91625benfavre wants to merge 1 commit intovercel:canaryfrom
Conversation
…che flush check Reduce per-request overhead in `createWriterFromResponse` for dynamic SSR: 1. **Lazy `drained` DetachedPromise**: Only allocate the backpressure promise and register the `drain` listener when `res.write()` actually returns false. For typical responses under the highWaterMark (~64KB), backpressure never occurs, saving a DetachedPromise allocation and an event listener registration per request. 2. **Skip noop trace span**: The `startResponse` trace called `() => undefined` but still created a full OpenTelemetry span on every request (it's in NextVanillaSpanAllowlist). Add `hideSpan: true` to skip span creation while preserving the call site for future use. 3. **Cache flush check**: Move the `'flush' in res` property existence check from per-chunk to per-request. The compression middleware's `flush` method won't appear or disappear mid-request, so checking once and storing a bound reference eliminates repeated property lookups on every chunk write. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
Allow CI Workflow Run
Note: this should only be enabled once the PR is ready to go and can only be enabled by a maintainer |
Performance ImpactContext: Every dynamic SSR response flows through Changes:
Per-request savings: ~5-10μs (1 Promise + 1 event listener + 6× flush check + 1 trace call eliminated). Regression Safety
Test Verification
|
Summary
Reduces per-request overhead in
createWriterFromResponse(packages/next/src/server/pipe-readable.ts) for dynamic SSR responses:drainedDetachedPromise and register thedrainevent listener whenres.write()actually returnsfalse. For typical responses under the highWaterMark (~64KB), backpressure never occurs — saves 1 promise allocation + 1 event listener per request.startResponsetrace created a full OTel span every request just to call() => undefined. SincestartResponseis inNextVanillaSpanAllowlist, this bypassed the fast path and went through full span creation. AddinghideSpan: truemakes the tracer callfn()directly (line 304 of tracer.ts), avoiding context/span overhead while preserving the call site.'flush' in resproperty existence check +typeofguard from per-chunk to once per-request. The compression middleware'sflushmethod is stable for the lifetime of the response.Net savings per request (no backpressure path): 1 DetachedPromise, 1 event listener registration, 1 OTel span creation, N-1 property lookups (where N = chunk count, typically 6 for a ~9.8KB page).
Test plan
next build && next startserves dynamic SSR pages correctlycompressionmiddleware flush still works (response arrives compressed)test/integration/andtest/e2e/cover these paths🤖 Generated with Claude Code