Skip to content

Commit 5515aa0

Browse files
perf: lazily materialize request headers (#389)
* perf: lazily materialize request headers * refactor(request): keep lazy headers internal * fix(request): resolve lazy headers from raw pairs * bench: isolate request header access * update benchmark --------- Co-authored-by: Taku Amano <taku@taaas.jp>
1 parent 82ba34e commit 5515aa0

14 files changed

Lines changed: 638 additions & 71 deletions

File tree

‎benchmarks/fetch/README.md‎

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Benchmark
22

3-
Benchmark to compare performance between the published npm version and local development version of @hono/node-server.
3+
Benchmark comparing the published npm version and local development version of @hono/node-server with srvx.
44

55
This benchmark uses a basic Fetch API-based application without the Hono framework to measure the raw performance of @hono/node-server's adapter.
66

@@ -19,28 +19,42 @@ pnpm run benchmark
1919

2020
## What's Being Tested
2121

22-
Tests three endpoints:
22+
Tests four endpoints:
2323

2424
1. **Ping (GET /)**: Simple response
2525
2. **Query (GET /id/:id)**: Path parameter and query parameter handling
2626
3. **Body (POST /json)**: JSON body processing
27+
4. **Headers (GET /headers)**: Isolated `request.headers.get()` access
2728

2829
Each endpoint is tested with 500 concurrent connections for 10 seconds, measuring requests per second (Reqs/sec).
2930

31+
## Benchmark Environment
32+
33+
- **Machine**: Lenovo LOQ 15IRX9 (83DV)
34+
- **CPU**: Intel Core i5-13450HX (10 cores, 16 threads)
35+
- **Memory**: 24 GB
36+
- **OS**: Arch Linux x86_64 (kernel 7.1.6)
37+
- **Node.js**: 24.19.0
38+
3039
## Understanding Results
3140

41+
Last updated: 2026-08-09
42+
3243
```
33-
| Benchmark | npm | dev | Difference |
34-
| ----------------- | -------------- | -------------- | ----------- |
35-
| Average | 111,514.97 | 115,234.56 | +3.34% |
36-
| Ping (GET /) | 122,207.70 | 125,678.90 | +2.84% |
37-
| Query (GET /id) | 106,624.16 | 110,123.45 | +3.28% |
38-
| Body (POST /json) | 105,713.04 | 109,901.23 | +3.96% |
44+
| Benchmark | @hono/node-server (2.1.0) | srvx (0.12.5, fast) | @hono/node-server (dev) | dev vs npm | dev vs srvx |
45+
| ----------------- | ------------------------- | ------------------- | ----------------------- | ---------- | ----------- |
46+
| Average | 83,588.79 | 89,245.89 | 88,398.73 | +5.75% | -0.95% |
47+
| Ping (GET /) | 87,502.45 | 97,875.62 | 96,320.69 | +10.08% | -1.59% |
48+
| Query (GET /id) | 92,967.16 | 89,524.22 | 93,474.95 | +0.55% | +4.41% |
49+
| Body (POST /json) | 72,621.78 | 75,968.80 | 73,823.52 | +1.65% | -2.82% |
50+
| Headers (GET) | 81,263.78 | 93,614.90 | 89,975.77 | +10.72% | -3.89% |
3951
```
4052

41-
- **npm**: Published npm version (`@hono/node-server`)
42-
- **dev**: Local development version (from repository root `dist/`)
43-
- **Difference**: Performance difference (positive values indicate improvement, negative values indicate regression)
53+
- **@hono/node-server (2.1.0)**: Published npm version
54+
- **@hono/node-server (dev)**: Local development version (from repository root `dist/`)
55+
- **srvx (0.12.5, fast)**: Published npm version using its opt-in `FastResponse`
56+
- **dev vs npm**: Development Hono compared with published Hono
57+
- **dev vs srvx**: Development Hono compared with srvx
4458

4559
## Reference
4660

‎benchmarks/fetch/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
"benchmark": "node --experimental-strip-types scripts/bench.ts"
77
},
88
"dependencies": {
9-
"@hono/node-server": "^1.19.9",
10-
"@hono/node-server-dev": "file:../.."
9+
"@hono/node-server": "^2.1.0",
10+
"srvx": "^0.12.5"
1111
}
1212
}

