Skip to content

perf: cache accepts instance on request object#7008

Open
jonathan-fulton wants to merge 2 commits intoexpressjs:masterfrom
jonathan-fulton:perf/cache-accepts-instance-5906
Open

perf: cache accepts instance on request object#7008
jonathan-fulton wants to merge 2 commits intoexpressjs:masterfrom
jonathan-fulton:perf/cache-accepts-instance-5906

Conversation

@jonathan-fulton
Copy link

Summary

The current implementation creates a new accepts instance for each call to req.accepts(), req.acceptsEncodings(), req.acceptsCharsets(), and req.acceptsLanguages(). This leads to redundant object creation and header parsing overhead when these methods are called multiple times within the same request lifecycle.

Solution

Cache the accepts instance on the request object using a Symbol to avoid property name collisions. The cached instance is reused for all subsequent accepts* method calls on the same request.

Changes

  • Added getAccepts() helper function that creates/returns cached instance
  • Modified all four accepts* methods to use the cached instance
  • Used a private Symbol to store the cached instance
  • Added test to verify multiple accepts methods work correctly together

Performance

This eliminates N-1 redundant accepts() instantiations and header parses when N accepts* methods are called on the same request.

Fixes #5906

Cache the accepts instance using a Symbol to avoid creating a new
instance on every call to req.accepts(), req.acceptsEncodings(),
req.acceptsCharsets(), and req.acceptsLanguages().

This reduces overhead when multiple accepts methods are called
within the same request lifecycle.

Fixes expressjs#5906
@bjohansebas
Copy link
Member

Also, if you’re going to submit PRs that claim performance improvements, please share what improvements you actually observed and include tests. Just because an issue says something could be done doesn’t mean it will result in real improvements.

Adds a benchmark to measure the performance impact of caching the
accepts instance on the request object. Tests both raw instantiation
overhead and end-to-end request scenarios.
@jonathan-fulton
Copy link
Author

Benchmark Results

@bjohansebas Thanks for the feedback. Here are the benchmark results:

Environment

  • Node.js v25.4.0
  • macOS (darwin arm64)
  • 100,000 iterations per test, 5 runs, median reported

Benchmark Script

I created a benchmark that compares the performance before (fresh accepts instance per call) vs after (cached instance). The script is included in this PR: benchmark-accepts.js

Results

Scenario Before (ops/s) After (ops/s) Change
Raw instantiation (×5) 24,144,628 74,397,842 +67%
Single accepts() call 1,022,089 1,000,159 ~0%
res.format() (×3 calls) 350,864 351,817 ~0%
Content negotiation (×4) 431,761 430,077 ~0%

Analysis

What we're measuring:

  1. Raw instantiation - Just creating the accepts() instance 5× per request. This shows the pure overhead savings: 3× faster with caching.

  2. End-to-end scenarios - Full req.accepts() calls including the negotiation methods. Here the improvement is minimal because:

    • The accepts library's Negotiator does lazy parsing - header parsing happens during method calls, not instantiation
    • Method call overhead (~1000ns) dwarfs instantiation overhead (~40ns)

Honest Assessment

The caching does provide a measurable benefit for instantiation (avoiding new Accepts() and new Negotiator() per call), but the real-world impact is modest because:

  1. Most of the CPU time is spent in the negotiation methods, not object creation
  2. Single-call requests (the majority) see negligible difference
  3. Multi-call patterns benefit from avoiding redundant allocations, but the % improvement is small

Why This Change Still Has Value

  1. Memory efficiency: Fewer object allocations per request
  2. Consistency: Matches the pattern used elsewhere in Express (e.g., caching parsed bodies)
  3. No regression: The cache check overhead (~13ns) is negligible
  4. Future-proofing: If applications make many accepts calls, they benefit automatically

Reproduce

cd /path/to/express
npm install
node benchmark-accepts.js

Happy to discuss or adjust the approach if needed!

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.

Cache accepts Instance in Request Methods?

2 participants

Comments