PoC: feat(reg-exp-router): Introduced PreparedRegExpRouter
What is the PR to improve?
With this PR, we aim to improve the reduction of RegExpRouter bundle size and initial addition time.
As you can see in the code I added to the following unit test, we can prepare regular expressions, etc. in advance by passing the routing information to buildInitParams(). This can be used to simplify the initialization process at startup.
src/router/reg-exp-router/router.test.ts
Benchmark
In Node.js, it is more than 10 times faster than RegExpRouter and close to LinearRouter; in Bun, it may be faster than LinearRouter.
$ npm run bench-includes-init:node
> bench-includes-init:node
> tsx ./src/bench-includes-init.mts
cpu: Apple M2 Pro
runtime: node v20.0.0 (arm64-darwin)
benchmark time (avg) (min … max) p75 p99 p995
------------------------------------------------------------ -----------------------------
• GET /user
------------------------------------------------------------ -----------------------------
RegExpRouter 34.18 µs/iter (26.04 µs … 1.7 ms) 31.29 µs 86.38 µs 382.17 µs
PreparedRegExpRouter 2.07 µs/iter (1.08 µs … 8.86 µs) 1.9 µs 8.86 µs 8.86 µs
TrieRouter 6.07 µs/iter (4.79 µs … 638.33 µs) 5.58 µs 7.63 µs 9.79 µs
LinearRouter 1.02 µs/iter (958.27 ns … 1.07 µs) 1.04 µs 1.07 µs 1.07 µs
MedleyRouter 3.04 µs/iter (2.93 µs … 3.25 µs) 3.07 µs 3.25 µs 3.25 µs
FindMyWay 91.65 µs/iter (78.04 µs … 2.14 ms) 88.04 µs 143.83 µs 607.83 µs
KoaTreeRouter 2.31 µs/iter (2.15 µs … 3.44 µs) 2.26 µs 3.44 µs 3.44 µs
TrekRouter 3.05 µs/iter (2.96 µs … 3.11 µs) 3.07 µs 3.11 µs 3.11 µs
summary for GET /user
LinearRouter
2.04x faster than PreparedRegExpRouter
2.27x faster than KoaTreeRouter
2.99x faster than MedleyRouter
2.99x faster than TrekRouter
5.97x faster than TrieRouter
33.6x faster than RegExpRouter
90.11x faster than FindMyWay
• GET /user/comments
------------------------------------------------------------ -----------------------------
RegExpRouter 41.4 µs/iter (26 µs … 8.62 ms) 30.79 µs 336 µs 827.21 µs
PreparedRegExpRouter 2.45 µs/iter (1.73 µs … 6.14 µs) 2.41 µs 6.14 µs 6.14 µs
TrieRouter 6.45 µs/iter (4.79 µs … 1.21 ms) 5.54 µs 8.83 µs 10 µs
LinearRouter 1.18 µs/iter (1.1 µs … 1.39 µs) 1.21 µs 1.39 µs 1.39 µs
MedleyRouter 3.65 µs/iter (3.21 µs … 9.79 µs) 3.4 µs 9.79 µs 9.79 µs
FindMyWay 99.71 µs/iter (77.96 µs … 3.26 ms) 87.96 µs 265.92 µs 1.24 ms
KoaTreeRouter 2.41 µs/iter (2.29 µs … 2.58 µs) 2.46 µs 2.58 µs 2.58 µs
TrekRouter 3.49 µs/iter (3.16 µs … 4.84 µs) 3.47 µs 4.84 µs 4.84 µs
summary for GET /user/comments
LinearRouter
2.04x faster than KoaTreeRouter
2.08x faster than PreparedRegExpRouter
2.96x faster than TrekRouter
3.09x faster than MedleyRouter
5.47x faster than TrieRouter
35.1x faster than RegExpRouter
84.55x faster than FindMyWay
• GET /user/lookup/username/hey
------------------------------------------------------------ -----------------------------
RegExpRouter 58.27 µs/iter (25.88 µs … 7.42 ms) 31.5 µs 971.42 µs 2 ms
PreparedRegExpRouter 8.07 µs/iter (2.71 µs … 35.7 µs) 5.3 µs 35.7 µs 35.7 µs
TrieRouter 7.3 µs/iter (4.96 µs … 5.18 ms) 5.83 µs 10.83 µs 12.71 µs
LinearRouter 1.39 µs/iter (1.27 µs … 1.82 µs) 1.43 µs 1.82 µs 1.82 µs
MedleyRouter 4.03 µs/iter (3.47 µs … 8.86 µs) 3.7 µs 8.86 µs 8.86 µs
FindMyWay 103.31 µs/iter (80.46 µs … 4.16 ms) 89.54 µs 306.04 µs 1.53 ms
KoaTreeRouter 2.83 µs/iter (2.53 µs … 4.82 µs) 2.77 µs 4.82 µs 4.82 µs
TrekRouter 4.01 µs/iter (3.41 µs … 11.82 µs) 3.71 µs 11.82 µs 11.82 µs
summary for GET /user/lookup/username/hey
LinearRouter
2.03x faster than KoaTreeRouter
2.88x faster than TrekRouter
2.9x faster than MedleyRouter
5.25x faster than TrieRouter
5.8x faster than PreparedRegExpRouter
41.85x faster than RegExpRouter
74.2x faster than FindMyWay
• GET /event/abcd1234/comments
------------------------------------------------------------ -----------------------------
RegExpRouter 68.34 µs/iter (26.33 µs … 8.38 ms) 30.92 µs 1.11 ms 2.45 ms
PreparedRegExpRouter 4.31 µs/iter (2.48 µs … 7.95 µs) 4.74 µs 7.95 µs 7.95 µs
TrieRouter 7.77 µs/iter (5.04 µs … 6.68 ms) 5.83 µs 9.42 µs 16.33 µs
LinearRouter 4.19 µs/iter (1.33 µs … 32.59 µs) 2.43 µs 32.59 µs 32.59 µs
MedleyRouter 4.29 µs/iter (3.5 µs … 9.6 µs) 4.06 µs 9.6 µs 9.6 µs
FindMyWay 126.04 µs/iter (80.38 µs … 8.11 ms) 90.5 µs 1.12 ms 2.56 ms
KoaTreeRouter 2.77 µs/iter (2.51 µs … 3.26 µs) 2.9 µs 3.26 µs 3.26 µs
TrekRouter 3.89 µs/iter (3.65 µs … 4.4 µs) 3.98 µs 4.4 µs 4.4 µs
summary for GET /event/abcd1234/comments
KoaTreeRouter
1.41x faster than TrekRouter
1.51x faster than LinearRouter
1.55x faster than MedleyRouter
1.56x faster than PreparedRegExpRouter
2.81x faster than TrieRouter
24.71x faster than RegExpRouter
45.58x faster than FindMyWay
• POST /event/abcd1234/comment
------------------------------------------------------------ -----------------------------
RegExpRouter 82.01 µs/iter (26.04 µs … 13.44 ms) 31.17 µs 906.29 µs 3.06 ms
PreparedRegExpRouter 6.74 µs/iter (2.58 µs … 35.23 µs) 6.05 µs 35.23 µs 35.23 µs
TrieRouter 8.08 µs/iter (5.04 µs … 10.08 ms) 5.83 µs 9.67 µs 16.13 µs
LinearRouter 564.22 ns/iter (433.07 ns … 1.31 µs) 617.83 ns 1.31 µs 1.31 µs
MedleyRouter 4.39 µs/iter (3.66 µs … 12.5 µs) 4.03 µs 12.5 µs 12.5 µs
FindMyWay 123.96 µs/iter (78.21 µs … 13.05 ms) 87.96 µs 438.83 µs 2.23 ms
KoaTreeRouter 3.22 µs/iter (2.52 µs … 6.81 µs) 3.35 µs 6.81 µs 6.81 µs
TrekRouter 4.21 µs/iter (2.54 µs … 14.32 ms) 2.92 µs 6.33 µs 7.13 µs
summary for POST /event/abcd1234/comment
LinearRouter
5.71x faster than KoaTreeRouter
7.46x faster than TrekRouter
7.78x faster than MedleyRouter
11.94x faster than PreparedRegExpRouter
14.32x faster than TrieRouter
145.34x faster than RegExpRouter
219.7x faster than FindMyWay
• GET /very/deeply/nested/route/hello/there
------------------------------------------------------------ -----------------------------
RegExpRouter 100.97 µs/iter (26.17 µs … 17.51 ms) 31.08 µs 1.21 ms 3.09 ms
PreparedRegExpRouter 5.94 µs/iter (2.83 µs … 10.33 µs) 7.18 µs 10.33 µs 10.33 µs
TrieRouter 8.45 µs/iter (4.96 µs … 12.71 ms) 5.71 µs 10.42 µs 19.38 µs
LinearRouter 1.65 µs/iter (1.35 µs … 2.85 µs) 1.67 µs 2.85 µs 2.85 µs
MedleyRouter 4.1 µs/iter (3.59 µs … 5.16 µs) 4.24 µs 5.16 µs 5.16 µs
FindMyWay 120.86 µs/iter (77.17 µs … 15.57 ms) 86.96 µs 415 µs 2.35 ms
KoaTreeRouter 2.77 µs/iter (2.53 µs … 3.9 µs) 2.8 µs 3.9 µs 3.9 µs
TrekRouter 3.92 µs/iter (3.56 µs … 4.99 µs) 3.99 µs 4.99 µs 4.99 µs
summary for GET /very/deeply/nested/route/hello/there
LinearRouter
1.68x faster than KoaTreeRouter
2.38x faster than TrekRouter
2.49x faster than MedleyRouter
3.6x faster than PreparedRegExpRouter
5.12x faster than TrieRouter
61.2x faster than RegExpRouter
73.25x faster than FindMyWay
• GET /static/index.html
------------------------------------------------------------ -----------------------------
RegExpRouter 99.91 µs/iter (26.13 µs … 18.35 ms) 31 µs 1.46 ms 3.55 ms
PreparedRegExpRouter 26.83 µs/iter (5.25 µs … 161.2 µs) 28.99 µs 161.2 µs 161.2 µs
TrieRouter 72.71 µs/iter (4.96 µs … 59.46 ms) 9.04 µs 258.71 µs 488.63 µs
LinearRouter 2.06 µs/iter (1.2 µs … 5.51 µs) 2.67 µs 5.51 µs 5.51 µs
MedleyRouter 3.9 µs/iter (3.6 µs … 4.12 µs) 3.99 µs 4.12 µs 4.12 µs
FindMyWay 125.48 µs/iter (79.04 µs … 19.41 ms) 88.38 µs 242.96 µs 2.3 ms
KoaTreeRouter 2.84 µs/iter (2.64 µs … 3.13 µs) 2.91 µs 3.13 µs 3.13 µs
TrekRouter 3.95 µs/iter (3.62 µs … 4.21 µs) 4.09 µs 4.21 µs 4.21 µs
summary for GET /static/index.html
LinearRouter
1.38x faster than KoaTreeRouter
1.89x faster than MedleyRouter
1.92x faster than TrekRouter
13.01x faster than PreparedRegExpRouter
35.26x faster than TrieRouter
48.46x faster than RegExpRouter
60.86x faster than FindMyWay
$ npm run bench-includes-init:bun
> bench-includes-init:bun
> bun run ./src/bench-includes-init.mts
cpu: Apple M2 Pro
runtime: bun 1.0.12 (arm64-darwin)
benchmark time (avg) (min … max) p75 p99 p995
------------------------------------------------------------ -----------------------------
• GET /user
------------------------------------------------------------ -----------------------------
RegExpRouter 30.48 µs/iter (23.83 µs … 1.7 ms) 30 µs 51.83 µs 66.29 µs
PreparedRegExpRouter 1.34 µs/iter (592.65 ns … 15.25 µs) 934.77 ns 15.25 µs 15.25 µs
TrieRouter 5.82 µs/iter (5.35 µs … 7.61 µs) 5.9 µs 7.61 µs 7.61 µs
LinearRouter 1.14 µs/iter (1.06 µs … 1.45 µs) 1.15 µs 1.45 µs 1.45 µs
MedleyRouter 3.89 µs/iter (3.74 µs … 4.41 µs) 3.93 µs 4.41 µs 4.41 µs
FindMyWay 54.18 µs/iter (40.46 µs … 16.06 ms) 54.29 µs 81.92 µs 91.33 µs
KoaTreeRouter 3.35 µs/iter (3.08 µs … 4.97 µs) 3.28 µs 4.97 µs 4.97 µs
TrekRouter 4.87 µs/iter (4.73 µs … 5.08 µs) 4.91 µs 5.08 µs 5.08 µs
summary for GET /user
LinearRouter
1.17x faster than PreparedRegExpRouter
2.92x faster than KoaTreeRouter
3.4x faster than MedleyRouter
4.25x faster than TrekRouter
5.08x faster than TrieRouter
26.62x faster than RegExpRouter
47.33x faster than FindMyWay
• GET /user/comments
------------------------------------------------------------ -----------------------------
RegExpRouter 31.15 µs/iter (25.58 µs … 896.29 µs) 32.42 µs 46.75 µs 54.83 µs
PreparedRegExpRouter 1.38 µs/iter (575.25 ns … 30.94 µs) 783.92 ns 30.94 µs 30.94 µs
TrieRouter 6.15 µs/iter (5.38 µs … 8.52 µs) 6.45 µs 8.52 µs 8.52 µs
LinearRouter 1.26 µs/iter (1.17 µs … 1.89 µs) 1.28 µs 1.89 µs 1.89 µs
MedleyRouter 4.07 µs/iter (3.89 µs … 4.84 µs) 4.08 µs 4.84 µs 4.84 µs
FindMyWay 52.99 µs/iter (40.88 µs … 8.88 ms) 53.75 µs 81.25 µs 87.21 µs
KoaTreeRouter 3.79 µs/iter (3.16 µs … 9.26 µs) 3.71 µs 9.26 µs 9.26 µs
TrekRouter 5.15 µs/iter (4.92 µs … 5.76 µs) 5.15 µs 5.76 µs 5.76 µs
summary for GET /user/comments
LinearRouter
1.1x faster than PreparedRegExpRouter
3.01x faster than KoaTreeRouter
3.23x faster than MedleyRouter
4.09x faster than TrekRouter
4.88x faster than TrieRouter
24.72x faster than RegExpRouter
42.06x faster than FindMyWay
• GET /user/lookup/username/hey
------------------------------------------------------------ -----------------------------
RegExpRouter 30.84 µs/iter (24.17 µs … 5.83 ms) 32.17 µs 45.5 µs 54.21 µs
PreparedRegExpRouter 1.18 µs/iter (604.05 ns … 16.35 µs) 906.84 ns 16.35 µs 16.35 µs
TrieRouter 10.31 µs/iter (5.73 µs … 72.14 µs) 6.87 µs 72.14 µs 72.14 µs
LinearRouter 1.47 µs/iter (1.39 µs … 1.59 µs) 1.49 µs 1.59 µs 1.59 µs
MedleyRouter 4.49 µs/iter (4.19 µs … 5.79 µs) 4.52 µs 5.79 µs 5.79 µs
FindMyWay 54.68 µs/iter (42.92 µs … 9.42 ms) 55.46 µs 81 µs 85.33 µs
KoaTreeRouter 3.56 µs/iter (3.2 µs … 6.45 µs) 3.49 µs 6.45 µs 6.45 µs
TrekRouter 5.39 µs/iter (5.05 µs … 9.84 µs) 5.2 µs 9.84 µs 9.84 µs
summary for GET /user/lookup/username/hey
PreparedRegExpRouter
1.24x faster than LinearRouter
3.01x faster than KoaTreeRouter
3.79x faster than MedleyRouter
4.56x faster than TrekRouter
8.7x faster than TrieRouter
26.05x faster than RegExpRouter
46.18x faster than FindMyWay
• GET /event/abcd1234/comments
------------------------------------------------------------ -----------------------------
RegExpRouter 34.33 µs/iter (25.54 µs … 6.94 ms) 33.04 µs 90.96 µs 150.29 µs
PreparedRegExpRouter 1.09 µs/iter (597.1 ns … 6.46 µs) 1.02 µs 6.46 µs 6.46 µs
TrieRouter 9.84 µs/iter (4.67 µs … 121.14 ms) 7.13 µs 14.17 µs 19.54 µs
LinearRouter 1.56 µs/iter (1.36 µs … 1.82 µs) 1.62 µs 1.82 µs 1.82 µs
MedleyRouter 4.49 µs/iter (4.09 µs … 4.76 µs) 4.63 µs 4.76 µs 4.76 µs
FindMyWay 56.66 µs/iter (42.75 µs … 1.88 ms) 56.79 µs 100.67 µs 121.04 µs
KoaTreeRouter 3.61 µs/iter (3.21 µs … 7.28 µs) 3.53 µs 7.28 µs 7.28 µs
TrekRouter 5.43 µs/iter (4.97 µs … 6.57 µs) 5.71 µs 6.57 µs 6.57 µs
summary for GET /event/abcd1234/comments
PreparedRegExpRouter
1.44x faster than LinearRouter
3.32x faster than KoaTreeRouter
4.13x faster than MedleyRouter
5x faster than TrekRouter
9.06x faster than TrieRouter
31.61x faster than RegExpRouter
52.17x faster than FindMyWay
• POST /event/abcd1234/comment
------------------------------------------------------------ -----------------------------
RegExpRouter 32.84 µs/iter (25.67 µs … 931.71 µs) 35.04 µs 50.38 µs 59.13 µs
PreparedRegExpRouter 1.86 µs/iter (703.25 ns … 19.42 µs) 1.22 µs 19.42 µs 19.42 µs
TrieRouter 15.42 µs/iter (6.05 µs … 142.22 µs) 9.01 µs 142.22 µs 142.22 µs
LinearRouter 573.28 ns/iter (468.48 ns … 1.16 µs) 562.93 ns 1.16 µs 1.16 µs
MedleyRouter 4.57 µs/iter (4.01 µs … 5.6 µs) 4.7 µs 5.6 µs 5.6 µs
FindMyWay 60.21 µs/iter (41.63 µs … 7.77 ms) 55.67 µs 105.25 µs 147.08 µs
KoaTreeRouter 3.73 µs/iter (3.32 µs … 7.55 µs) 3.68 µs 7.55 µs 7.55 µs
TrekRouter 5.77 µs/iter (5.37 µs … 6.23 µs) 6.01 µs 6.23 µs 6.23 µs
summary for POST /event/abcd1234/comment
LinearRouter
3.25x faster than PreparedRegExpRouter
6.5x faster than KoaTreeRouter
7.97x faster than MedleyRouter
10.07x faster than TrekRouter
26.9x faster than TrieRouter
57.28x faster than RegExpRouter
105.02x faster than FindMyWay
• GET /very/deeply/nested/route/hello/there
------------------------------------------------------------ -----------------------------
RegExpRouter 38.99 µs/iter (26.88 µs … 1.5 ms) 41.25 µs 99.33 µs 115.13 µs
PreparedRegExpRouter 987.13 ns/iter (618.27 ns … 2.18 µs) 1.05 µs 2.18 µs 2.18 µs
TrieRouter 8.33 µs/iter (6.15 µs … 13.69 µs) 10.29 µs 13.69 µs 13.69 µs
LinearRouter 1.86 µs/iter (1.38 µs … 3.83 µs) 2.06 µs 3.83 µs 3.83 µs
MedleyRouter 4.9 µs/iter (4.29 µs … 6.77 µs) 5.09 µs 6.77 µs 6.77 µs
FindMyWay 73.58 µs/iter (45.88 µs … 2.08 ms) 71.46 µs 252.63 µs 405 µs
KoaTreeRouter 4.12 µs/iter (3.24 µs … 7.15 µs) 4.46 µs 7.15 µs 7.15 µs
TrekRouter 5.96 µs/iter (5.07 µs … 7.89 µs) 6.45 µs 7.89 µs 7.89 µs
summary for GET /very/deeply/nested/route/hello/there
PreparedRegExpRouter
1.88x faster than LinearRouter
4.18x faster than KoaTreeRouter
4.96x faster than MedleyRouter
6.04x faster than TrekRouter
8.44x faster than TrieRouter
39.5x faster than RegExpRouter
74.54x faster than FindMyWay
• GET /static/index.html
------------------------------------------------------------ -----------------------------
RegExpRouter 38.2 µs/iter (26.29 µs … 1.5 ms) 40.21 µs 83.33 µs 121.96 µs
PreparedRegExpRouter 1.33 µs/iter (894.09 ns … 2.23 µs) 1.66 µs 2.23 µs 2.23 µs
TrieRouter 14.67 µs/iter (4.67 µs … 2.86 ms) 12.67 µs 117.17 µs 197.13 µs
LinearRouter 2.04 µs/iter (1.34 µs … 5.22 µs) 2.37 µs 5.22 µs 5.22 µs
MedleyRouter 6.41 µs/iter (4.99 µs … 12.08 µs) 6.65 µs 12.08 µs 12.08 µs
FindMyWay 72.48 µs/iter (41.42 µs … 1.62 ms) 68.83 µs 280.33 µs 408.92 µs
KoaTreeRouter 4.76 µs/iter (3.4 µs … 7.76 µs) 5.53 µs 7.76 µs 7.76 µs
TrekRouter 6.68 µs/iter (5.18 µs … 9.86 µs) 8.48 µs 9.86 µs 9.86 µs
summary for GET /static/index.html
PreparedRegExpRouter
1.53x faster than LinearRouter
3.57x faster than KoaTreeRouter
4.8x faster than MedleyRouter
5.01x faster than TrekRouter
11x faster than TrieRouter
28.65x faster than RegExpRouter
54.36x faster than FindMyWay
Bundlesize
I compared the app created by npm create sonik@latest with the following changes.
Add RegExpRouter preset
import { HonoBase } from './hono-base'
import type { HonoOptions } from './hono-base'
import { RegExpRouter } from './router/reg-exp-router'
import type { Env, Schema } from './types'
export class Hono<
E extends Env = Env,
S extends Schema = {},
BasePath extends string = '/'
> extends HonoBase<E, S, BasePath> {
constructor(options: HonoOptions<E> = {}) {
super(options)
this.router = new RegExpRouter()
}
}
Add rollup plugin
import { defineConfig } from "vite";
import sonik from "sonik/vite";
import pages from "@sonikjs/cloudflare-pages";
import {
buildInitParams,
serializeInitParams,
} from "../../honojs/hono/src/router/reg-exp-router";
// replace RegExpRouter with PreparedRegExpRouter at build time
function replacePreparedRegExpRouter(initPrams) {
return {
name: "hono-prepared-reg-exp-router",
load(id) {
const match = id.match(/router\/reg-exp-router\/index.(js|ts)$/);
if (match) {
const ext = match[1];
const serialized = serializeInitParams(buildInitParams(initPrams));
return `
import { PreparedRegExpRouter } from './prepared-router.${ext}'
export class RegExpRouter extends PreparedRegExpRouter {
constructor() {
super(...${serialized});
}
}
`;
}
return null;
},
};
}
export default defineConfig({
plugins: [
replacePreparedRegExpRouter({
routes: [
{
method: "ALL",
path: "/about/*",
},
{
method: "ALL",
path: "/*",
},
{
method: "ALL",
path: "/static/*",
},
{
method: "GET",
path: "/about/:name",
},
{
method: "GET",
path: "/",
},
],
}),
sonik(),
pages(),
],
});
With this setup, the npx vite build resulted in 71.27 kB -> 55.30 kB.
When can we use it?
It can be used for general applications, but it is a bit difficult to use. File-based routing has the following characteristics that make it easy to implement.
- Routing that falls back to TrieRouter is not generated; can assume RegExpRouter.
- Basically, we can know the routing in advance because it is done via a build tool.
Author should do the followings, if applicable
- [ ] Add tests
- [ ] Run tests
- [ ]
yarn denoifyto generate files for Deno
Hi, @yusukebe. What do you think about this?
Hi @usualoma
Interesting approach. I'm glad you are interested in file-based routing!
Routing that falls back to TrieRouter is not generated; can assume RegExpRouter. Basically, we can know the routing in advance because it is done via a build tool.
Exactly right. We could make Hono specialize in file-based routing and improve the router. However, it might be too much optimization. I'd like to take some more time to think about it.
This might not be related, but it's one idea. Can we improve performance like this with explicitly build routes?
app.get('/', (c) => c.json(0))
app.get('/a', (c) => c.json(0))
app.get('/b', (c) => c.json(0))
export default app.build()
@yusukebe Thank you for your comment.
After creating the PR, I thought about it for a while. File-based routing can easily extract paths, but it is expensive to run import to extract even the method names (GET, POST, etc) used.
Based on this, I changed the API in 54b82ca to "just list the paths that may be used". If we proceed with this PR, I would like to do it this way.
before
buildInitParams({
routes: [
{ method: 'ALL', path: '*' },
{ method: 'ALL', path: '/posts/:id/*' },
{ method: 'GET', path: '*' },
{ method: 'GET', path: '/' },
{ method: 'GET', path: '/static' },
{ method: 'GET', path: '/posts/:id/*' },
{ method: 'GET', path: '/posts/:id' },
{ method: 'GET', path: '/posts/:id/comments' },
{ method: 'POST', path: '/posts' },
{ method: 'PUT', path: '/posts/:id' },
],
})
after
buildInitParams({
paths: ['*', '/static', '/posts/:id/*', '/posts/:id', '/posts/:id/comments', '/posts'],
})
@yusukebe
This might not be related, but it's one idea. Can we improve performance like this with explicitly build routes?
I guess you mean "finish initializing the router before the first request comes in". If so, I think the following changes will do it.
diff --git a/src/hono-base.ts b/src/hono-base.ts
index f118a741..b4539b74 100644
--- a/src/hono-base.ts
+++ b/src/hono-base.ts
@@ -236,10 +236,14 @@ class Hono<
}
get routerName() {
- this.matchRoute('GET', '/')
+ this.build()
return this.router.name
}
+ build() {
+ this.router.build?.()
+ return this
+ }
+
/**
* @deprecated
* `app.head()` is no longer used.
diff --git a/src/router.ts b/src/router.ts
index 78cd65e5..71e45005 100644
--- a/src/router.ts
+++ b/src/router.ts
@@ -8,6 +8,7 @@ export interface Router<T> {
name: string
add(method: string, path: string, handler: T): void
match(method: string, path: string): Result<T>
+ build?(): void
}
export type ParamIndexMap = Record<string, number>
diff --git a/src/router/reg-exp-router/router.ts b/src/router/reg-exp-router/router.ts
index 82a8d0ea..30affc24 100644
--- a/src/router/reg-exp-router/router.ts
+++ b/src/router/reg-exp-router/router.ts
@@ -208,6 +208,11 @@ export class RegExpRouter<T> implements Router<T> {
}
match(method: string, path: string): Result<T> {
+ this.build()
+ return this.match(method, path)
+ }
+
+ build() {
clearWildcardRegExpCache() // no longer used.
const matchers = this.buildAllMatchers()
@@ -228,8 +233,6 @@ export class RegExpRouter<T> implements Router<T> {
const index = match.indexOf('', 1)
return [matcher[1][index], match]
}
-
- return this.match(method, path)
}
private buildAllMatchers(): Record<string, Matcher<T>> {
And maybe SmartRouter. I have not confirmed that it works.
diff --git a/src/router/smart-router/router.ts b/src/router/smart-router/router.ts
index 7b0510d8..3a970ca8 100644
--- a/src/router/smart-router/router.ts
+++ b/src/router/smart-router/router.ts
@@ -20,6 +20,11 @@ export class SmartRouter<T> implements Router<T> {
}
match(method: string, path: string): Result<T> {
+ this.build()
+ return this.match(method, path)
+ }
+
+ build() {
if (!this.routes) {
throw new Error('Fatal error')
}
@@ -27,14 +32,13 @@ export class SmartRouter<T> implements Router<T> {
const { routers, routes } = this
const len = routers.length
let i = 0
- let res
for (; i < len; i++) {
const router = routers[i]
try {
routes.forEach((args) => {
router.add(...args)
})
- res = router.match(method, path)
+ router.build?.()
} catch (e) {
if (e instanceof UnsupportedPathError) {
continue
@@ -55,8 +59,6 @@ export class SmartRouter<T> implements Router<T> {
// e.g. "SmartRouter + RegExpRouter"
this.name = `SmartRouter + ${this.activeRouter.name}`
-
- return res as Result<T>
}
get activeRouter() {
@usualoma
For example, in this project that provides file-based routing:
https://github.com/yusukebe/file-base-routing-framework
We can write src/framework.ts using PreparedRegExpRouter as follows:
diff --git a/src/framework.ts b/src/framework.ts
index d75e3b9..e781263 100644
--- a/src/framework.ts
+++ b/src/framework.ts
@@ -35,7 +35,19 @@ const ROUTES = import.meta.glob<RouteFile>('/app/routes/**/[a-z0-9[-][a-z0-9[_-]
})
export const createApp = () => {
- const app = new Hono()
+ const paths: string[] = []
+ Object.keys(ROUTES).map((key) => {
+ paths.push(filePathToPath(key.replace(/^\/app\/routes/, '')))
+ })
+
+ const preparedParams = buildInitParams({
+ paths
+ })
+
+ const app = new HonoBase({
+ // @ts-ignore
+ router: new PreparedRegExpRouter(preparedParams[0], preparedParams[1])
+ })
for (const [key, routes] of Object.entries(RENDERERS)) {
const dirPath = pathToDirPath(key)
The bundle size difference:
- Default:
dist/_worker.js 30.39 kB - Only
PreparedRegExpRouter:dist/_worker.js 27.84 kB
The difference isn't as significant as I expected, but this approach allows us to implement it in the framework without creating Vite or Rollup plugins.
I guess you mean "finish initializing the router before the first request comes in".
Yes, I mean that. But, I think we can make it like the following:
diff --git a/src/hono-base.ts b/src/hono-base.ts
index f118a74..40a3ec4 100644
--- a/src/hono-base.ts
+++ b/src/hono-base.ts
@@ -5,6 +5,7 @@ import { HTTPException } from './http-exception'
import { HonoRequest } from './request'
import type { Router } from './router'
import { METHOD_NAME_ALL, METHOD_NAME_ALL_LOWERCASE, METHODS } from './router'
+import { PreparedRegExpRouter, buildInitParams } from './router/reg-exp-router'
import type {
Env,
ErrorHandler,
@@ -251,10 +252,7 @@ class Hono<
}
private addRoute(method: string, path: string, handler: H) {
- method = method.toUpperCase()
- path = mergePath(this.#basePath, path)
const r: RouterRoute = { path: path, method: method, handler: handler }
- this.router.add(method, path, [handler, r])
this.routes.push(r)
}
@@ -373,6 +371,24 @@ class Hono<
event.respondWith(this.dispatch(event.request, event, undefined, event.request.method))
})
}
+
+ build = () => {
+ const paths = this.routes.map((route) => {
+ return route.path
+ })
+ console.log(paths)
+ const preparedParams = buildInitParams({
+ paths,
+ })
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ this.router = new PreparedRegExpRouter(preparedParams[0], preparedParams[1])
+ this.routes.map((route) => {
+ const r: RouterRoute = { path: route.path, method: route.method, handler: route.handler }
+ this.router.add(route.method.toUpperCase(), route.path, [route.handler, r])
+ })
+ return this
+ }
}
export { Hono as HonoBase }
The bundle size difference:
yusuke $ wrangler deploy --minify --dry-run src/default-router.ts
⛅️ wrangler 3.9.0 (update available 3.19.0)
------------------------------------------------------
Total Upload: 21.95 KiB / gzip: 8.04 KiB
--dry-run: exiting now.
yusuke $ wrangler deploy --minify --dry-run src/prepared-router.ts
⛅️ wrangler 3.9.0 (update available 3.19.0)
------------------------------------------------------
Total Upload: 18.27 KiB / gzip: 7.09 KiB
--dry-run: exiting now.
Nevertheless, this code actually works, but it is not practical to write this process in hono-base.ts.
@yusukebe Thank you so much! I think the following result is "smaller because TrieRouter is no longer included".
Default: dist/_worker.js 30.39 kB Only PreparedRegExpRouter: dist/_worker.js 27.84 kB
The main point of PreparedRegExpRouter is to exclude the following source code from the bundle
src/router/reg-exp-router/{trie,node}.ts
Therefore, we need to somehow work with the bundling tool to remove the following code.
import { RegExpRouter } from '. /router/reg-exp-router'
Therefore, we need to somehow work with the bundling tool to remove the following code.
Ah, I didn't write that, but it imports them like this. So, it does not import RegExpRouter or TrieRouter:
import { HonoBase } from '/Users/yusuke/work/honojs/hono/src/hono-base'
import {
PreparedRegExpRouter,
buildInitParams
} from '/Users/yusuke/work/honojs/hono/src/router/reg-exp-router/prepared-router'
import type { H, MiddlewareHandler } from '/Users/yusuke/work/honojs/hono/src/types'
The entire diff:
diff --git a/src/framework.ts b/src/framework.ts
index d75e3b9..96d4316 100644
--- a/src/framework.ts
+++ b/src/framework.ts
@@ -1,5 +1,9 @@
-import { Hono } from 'hono'
-import type { H, MiddlewareHandler } from 'hono/types'
+import { HonoBase } from '/Users/yusuke/work/honojs/hono/src/hono-base'
+import {
+ PreparedRegExpRouter,
+ buildInitParams
+} from '/Users/yusuke/work/honojs/hono/src/router/reg-exp-router/prepared-router'
+import type { H, MiddlewareHandler } from '/Users/yusuke/work/honojs/hono/src/types'
const METHODS = ['GET', 'POST', 'PUT', 'DELETE'] as const
@@ -35,7 +39,19 @@ const ROUTES = import.meta.glob<RouteFile>('/app/routes/**/[a-z0-9[-][a-z0-9[_-]
})
export const createApp = () => {
- const app = new Hono()
+ const paths: string[] = []
+ Object.keys(ROUTES).map((key) => {
+ paths.push(filePathToPath(key.replace(/^\/app\/routes/, '')))
+ })
+
+ const preparedParams = buildInitParams({
+ paths
+ })
+
+ const app = new HonoBase({
+ // @ts-ignore
+ router: new PreparedRegExpRouter(preparedParams[0], preparedParams[1])
+ })
for (const [key, routes] of Object.entries(RENDERERS)) {
const dirPath = pathToDirPath(key)
@yusukebe
Yeah, I know, but buildInitParams() depends on RegExpRouter(), so when we import buildInitParams(), it bundles the following source code.
src/router/reg-exp-router/{trie,node}.ts https://github.com/honojs/hono/pull/1796/files#diff-05f98af8ac89c7dd686b79c41d8167478c790c0051245d79c3b0f0554351dc63R77
And buildInitParams() is as heavy as the normal RegExpRouter() initialization, so it must be done before bundling to optimize.
Ah, but I don't deny the following opinion.
it might be too much optimization
This code is very high-performing, but it might be too complicated.
We might be happier to optimize PatternRouter than this PR approach.
Yeah, I know, but
buildInitParams()depends onRegExpRouter(), so when we importbuildInitParams(), it bundles the following source code.
And
buildInitParams()is as heavy as the normalRegExpRouter()initialization, so it must be done before bundling to optimize.
I see, you are right. But, I don't want to create this router and plugins for bundlers just for this optimization. Perhaps we could enhance performance further with static analytics before bundling, but that would be really too much optimization.
Either way, this PR has inspired us. Let's keep it open still.
In addition, It is a good idea to have a "RegExpRouter" preset for file-based routing.