‎benchmarks/fetch/scripts/bench.ts‎

Lines changed: 56 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { spawn } from 'node:child_process'
2+
import { once } from 'node:events'
23
import { setTimeout } from 'node:timers/promises'
34

45
const PORT = 3000
@@ -16,6 +17,7 @@ interface ServerResult {
1617
ping: number
1718
query: number
1819
body: number
20+
headers: number
1921
}
2022

2123
async function waitForServer(): Promise<void> {
@@ -44,6 +46,15 @@ async function retryFetch(url: string, options?: RequestInit, retries = 0): Prom
4446
}
4547
}
4648

49+
async function stopServer(server: ReturnType<typeof spawn>): Promise<void> {
50+
if (server.exitCode !== null || server.signalCode !== null) {
51+
return
52+
}
53+
const exited = once(server, 'exit')
54+
server.kill('SIGKILL')
55+
await exited
56+
}
57+
4758
async function testEndpoints(): Promise<void> {
4859
// Test GET /
4960
const res1 = await retryFetch('http://127.0.0.1:3000/')
@@ -75,6 +86,17 @@ async function testEndpoints(): Promise<void> {
7586
`Body: Result not match - expected ${JSON.stringify(body)}, got ${JSON.stringify(json3)}`
7687
)
7788
}
89+
90+
// Test an isolated incoming request-header read.
91+
const res4 = await retryFetch('http://127.0.0.1:3000/headers', {
92+
headers: {
93+
'x-test': '123',
94+
},
95+
})
96+
const text4 = await res4.text()
97+
if (res4.status !== 200 || text4 !== '123') {
98+
throw new Error(`Headers: Result not match - expected "123", got "${text4}"`)
99+
}
78100
}
79101

