Yesterday I published Wajha (chani/wajha) and shared the initial benchmarks.
While Wajha crushed FastRoute on large datasets, there was still that tiny 8–10% deficit on dynamic routes when benchmarked against FastRoute’s small reference dataset (~40 routes). That bugged me enough to throw it into the VM profiler one last time.
My initial benchmarks discarded positional captures (?|...) because generic PHP loops negated their benefit. Re-visiting positional captures with unrolled index extraction ($matches[1]) was the missing piece—eliminating both PCRE2’s named subpattern lookup overhead and PHP’s loop cost.
What changed
I re-visited positional branch-reset matching (?|...) combined with inlined array index extraction:
- Compile time: Dynamic chunks now use
(?|...), resetting capture group numbers across alternations so variable 1 always lands in$matches[1]. - Dispatch time: Variable mapping bypasses string array key lookups entirely and assigns directly via unrolled positional indices.
Updated Real-World Benchmark (~40 routes)
(100,000 iterations on PHP 8.5)
- Dynamic Last Route (
/admin/category/123): 782,319 ops/s (Wajha) vs 711,334 ops/s (FastRoute) → 1.10x faster - HTTP 405 Method Not Allowed: 476,393 ops/s (Wajha) vs 373,702 ops/s (FastRoute) → 1.27x faster
- HTTP 404 Unknown Route: 599,126 ops/s (Wajha) vs 373,668 ops/s (FastRoute) → 1.60x faster
On large route tables (1,000 routes), Wajha maintains its baseline at 420,320 req/s vs FastRoute’s 117,810 req/s (3.57x faster).
Other v1.0 updates
- Reverse Routing (
WajhaUrlGenerator): Added URL generation with parameter interpolation and automatic query string generation. It runs via a standalone class generated at compile time, keeping 0% overhead on the inbound dispatcher. - PHP 8.3+: Lowered minimum requirements from PHP 8.5 to PHP 8.3+.
- Quality Gate: 100% PHPStan Level Max (level 9) clean and auto-normalizes route slash sequences.
That’s it.