v5: `hono/build`
I'm creating a new router named "PreparedRouter" using AOT.
This issue is a thread to solicit opinions on concerns.
Quick response.
Rather than AOT, it would be nice to have a build script or bundler that writes out an optimal application "file" that includes the hono. Then, it will work in Cloudflare Workers and other environments where eval and Function are not available.
I forgot, but we discussed this issue - creating a build script or bundler - in this project Issue or PR with @usualoma.
I have seen PreparedRegExpRouter in the past.
I've seen the whole discussion, but it seems difficult to interfere with the build.
And you as said, it currently depends on Function, so it won't work with Cloudflare workers, etc.
However, I think it will help speed things up in VPS and other environments.
I’ve also been considering a feature like this for Hono, and I agree with @yusukebe on incorporating a build script. I recall we previously discussed this topic while comparing the speeds of Hono and Elysia with AOT compilation.
Rather than embedding this directly into the router, integrating it into something like Honox might be more effective. The code/logic is already modularized with nested routes, allowing us to implement a script that merges these routes into a single, optimized Hono app. This approach could enhance performance across different runtimes.
The key question now is which specific optimizations we could include in the build script to maximize efficiency. Thoughts?
Even if we choice to build, we need the logic of code generation for optimization, so we will work on PreparedRouter.
Also, the following may be of use.
new PreparedRouter({
preparedMatch: () => {
...
} // precompiled
})
It should be able to be utilized when build.
I don't know if that's possible, but if we can make good use of Parcel's macros, the prepared router will seem to make sense. https://parceljs.org/features/macros/
The macros also work with Vite etc. https://github.com/devongovett/unplugin-parcel-macros
But, IMO, we don't have to implement the PreparedRouter or build a script or bundler (just for a "router") because our routers are already fast enough. If we have a faster router, it may not affect us in the real world.
I'm also interested in this area, and it's worth considering if there is a good approach. I'm also interested in macros.
The reasons I considered this with "honox" before are as follows.
- Going through the build process
- Routing being determined statically
It isn't easy to prepare for apps that don't use static routing (use variables). I think that apps that use variables are in the minority, but I think it's inevitable that there will be conditions that determine whether or not they can be applied.
There are currently hono, hono/quick, and hono/tiny, and they meet most requirements. So, I think the difficult part is whether or not we can prepare something that makes sense to add to them.
What do you think about anything other than routing?
For example, define a schema for the incoming JSON. After building the app, an optimized JSON parser is written in the build script. The parsing is then much faster than normally using JSON.parse.
some initial ideas -
- Combine all chained validators into a single, unified validator during the build process.
- Implement a handler that pre-generates responses at build time for faster delivery, taking a similar approach to static site generation.
- Move toward compiling all components at build time for improved efficiency.
Haha, topics are getting bigger! But I have the most fun when we talk about these things.
For example, define a schema for the incoming JSON. After building the app, an optimized JSON parser is written in the build script. The parsing is then much faster than normally using JSON.parse.
It is why elysia is fast. I want Hono to implement it.
And my personal opinion, build-time building is better than run-time building. Outputting code can reduce build size.
It is why elysia is fast.
Yes, exactly (though I don't want to fight with Elysia).
hono/build is interesting for me.
It has the potential to become a hot topic for v5.
In the future it may depend external package such as TypeScript compiler API and babel. So I think it should be in hono/middleware or other repo, and create @hono/build.
In the future it may depend external package such as TypeScript compiler API and babel. So I think it should be in hono/middleware or other repo, and create
@hono/build.
+1
Most of the work has been completed. I will only write a brief description when school is over.
Precompiled Prepared Router (Example)
new function(){let t=function t(e,n,r,u,l,[i]){let o=l[n];if(o){let a=o[e]||o.ALL;if(a)return[a]}let f=u[n],p=f&&(f[e]||f.ALL)||[];Object.create(null);let c=n.split("/");if("GET"===e&&"bar"===c[1]&&3===c.length&&!0==!!c[2]){let h=new r;h.id=c[2],p.push([i,h,1])}return p.length>1&&p.sort((t,e)=>t[2]-e[2]),[p.map(([t,e])=>[t,e])]},e=Object.create(null),n=(()=>{let t=function(){};return t.prototype=e,t})(),r={"/baz/baz":{ALL:[]}},u={"/foo":{GET:[]}},l=[];return{name:"PreparedRouter",add:function(t,n,i){t in(r[n]||e)?r[n][t].push([i,e]):t in(u[n]||e)?u[n][t].push([i,e]):l.push(i)},match:function(e,i){return t(e,i,n,r,u,l)}}};
(this is old)
router.add('GET', '/foo', 'foo')
router.add('GET', '/bar/:id', 'bar + :id')
router.add('ALL', '/baz/baz', 'baz + baz')
- Generated Matcher
function anonymous(method, path, createParams, staticHandlers, preparedHandlers, {
handler1,
handler2,
handler3,
handler4,
handler5,
handler6,
handler7
}) {
const preparedMethods = preparedHandlers[path];
const preparedResult = preparedMethods?.[method]
if (preparedResult) {
return preparedResult;
}
const matchResult = [];
const emptyParams = new createParams();
const pathParts = path.split('/');
if (method === 'GET') {
if (!!pathParts[2] === true) {
if (pathParts[1] === 'event') {
if (pathParts.length === 3) {
const params = new createParams();
params.id = pathParts[2];
matchResult.push({
handler: handler3,
params: params,
order: 5
})
} else if (pathParts.length === 4) {
if (pathParts[3] === 'comments') {
const params = new createParams();
params.id = pathParts[2];
matchResult.push({
handler: handler4,
params: params,
order: 6
})
}
}
} else if (pathParts[1] === 'map') {
if (pathParts.length === 4) {
if (pathParts[3] === 'events') {
const params = new createParams();
params.location = pathParts[2];
matchResult.push({
handler: handler6,
params: params,
order: 8
})
}
}
}
}
if (pathParts[1] === 'user') {
if (pathParts[2] === 'lookup') {
if (pathParts.length === 5) {
if (!!pathParts[4] === true) {
if (pathParts[3] === 'username') {
const params = new createParams();
params.username = pathParts[4];
matchResult.push({
handler: handler1,
params: params,
order: 3
})
} else if (pathParts[3] === 'email') {
const params = new createParams();
params.address = pathParts[4];
matchResult.push({
handler: handler2,
params: params,
order: 4
})
}
}
}
}
} else if (pathParts[1] === 'static') {
if (pathParts.length >= 2) {
matchResult.push({
handler: handler7,
params: emptyParams,
order: 11
})
}
}
} else if (method === 'POST') {
if (pathParts[1] === 'event') {
if (!!pathParts[2] === true) {
if (pathParts.length === 4) {
if (pathParts[3] === 'comment') {
const params = new createParams();
params.id = pathParts[2];
matchResult.push({
handler: handler5,
params: params,
order: 7
})
}
}
}
}
}
if (matchResult.length > 1) {
matchResult.sort((a, b) => a.order - b.order);
}
return [matchResult.map(({
handler,
params
}) => [handler, params])];
};
Prepared Router, like Trie Router, supports all path formats
and the speed depends on the format of the application path, it is a little inferior to RegExp Router, but only by a factor of 1.1 ~ 1.3 and very fast.. (not precompiled and measured in benchmarks/routers and this may be improved in the future.)
Initialization is as fast as Linear Router when precompiled, and the post-compiled code is as minimal as PatternRouter.
And it generates matchers as smartly as SmartRouter ( Really!! ).
By introducing the concept of precompilation, Prepared Router can be used in environments where AOT is not available, such as Cloudflare Workers.
I will make the slides about this later.
summary for all together
Hono RegExpRouter
1.21x faster than Memoirist
1.29x faster than Hono PreparedRouter
1.41x faster than koa-tree-router
1.57x faster than @medley/router
1.72x faster than rou3
1.87x faster than radix3
2.39x faster than trek-router
3.04x faster than find-my-way
4.04x faster than Hono PatternRouter
5.11x faster than koa-router
12.22x faster than Hono TrieRouter
16.09x faster than express (WARNING: includes handling)
(Deno v2)
// 1
summary for all together
Memoirist
1.09x faster than Hono RegExpRouter
1.23x faster than Hono PreparedRouter
1.26x faster than @medley/router
2.13x faster than radix3
2.28x faster than rou3
2.9x faster than find-my-way
3.01x faster than koa-tree-router
4.06x faster than Hono PatternRouter
4.7x faster than trek-router
5.28x faster than koa-router
10.92x faster than Hono TrieRouter
14.45x faster than express (WARNING: includes handling)
// 2
summary for all together
Memoirist
1.14x faster than Hono RegExpRouter
1.3x faster than @medley/router
1.4x faster than Hono PreparedRouter
2.11x faster than radix3
2.35x faster than rou3
2.45x faster than koa-tree-router
2.95x faster than find-my-way
4.06x faster than trek-router
4.22x faster than Hono PatternRouter
5.55x faster than koa-router
10.22x faster than Hono TrieRouter
12.91x faster than express (WARNING: includes handling)
// 3
summary for all together
Memoirist
1.07x faster than Hono PreparedRouter
1.11x faster than Hono RegExpRouter
1.33x faster than @medley/router
2.08x faster than radix3
2.36x faster than rou3
2.85x faster than koa-tree-router
2.91x faster than find-my-way
3.96x faster than Hono PatternRouter
4.35x faster than trek-router
5.25x faster than koa-router
11.09x faster than Hono TrieRouter
12.83x faster than express (WARNING: includes handling)
(Bun)
// includes precompiled
summary for all together
Memoirist
1.02x faster than Hono PreparedRouter (precompiled)
1.08x faster than Hono RegExpRouter
1.24x faster than @medley/router
1.57x faster than Hono PreparedRouter
2.03x faster than radix3
2.24x faster than rou3
2.93x faster than find-my-way
2.95x faster than koa-tree-router
4.03x faster than trek-router
4.32x faster than Hono PatternRouter
5.13x faster than koa-router
11.8x faster than Hono TrieRouter
12.49x faster than express (WARNING: includes handling)
(Bun)
When precompile is performed, it speeds up the process, perhaps due to optimization.
Since then, several optimizations have been made, Benchmark results often show that the speed of a precompiled Prepared Router exceeds that of a RegExp Router.
Prepared Router may be the fastest.
summary for all together
Hono PreparedRouter (precompiled)
1.05x faster than Memoirist
1.09x faster than Hono PreparedRouter
1.12x faster than Hono RegExpRouter
1.37x faster than @medley/router
2.05x faster than radix3
2.09x faster than rou3
2.34x faster than koa-tree-router
3.24x faster than find-my-way
4.34x faster than Hono PatternRouter
4.56x faster than trek-router
5.43x faster than koa-router
10.23x faster than Hono TrieRouter
10.7x faster than express (WARNING: includes handling)
24.23x faster than Hono LinearRouter
Fastest in Bun. Node and Deno marked about 1.5x.
I think we've gone fast spped enough, so we can move on to the next step. This is what I am assuming.
const app = new Hono({
router: new PreparedRouter()
})
after build
const app = new Hono({
router: new (function() {...})()
})
I think it is fast enough without building on runtimes other than edge. Cloudflare Workers and others don't allow I/O operations at the top level, but that's preferable. We can do a dry run at build time to collect route information and precompile.
I would like to hear your opinions.
Short description https://speakerdeck.com/edamamex2189/a-description-of-hono-prepared-router
Hi @EdamAme-x, Happy New Year!
Can you share any scripts to compile that prepared router?
The JavaScript engine may optimize for switch rather than if trie trees.
A simple benchmark is here.
Invalid Benchmark
Node: v22.12.0
| (index) | Task Name | ops/sec | Average Time (ns) | Margin | Samples |
|---|---|---|---|---|---|
| 0 | 'Trie Match' | '3,764,011' | 265.674020167818 | '±0.55%' | 1882006 |
| 1 | 'Switch Match' | '3,817,087' | 261.9798652795252 | '±0.52%' | 1908544 |
Bun: v1.1.38
| Task Name | ops/sec | Average Time (ns) | Margin | Samples | |
|---|---|---|---|---|---|
| 0 | Trie Match | 2,682,936 | 372.7259444683287 | ±1.96% | 1341469 |
| 1 | Switch Match | 6,026,660 | 165.92936521072212 | ±1.65% | 3013331 |
Benchmark Source
// benchmark.ts
import { Bench } from 'tinybench';
// Define the test strings
const testStrings = [
'apple',
'banana',
'cherry',
'date',
'elderberry',
'fig',
'grape',
'honeydew',
'kiwi',
'lemon',
'mango',
'nectarine',
'orange',
'papaya',
'quince',
'raspberry',
'strawberry',
'tangerine',
'ugli fruit',
'vanilla',
'watermelon',
'xigua',
'yellow passion fruit',
'zucchini'
];
// Prepare test inputs (including strings that do not exist)
const inputs = [
...testStrings,
'blueberry',
'cantaloupe',
'dragonfruit',
'eggplant',
'guava'
];
function trieMatch(input: string): string {
if (input.length === 5) {
if (input === 'apple') {
return 'apple is in the trie.';
} else if (input === 'mango') {
return 'mango is in the trie.';
}
} else if (input.length === 6) {
if (input === 'banana') {
return 'banana is in the trie.';
} else if (input === 'cherry') {
return 'cherry is in the trie.';
} else if (input === 'orange') {
return 'orange is in the trie.';
}
}
else if (input.length === 4) {
if (input === 'date') {
return 'date is in the trie.';
} else if (input === 'fig') {
return 'fig is in the trie.';
}
} else if (input.length === 9) {
if (input === 'honeydew') {
return 'honeydew is in the trie.';
} else if (input === 'tangerine') {
return 'tangerine is in the trie.';
}
} else if (input.length === 7) {
if (input === 'grape') {
return 'grape is in the trie.';
} else if (input === 'nectarine') {
return 'nectarine is in the trie.';
}
}
if (input === 'elderberry') {
return 'elderberry is in the trie.';
} else if (input === 'kiwi') {
return 'kiwi is in the trie.';
} else if (input === 'lemon') {
return 'lemon is in the trie.';
} else if (input === 'papaya') {
return 'papaya is in the trie.';
} else if (input === 'quince') {
return 'quince is in the trie.';
} else if (input === 'raspberry') {
return 'raspberry is in the trie.';
} else if (input === 'strawberry') {
return 'strawberry is in the trie.';
} else if (input === 'ugli fruit') {
return 'ugli fruit is in the trie.';
} else if (input === 'vanilla') {
return 'vanilla is in the trie.';
} else if (input === 'watermelon') {
return 'watermelon is in the trie.';
} else if (input === 'xigua') {
return 'xigua is in the trie.';
} else if (input === 'yellow passion fruit') {
return 'yellow passion fruit is in the trie.';
} else if (input === 'zucchini') {
return 'zucchini is in the trie.';
}
return `${input} is NOT in the trie.`;
}
// Function for matching using switch statement
function switchMatch(input: string): string {
switch (input) {
case 'apple':
return 'apple is in the switch.';
case 'banana':
return 'banana is in the switch.';
case 'cherry':
return 'cherry is in the switch.';
case 'date':
return 'date is in the switch.';
case 'elderberry':
return 'elderberry is in the switch.';
case 'fig':
return 'fig is in the switch.';
case 'grape':
return 'grape is in the switch.';
case 'honeydew':
return 'honeydew is in the switch.';
case 'kiwi':
return 'kiwi is in the switch.';
case 'lemon':
return 'lemon is in the switch.';
case 'mango':
return 'mango is in the switch.';
case 'nectarine':
return 'nectarine is in the switch.';
case 'orange':
return 'orange is in the switch.';
case 'papaya':
return 'papaya is in the switch.';
case 'quince':
return 'quince is in the switch.';
case 'raspberry':
return 'raspberry is in the switch.';
case 'strawberry':
return 'strawberry is in the switch.';
case 'tangerine':
return 'tangerine is in the switch.';
case 'ugli fruit':
return 'ugli fruit is in the switch.';
case 'vanilla':
return 'vanilla is in the switch.';
case 'watermelon':
return 'watermelon is in the switch.';
case 'xigua':
return 'xigua is in the switch.';
case 'yellow passion fruit':
return 'yellow passion fruit is in the switch.';
case 'zucchini':
return 'zucchini is in the switch.';
default:
return `${input} is NOT in the switch.`;
}
}
// Setup benchmarks
const bench = new Bench();
// Benchmark for trie-based matching
bench.add('Trie Match', () => {
for (const input of inputs) {
trieMatch(input);
}
});
// Benchmark for switch-based matching
bench.add('Switch Match', () => {
for (const input of inputs) {
switchMatch(input);
}
});
// Run benchmarks
await bench.warmup();
await bench.run()
console.table(bench.table());
The speed improvement with switch is interesting. I will consider it later.
Hi @EdamAme-x, Happy New Year!
Can you share any scripts to compile that prepared router?
Happy New Year! (1day late)
This is a branch under development. Please forgive that the code does not follow the overall style and that the naming conventions are disparate.
https://github.com/EdamAme-x/hono/tree/feat/prepared-router
There is still plenty of room for improvement, but this is the current code. Perhaps it should be separated into other repositories in terms of code size.
https://github.com/EdamAme-x/hono/blob/feat/prepared-router/src/router/prepared-router/builder.ts
Sorry. My benchmark is invalid. Correct benchmark is here. Node(V8): length faster than switch Bun(JavaScriptCore): switch faster than length
Node: v22.12.0
| (index) | Task Name | ops/sec | Average Time (ns) | Margin | Samples |
|---|---|---|---|---|---|
| 0 | 'Switch-based' | '2,118,938' | 471.93445779492174 | '±1.05%' | 1059470 |
| 1 | 'Length-based' | '2,633,433' | 379.73231909365006 | '±0.85%' | 1316717 |
Bun: v1.1.38
| Task Name | ops/sec | Average Time (ns) | Margin | Samples | |
|---|---|---|---|---|---|
| 0 | Switch-based | 3,073,569 | 325.35462019734763 | ±2.10% | 1536785 |
| 1 | Length-based | 1,982,698 | 504.36304029853005 | ±1.49% | 991350 |
Benchmark Source Code
// benchmark.ts
import { Bench } from "tinybench";
/**
* 1) A simple switch-based matching (switchMatch)
* 2) A real Trie-based matching (realTrieMatch)
* 3) A "trie-inspired" switch-based matching (optimizedSwitchMatch)
*/
// Test strings (no additional strings will be added)
const testStrings = [
"apple",
"banana",
"cherry",
"date",
"elderberry",
"fig",
"grape",
"honeydew",
"kiwi",
"lemon",
"mango",
"nectarine",
"orange",
"papaya",
"quince",
"raspberry",
"strawberry",
"tangerine",
"ugli fruit",
"vanilla",
"watermelon",
"xigua",
"yellow passion fruit",
"zucchini",
];
// Extra inputs that do not exist in the set
const extraStrings = [
"blueberry",
"cantaloupe",
"dragonfruit",
"eggplant",
"guava",
];
// Combined input set for benchmark
const inputs = [...testStrings, ...extraStrings];
/* ---------------------------------------------
(1) Normal switch-based function
--------------------------------------------- */
function switchMatch(input: string): string {
switch (input) {
case "apple":
return "apple (switch)";
case "banana":
return "banana (switch)";
case "cherry":
return "cherry (switch)";
case "date":
return "date (switch)";
case "elderberry":
return "elderberry (switch)";
case "fig":
return "fig (switch)";
case "grape":
return "grape (switch)";
case "honeydew":
return "honeydew (switch)";
case "kiwi":
return "kiwi (switch)";
case "lemon":
return "lemon (switch)";
case "mango":
return "mango (switch)";
case "nectarine":
return "nectarine (switch)";
case "orange":
return "orange (switch)";
case "papaya":
return "papaya (switch)";
case "quince":
return "quince (switch)";
case "raspberry":
return "raspberry (switch)";
case "strawberry":
return "strawberry (switch)";
case "tangerine":
return "tangerine (switch)";
case "ugli fruit":
return "ugli fruit (switch)";
case "vanilla":
return "vanilla (switch)";
case "watermelon":
return "watermelon (switch)";
case "xigua":
return "xigua (switch)";
case "yellow passion fruit":
return "yellow passion fruit (switch)";
case "zucchini":
return "zucchini (switch)";
default:
return `${input} (switch) - NOT found`;
}
}
/* ---------------------------------------------
(2) Length-based function
--------------------------------------------- */
function lengthBasedMatch(input: string): string {
if (input.length === 3) {
if (input === 'fig') {
return 'fig is in the trie.';
}
} else if (input.length === 4) {
if (input === 'date') {
return 'date is in the trie.';
} else if (input === 'kiwi') {
return 'kiwi is in the trie.';
}
} else if (input.length === 5) {
if (input === 'apple') {
return 'apple is in the trie.';
} else if (input === 'mango') {
return 'mango is in the trie.';
} else if (input === 'grape') {
return 'grape is in the trie.';
} else if (input === 'lemon') {
return 'lemon is in the trie.';
} else if (input === 'xigua') {
return 'xigua is in the trie.';
}
} else if (input.length === 6) {
if (input === 'banana') {
return 'banana is in the trie.';
} else if (input === 'cherry') {
return 'cherry is in the trie.';
} else if (input === 'orange') {
return 'orange is in the trie.';
} else if (input === 'papaya') {
return 'papaya is in the trie.';
} else if (input === 'quince') {
return 'quince is in the trie.';
}
} else if (input.length === 7) {
if (input === 'vanilla') {
return 'vanilla is in the trie.';
}
} else if (input.length === 8) {
if (input === 'honeydew') {
return 'honeydew is in the trie.';
} else if (input === 'zucchini') {
return 'zucchini is in the trie.';
}
} else if (input.length === 9) {
if (input === 'tangerine') {
return 'tangerine is in the trie.';
} else if (input === 'nectarine') {
return 'nectarine is in the trie.';
} else if (input === 'raspberry') {
return 'raspberry is in the trie.';
}
} else if (input.length === 10) {
if (input === 'elderberry') {
return 'elderberry is in the trie.';
} else if (input === 'watermelon') {
return 'watermelon is in the trie.';
} else if (input === 'strawberry') {
return 'strawberry is in the trie.';
} else if (input === 'ugli fruit') {
return 'ugli fruit is in the trie.';
}
} else if (input.length === 20) {
if (input === 'yellow passion fruit') {
return 'yellow passion fruit is in the trie.';
}
}
return `${input} is NOT in the trie.`;
}
/* ---------------------------------------------
Setup benchmark
--------------------------------------------- */
const bench = new Bench();
// 1) Plain switch-based
bench.add("Switch-based", () => {
for (const inp of inputs) {
switchMatch(inp);
}
});
// 2) Length-based
bench.add("Length-based", () => {
for (const inp of inputs) {
lengthBasedMatch(inp);
}
});
await bench.warmup();
await bench.run();
console.table(bench.table());
interesting news https://github.com/cloudflare/workerd/pull/4142
@EdamAme-x
Yes. Interesting.
What we have to consider is that the process is not permanently on Cloudflare Workers. This means that if you can build something and place it globally, but the process is deleted, it will need to be rebuilt the next time.