80102
async function runBenchmarkForServer(
@@ -102,6 +124,7 @@ async function runBenchmarkForServer(
102124
{ name: 'GET /', url: 'http://127.0.0.1:3000/' },
103125
{ name: 'GET /id/:id', url: 'http://127.0.0.1:3000/id/1?name=bun' },
104126
{ name: 'POST /json', url: 'http://127.0.0.1:3000/json', method: 'POST' },
127+
{ name: 'GET /headers', url: 'http://127.0.0.1:3000/headers' },
105128
]
106129

107130
const results: BenchmarkResult[] = []
@@ -111,6 +134,9 @@ async function runBenchmarkForServer(
111134
if (bench.method === 'POST') {
112135
args.push('-m', 'POST', '-H', 'Content-Type:application/json', '-f', './scripts/body.json')
113136
}
137+
if (bench.name === 'GET /headers') {
138+
args.push('-H', 'x-test:123')
139+
}
114140
args.push(bench.url)
115141

116142
const output = await new Promise<string>((resolve, reject) => {
@@ -147,7 +173,8 @@ async function runBenchmarkForServer(
147173
const ping = results[0]?.reqsPerSec || 0
148174
const query = results[1]?.reqsPerSec || 0
149175
const body = results[2]?.reqsPerSec || 0
150-
const average = (ping + query + body) / 3
176+
const headers = results[3]?.reqsPerSec || 0
177+
const average = (ping + query + body + headers) / 4
151178

152179
return {
153180
server: serverName,
@@ -156,14 +183,14 @@ async function runBenchmarkForServer(
156183
ping,
157184
query,
158185
body,
186+
headers,
159187
}
160188
} catch (error) {
161189
console.error('Error:', (error as Error).message)
162190
throw error
163191
} finally {
164192
console.log('Stopping server...')
165-
server.kill()
166-
await setTimeout(1000)
193+
await stopServer(server)
167194
}
168195
}
169196

@@ -185,14 +212,14 @@ async function testServer(serverFile: string, serverName: string): Promise<boole
185212
console.log(' ', (error as Error)?.message || error)
186213
return false
187214
} finally {
188-
server.kill()
189-
await setTimeout(1000)
215+
await stopServer(server)
190216
}
191217
}
192218

193219
async function main(): Promise<void> {
194220
const servers = [
195-
{ file: 'src/server-npm.js', name: '@hono/node-server (npm)' },
221+
{ file: 'src/server-npm.js', name: '@hono/node-server (2.1.0)' },
222+
{ file: 'src/server-srvx.js', name: 'srvx (0.12.5, fast)' },
196223
{ file: 'src/server-dev.js', name: '@hono/node-server (dev)' },
197224
]
198225

@@ -238,47 +265,53 @@ async function main(): Promise<void> {
238265
})
239266
}
240267

241-
const formatDiff = (npm: number, dev: number): string => {
242-
const diff = ((dev - npm) / npm) * 100
243-
const sign = diff > 0 ? '+' : ''
244-
return `${sign}${diff.toFixed(2)}%`
268+
const formatDiff = (baseline: number, dev: number): string => {
269+
const diff = ((dev - baseline) / baseline) * 100
270+
return `${diff > 0 ? '+' : ''}${diff.toFixed(2)}%`
245271
}
246272

247-
if (allResults.length === 2) {
248-
// Comparison mode: npm vs dev
249-
const npmResult = allResults.find((r) => r.server.includes('npm'))
273+
if (allResults.length === 3) {
274+
const npmResult = allResults.find((r) => r.server === '@hono/node-server (2.1.0)')
275+
const srvxResult = allResults.find((r) => r.server === 'srvx (0.12.5, fast)')
250276
const devResult = allResults.find((r) => r.server.includes('dev'))
251277

252-
if (npmResult && devResult) {
253-
console.log('| Benchmark | npm | dev | Difference |')
254-
console.log('| ----------------- | -------------- | -------------- | ----------- |')
278+
if (npmResult && srvxResult && devResult) {
279+
console.log(
280+
'| Benchmark | @hono/node-server (2.1.0) | srvx (0.12.5, fast) | @hono/node-server (dev) | dev vs npm | dev vs srvx |'
281+
)
282+
console.log(
283+
'| ----------------- | ------------------------- | ------------------- | ----------------------- | ---------- | ----------- |'
284+
)
285+
console.log(
286+
`| Average | ${formatNumber(npmResult.average).padEnd(25)} | ${formatNumber(srvxResult.average).padEnd(19)} | ${formatNumber(devResult.average).padEnd(23)} | ${formatDiff(npmResult.average, devResult.average).padEnd(10)} | ${formatDiff(srvxResult.average, devResult.average).padEnd(11)} |`
287+
)
255288
console.log(
256-
`| Average | ${formatNumber(npmResult.average).padEnd(14)} | ${formatNumber(devResult.average).padEnd(14)} | ${formatDiff(npmResult.average, devResult.average).padEnd(11)} |`
289+
`| Ping (GET /) | ${formatNumber(npmResult.ping).padEnd(25)} | ${formatNumber(srvxResult.ping).padEnd(19)} | ${formatNumber(devResult.ping).padEnd(23)} | ${formatDiff(npmResult.ping, devResult.ping).padEnd(10)} | ${formatDiff(srvxResult.ping, devResult.ping).padEnd(11)} |`
257290
)
258291
console.log(
259-
`| Ping (GET /) | ${formatNumber(npmResult.ping).padEnd(14)} | ${formatNumber(devResult.ping).padEnd(14)} | ${formatDiff(npmResult.ping, devResult.ping).padEnd(11)} |`
292+
`| Query (GET /id) | ${formatNumber(npmResult.query).padEnd(25)} | ${formatNumber(srvxResult.query).padEnd(19)} | ${formatNumber(devResult.query).padEnd(23)} | ${formatDiff(npmResult.query, devResult.query).padEnd(10)} | ${formatDiff(srvxResult.query, devResult.query).padEnd(11)} |`
260293
)
261294
console.log(
262-
`| Query (GET /id) | ${formatNumber(npmResult.query).padEnd(14)} | ${formatNumber(devResult.query).padEnd(14)} | ${formatDiff(npmResult.query, devResult.query).padEnd(11)} |`
295+
`| Body (POST /json) | ${formatNumber(npmResult.body).padEnd(25)} | ${formatNumber(srvxResult.body).padEnd(19)} | ${formatNumber(devResult.body).padEnd(23)} | ${formatDiff(npmResult.body, devResult.body).padEnd(10)} | ${formatDiff(srvxResult.body, devResult.body).padEnd(11)} |`
263296
)
264297
console.log(
265-
`| Body (POST /json) | ${formatNumber(npmResult.body).padEnd(14)} | ${formatNumber(devResult.body).padEnd(14)} | ${formatDiff(npmResult.body, devResult.body).padEnd(11)} |`
298+
`| Headers (GET) | ${formatNumber(npmResult.headers).padEnd(25)} | ${formatNumber(srvxResult.headers).padEnd(19)} | ${formatNumber(devResult.headers).padEnd(23)} | ${formatDiff(npmResult.headers, devResult.headers).padEnd(10)} | ${formatDiff(srvxResult.headers, devResult.headers).padEnd(11)} |`
266299
)
267300
}
268301
} else {
269302
// Fallback: original table format
270303
console.log(
271-
'| Server | Runtime | Average | Ping | Query | Body |'
304+
'| Server | Runtime | Average | Ping | Query | Body | Headers |'
272305
)
273306
console.log(
274-
'| -------------------------- | ------- | ------------ | ------------ | ------------ | ------------ |'
307+
'| -------------------------- | ------- | ------------ | ------------ | ------------ | ------------ | ------------ |'
275308
)
276309

277310
const sortedResults = allResults.sort((a, b) => b.average - a.average)
278311

279312
for (const result of sortedResults) {
280313
console.log(
281-
`| ${result.server.padEnd(26)} | ${result.runtime.padEnd(7)} | ${formatNumber(result.average).padEnd(12)} | ${formatNumber(result.ping).padEnd(12)} | ${formatNumber(result.query).padEnd(12)} | ${formatNumber(result.body).padEnd(12)} |`
314+
`| ${result.server.padEnd(26)} | ${result.runtime.padEnd(7)} | ${formatNumber(result.average).padEnd(12)} | ${formatNumber(result.ping).padEnd(12)} | ${formatNumber(result.query).padEnd(12)} | ${formatNumber(result.body).padEnd(12)} | ${formatNumber(result.headers).padEnd(12)} |`
282315
)
283316
}
284317
}

‎benchmarks/fetch/src/app.js‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ export default {
99
switch (url.pathname) {
1010
case '/':
1111
return new Response('Hi')
12+
case '/headers':
13+
return new Response(request.headers.get('x-test'))
1214
}
1315

1416
if (url.pathname.startsWith('/id/')) {

‎benchmarks/fetch/src/server-dev.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { serve } from '@hono/node-server-dev'
1+
import { serve } from '../../../dist/index.mjs'
22
import app from './app.js'
33

44
const port = 3000
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { FastResponse, serve } from 'srvx'
2+
import app from './app.js'
3+
4+
const port = 3000
5+
6+
// opt into srvx fast response, since hono uses request/response shims by default
7+
globalThis.Response = FastResponse
8+
9+
serve({
10+
fetch: app.fetch,
11+
port,
12+
})

‎pnpm-lock.yaml‎

Lines changed: 15 additions & 18 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)