Skip to content

perf(router): replace matchit with a registration-ordered route table - #137

Open
RomainLanz wants to merge 2 commits into
9.xfrom
perf/routing
Open

perf(router): replace matchit with a registration-ordered route table#137
RomainLanz wants to merge 2 commits into
9.xfrom
perf/routing

Conversation

@RomainLanz

@RomainLanz RomainLanz commented Aug 24, 2026

Copy link
Copy Markdown
Member

Note

AI assistance was used to draft parts of the implementation, tests, benchmarks, and PR description.

Hey! 👋🏻

This PR replaces the runtime @poppinss/matchit dependency with an internal, registration-ordered route table.

It addresses the same linear route-matching cost as #135, but explores a different architecture. Instead of keeping matchit behind multiple indexes, this implementation moves matching itself into a persistent route table.

How it works

The route table combines:

  • a Map fast path for exact static routes;
  • a segment tree for parameters, optional parameters, suffixes, and wildcards;
  • a monotonically increasing registration ordinal for every route;
  • an ordered merge of structurally possible candidates, so the first registered
    matching route always wins;
  • a flat ordered fallback for stateful or observable regular expressions
    (global, sticky, or overridden exec/test methods).

The structural index only eliminates impossible routes. It never changes route precedence.

matchRoute, which receives a transient list of patterns, keeps a flat matching path instead of constructing a temporary index.

The public routes tree and StoreMethodNode shape remain unchanged.

Compatibility

Registration order remains part of the routing semantics, including precedence between:

  • static routes;
  • parameters;
  • optional parameters;
  • wildcards;
  • custom matchers and casts;
  • explicit and parameterized domains;
  • malformed or repeated separators already accepted by the previous matcher.

Compatibility was checked with 1,603,716 differential comparisons against matchit, covering generated route patterns, route permutations, domains, wildcards, optionals, casts, decoding, stateful regexes, and custom regex methods.

matchit remains a dev dependency only, as the differential-test oracle. No production source or built output imports it.

Benchmarks

All measurements below compare the previous matcher and this implementation on the same machine and environment.

Full-stack HTTP: dynamic and wildcard routes

Protocol: 5 rounds, 3 seconds warmup, 5 seconds measurement, 100 connections, pipelining 10.

Scenario Before req/s After req/s Change Before p50 After p50 Before CV After CV
Dynamic, last of 1,000 38,249.6 75,385.6 +97.1% 29 ms 9 ms 5.51% 1.25%
Wildcard, last of 1,000 39,414.4 75,552.0 +91.7% 28 ms 9 ms 3.11% 1.79%

Both scenarios passed functional validation with no errors or timeouts.

Full-stack HTTP: static routes

Protocol: 5 rounds, 10 seconds warmup, 10 seconds measurement, 100 connections, pipelining 10.

Scenario, 1,000 routes Before req/s After req/s Change
First 67,927.3 61,717.8 -9.1%
Middle 46,264.7 63,394.9 +37.0%
Last 33,264.0 65,170.9 +95.9%
Missing 35,946.2 59,963.6 +66.8%

The first-route result is within the relatively high variance of this exploratory run and should not be treated as a demonstrated regression.

These static measurements were collected after the static-index commit. The final matcher preserves that exact static Map fast path, but a final-state 40s/40s run is still planned.

Matcher microbenchmarks

With 1,000 routes:

Scenario Speedup
Dynamic route at the end 18x
Dynamic miss 22.8x
Deep dynamic route 12.1x
Wildcard route at the end 18.4x
1,000 fully overlapping regex matchers 1.19x

The overlapping-regex case is intentionally included as an adversarial scenario where the tree cannot eliminate any candidate.

Costs

Measured while building indexes for 10,000 routes:

Shape Additional memory Index construction
Static ~119 bytes/route ~0.18 µs/route
Dynamic ~631 bytes/route ~0.16 µs/route

The additional work happens when routes are committed and is amortized across requests.

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