Skip to content

perf/hardening: request context, middleware context and handle store sit in V8 dictionary mode; request-derived keys on plain objects #1109

Closed

Description

@ivogt

Summary

A read-only performance review measured three per-request objects that sit in V8's slow "dictionary mode" for their whole lifetime, and found a few places where request-derived keys are looked up on plain objects. These are server-only changes with no public API, Flight or client bundle impact. Measured on Node 24.12 (V8 13).

Performance: object literals with get/set members

A get/set member inside an object literal puts the object in dictionary mode, so every property read on it is a hash lookup instead of a fast inline-cached load. Adding the accessor with Object.defineProperty keeps the object fast. request-context.ts already does that for theme.

Location Object Measured / estimated cost today
server/request-context.ts (the request context literal, get res() / set res()) 62 members, once per request ~690 ns and ~3.3 KB to create, against ~20 ns and ~520 B as a data-only literal; slower reads for the whole request
router/middleware.ts (middleware context getters) ~21 members, per middleware call est. ~300 ns and ~1.2 KB per call
server/handle-store.ts (store getters) ~20 members, once per request est. ~300 ns and ~1 KB per request

Possible lazy step: the request context builds a handle snapshot eagerly on every non-streaming request, but only ctx.get(handle) reads it, and only after rendered(). Building it on first read is estimated to save ~310 ns and ~1.5 KB per request, if the store is final at that point.

Hardening: request-derived keys on plain objects

None of these pollutes Object.prototype. They can make lookups hit inherited properties, or carry odd keys into user code:

  • Route matching looks up static children in a plain object keyed by raw URL segments, so segments such as __proto__, constructor or toString hit inherited properties. A null prototype on those maps fixes it, keeps lookups fast (measured), and makes reads slightly faster.
  • Cookie parsing builds a plain object keyed by cookie names, so cookies().has("toString") and cookies().get("constructor") see inherited values.
  • The loader fetch endpoint spreads request JSON into ctx.params. Keys and values should be validated: strings only, with reserved keys rejected.
  • Code that copies loader-returned objects may carry an own __proto__ key from user JSON.parse data, which then fails in Flight. To verify.

Not worth doing (measured)

  • Null-prototype objects for params, segments, loader data or handle data: they are slower and larger, React Flight rejects them in production, and user code calling params.hasOwnProperty(...) would break.
  • Map for params or handle buckets: slower and larger for 1-3 entries.
  • A shared frozen empty params object: handlers may mutate params.
  • Client-side swaps: each costs client bundle bytes for well under a microsecond per navigation.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions