Skip to content

perf: adopt h3 composeMiddleware and drop the ~getMiddleware override #4443

Description

@pi0x

Context

h3js/h3#1475 precomposes middleware chains: the global chain and each route's middleware+handler are composed once and cached, replacing per-request array merging and recursive dispatch (monomorphic call sites, ~10-18% faster dispatch, ~20% fewer allocations in isolated benchmarks). It also exposes composeMiddleware(middleware): (event, handler) => result as public API.

Nitro currently opts out of all of this: the ~getMiddleware override in src/build/virtual/app.ts rebuilds the middleware array on every request (route-rule middleware → global middleware → routed middleware → route middleware, 3-4 spreads) and h3 falls back to the recursive callMiddleware compat path whenever the override is present.

Proposal

Drop the ~getMiddleware override and let each source land where h3 already precomposes:

  1. Route-rule middleware + global middleware → register via app.use() in the same order (route rules first). h3 precomposes the global chain automatically; nitro writes no composition code.
  2. Route middleware → keep passing middleware in route options; h3 composes it with the handler per route on first match. Ordering matches today (runs after the global chain).
  3. Routed middleware (middleware/ dir with path prefixes) — the one dynamic source. Keep the rou3 lookup but compose per matched node, cached on the match data (bounded by router nodes):
app.use((event, next) => {
  const matched = findRoutedMiddleware(event.req.method, event.url.pathname);
  if (matched.length === 0) return next();
  const chain = (matched["~composed"] ??= composeMiddleware(matched.map((r) => r.data)));
  return chain(event, next);
});

Notes

  • Composed chains are static snapshots: HMR-mutated middleware must flow through use()/route re-registration (which invalidate h3's caches) or an app rebuild, as dev mode already does.
  • If the compat path is kept in the interim: h3 now resolves the dispatch strategy once per app (first request, re-checked after use()/mount()), so the ~getMiddleware override must be assigned before the first request — nitro assigns at build time, so this already holds.
  • h3 also has an internal composeHandler(middleware, handler) (fixed terminal handler) that can be exported if useful here.
  • End state: once nitro no longer overrides ~getMiddleware, h3 can consider removing it in v2 proper.

Requires a version of h3 including h3js/h3#1475.

🤖 Generated with Claude Code

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