-
-
Notifications
You must be signed in to change notification settings - Fork 630
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
v5: hono/build
#3659
Comments
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 |
I forgot, but we discussed this issue - creating a build script or bundler - in this project Issue or PR with @usualoma. |
I have seen And you as said, it currently depends on Function, so it won't work with Cloudflare workers, etc. |
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 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. The macros also work with Vite etc. |
But, IMO, we don't have to implement the |
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.
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 |
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 |
some initial ideas -
|
Haha, topics are getting bigger! But I have the most fun when we talk about these things. |
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. |
Yes, exactly (though I don't want to fight with Elysia). |
|
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 |
+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)}}}; router.add('GET', '/foo', 'foo')
router.add('GET', '/bar/:id', 'bar + :id')
router.add('ALL', '/baz/baz', 'baz + baz')
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])];
};
Initialization is as fast as And it generates matchers as smartly as By introducing the concept of precompilation, 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)
When precompile is performed, it speeds up the process, perhaps due to optimization. |
Since then, several optimizations have been made, |
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. |
I think we've gone fast spped enough, so we can move on to the next step. 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. |
I would like to hear your opinions. |
Hi @EdamAme-x, Happy New Year! Can you share any scripts to compile that prepared router? |
The JavaScript engine may optimize for Invalid BenchmarkNode: v22.12.0
Bun: v1.1.38
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. |
Happy New Year! (1day late) This is a branch under development. https://github.com/EdamAme-x/hono/tree/feat/prepared-router There is still plenty of room for improvement, but this is the current code. 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: v22.12.0
Bun: v1.1.38
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()); |
I'm creating a new router named "PreparedRouter" using AOT.
This issue is a thread to solicit opinions on concerns.
The text was updated successfully, but these errors were encountered: