Skip to content

Commit 33c8b0f

Browse files
authored
Performance and tests improvements (#8)
* refactor: performance optimizations * refactor test coverage
1 parent 081d94e commit 33c8b0f

File tree

18 files changed

+2705
-238
lines changed

18 files changed

+2705
-238
lines changed

.vscode/settings.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}

bench.ts

Lines changed: 30 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,53 +1,53 @@
1-
import { run, bench, group } from "mitata"
2-
import httpNext from "./index"
3-
import httpPrevious from "0http-bun"
1+
import {run, bench, group} from 'mitata'
2+
import httpNext from './index'
3+
import httpPrevious from '0http-bun'
44

55
function setupRouter(router) {
66
router.use((req, next) => {
77
return next()
88
})
99

10-
router.get("/", () => {
10+
router.get('/', () => {
1111
return new Response()
1212
})
13-
router.get("/:id", async (req) => {
13+
router.get('/:id', async (req) => {
1414
return new Response(req.params.id)
1515
})
16-
router.get("/:id/error", () => {
17-
throw new Error("Error")
16+
router.get('/:id/error', () => {
17+
throw new Error('Error')
1818
})
1919
}
2020

21-
const { router } = httpNext()
21+
const {router} = httpNext()
2222
setupRouter(router)
2323

24-
const { router: routerPrevious } = httpPrevious()
24+
const {router: routerPrevious} = httpPrevious()
2525
setupRouter(routerPrevious)
2626

27-
group("Next Router", () => {
28-
bench("Parameter URL", () => {
29-
router.fetch(new Request(new URL("http://localhost/0")))
30-
}).gc("inner")
31-
bench("Not Found URL", () => {
32-
router.fetch(new Request(new URL("http://localhost/0/404")))
33-
}).gc("inner")
34-
bench("Error URL", () => {
35-
router.fetch(new Request(new URL("http://localhost/0/error")))
36-
}).gc("inner")
27+
group('Next Router', () => {
28+
bench('Parameter URL', () => {
29+
router.fetch(new Request(new URL('http://localhost/0')))
30+
}).gc('inner')
31+
bench('Not Found URL', () => {
32+
router.fetch(new Request(new URL('http://localhost/0/404')))
33+
}).gc('inner')
34+
bench('Error URL', () => {
35+
router.fetch(new Request(new URL('http://localhost/0/error')))
36+
}).gc('inner')
3737
})
3838

39-
group("Previous Router", () => {
40-
bench("Parameter URL", () => {
41-
routerPrevious.fetch(new Request(new URL("http://localhost/0")))
42-
}).gc("inner")
43-
bench("Not Found URL", () => {
44-
routerPrevious.fetch(new Request(new URL("http://localhost/0/404")))
45-
}).gc("inner")
46-
bench("Error URL", () => {
47-
routerPrevious.fetch(new Request(new URL("http://localhost/0/error")))
48-
}).gc("inner")
39+
group('Previous Router', () => {
40+
bench('Parameter URL', () => {
41+
routerPrevious.fetch(new Request(new URL('http://localhost/0')))
42+
}).gc('inner')
43+
bench('Not Found URL', () => {
44+
routerPrevious.fetch(new Request(new URL('http://localhost/0/404')))
45+
}).gc('inner')
46+
bench('Error URL', () => {
47+
routerPrevious.fetch(new Request(new URL('http://localhost/0/error')))
48+
}).gc('inner')
4949
})
5050

51-
await run({
51+
run({
5252
colors: true,
5353
})

common.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Pattern, Methods } from 'trouter'
1+
import {Pattern, Methods} from 'trouter'
22

33
export interface IRouterConfig {
44
defaultRoute?: RequestHandler
@@ -16,7 +16,7 @@ type ZeroRequest = Request & {
1616

1717
export type RequestHandler = (
1818
req: ZeroRequest,
19-
next: StepFunction
19+
next: StepFunction,
2020
) => Response | Promise<Response>
2121

2222
export interface IRouter {

index.d.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { IRouter, IRouterConfig } from './common'
1+
import {IRouter, IRouterConfig} from './common'
22

33
export default function zero(config?: IRouterConfig): {
44
router: IRouter
55
}
66

7-
export * from './common'
7+
export * from './common'

lib/next.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,21 @@ module.exports = function next(
55
defaultRoute,
66
errorHandler,
77
) {
8-
if (index >= middlewares.length) {
8+
// Optimized loop unrolling for common cases
9+
const length = middlewares.length
10+
if (index >= length) {
911
return defaultRoute(req)
1012
}
1113

12-
const middleware = middlewares[index++]
14+
const middleware = middlewares[index]
15+
const nextIndex = index + 1
1316

1417
try {
1518
return middleware(req, (err) => {
1619
if (err) {
1720
return errorHandler(err, req)
1821
}
19-
return next(middlewares, req, index, defaultRoute, errorHandler)
22+
return next(middlewares, req, nextIndex, defaultRoute, errorHandler)
2023
})
2124
} catch (err) {
2225
return errorHandler(err, req)

lib/router/sequential.d.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
1-
import { IRouter, IRouterConfig } from './../../index'
1+
import {IRouter, IRouterConfig} from './../../index'
22

33
export default function createSequentialRouter(config?: IRouterConfig): IRouter

lib/router/sequential.js

Lines changed: 83 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
const { Trouter } = require("trouter")
2-
const qs = require("fast-querystring")
3-
const next = require("./../next")
1+
const {Trouter} = require('trouter')
2+
const qs = require('fast-querystring')
3+
const next = require('./../next')
44

55
const STATUS_404 = {
66
status: 404,
@@ -12,26 +12,26 @@ const STATUS_500 = {
1212
module.exports = (config = {}) => {
1313
const cache = new Map()
1414

15-
if (!config.defaultRoute) {
16-
config.defaultRoute = () => {
17-
return new Response(null, STATUS_404)
18-
}
19-
}
20-
if (!config.errorHandler) {
21-
config.errorHandler = (err) => {
22-
return new Response(err.message, STATUS_500)
23-
}
24-
}
15+
// Pre-create default responses to avoid object creation overhead
16+
const default404Response = new Response(null, STATUS_404)
17+
18+
// Cache default functions to avoid closure creation
19+
const defaultRouteHandler = config.defaultRoute || (() => default404Response)
20+
const errorHandlerFn =
21+
config.errorHandler || ((err) => new Response(err.message, STATUS_500))
22+
23+
// Optimize empty params object reuse
24+
const emptyParams = {}
2525

2626
const router = new Trouter()
2727
router.port = config.port || 3000
2828

2929
const _use = router.use
3030

3131
router.use = (prefix, ...middlewares) => {
32-
if (typeof prefix === "function") {
32+
if (typeof prefix === 'function') {
3333
middlewares = [prefix, ...middlewares]
34-
prefix = "/"
34+
prefix = '/'
3535
}
3636
_use.call(router, prefix, middlewares)
3737

@@ -40,40 +40,84 @@ module.exports = (config = {}) => {
4040

4141
router.fetch = (req) => {
4242
const url = req.url
43-
const startIndex = url.indexOf("/", 11)
44-
const queryIndex = url.indexOf("?", startIndex + 1)
45-
const path =
46-
queryIndex === -1
47-
? url.substring(startIndex)
48-
: url.substring(startIndex, queryIndex)
49-
50-
req.path = path || "/"
51-
req.query = queryIndex > 0 ? qs.parse(url.substring(queryIndex + 1)) : {}
52-
53-
const cacheKey = `${req.method}:${req.path}`
54-
let match = null
55-
if (cache.has(cacheKey)) {
56-
match = cache.get(cacheKey)
43+
44+
// Highly optimized URL parsing - single pass through the string
45+
let pathStart = 0
46+
let pathEnd = url.length
47+
let queryString = null
48+
49+
// Find protocol end
50+
const protocolEnd = url.indexOf('://')
51+
if (protocolEnd !== -1) {
52+
// Find host end (start of path)
53+
pathStart = url.indexOf('/', protocolEnd + 3)
54+
if (pathStart === -1) {
55+
pathStart = url.length
56+
}
57+
}
58+
59+
// Find query start
60+
const queryStart = url.indexOf('?', pathStart)
61+
if (queryStart !== -1) {
62+
pathEnd = queryStart
63+
queryString = url.substring(queryStart + 1)
64+
}
65+
66+
const path = pathStart < pathEnd ? url.substring(pathStart, pathEnd) : '/'
67+
68+
req.path = path
69+
req.query = queryString ? qs.parse(queryString) : {}
70+
71+
// Optimized cache lookup with method-based Map structure
72+
const method = req.method
73+
let methodCache = cache.get(method)
74+
let match_result
75+
76+
if (methodCache) {
77+
match_result = methodCache.get(path)
78+
if (match_result === undefined) {
79+
match_result = router.find(method, path)
80+
methodCache.set(path, match_result)
81+
}
5782
} else {
58-
match = router.find(req.method, req.path)
59-
cache.set(cacheKey, match)
83+
match_result = router.find(method, path)
84+
methodCache = new Map([[path, match_result]])
85+
cache.set(method, methodCache)
6086
}
6187

62-
if (match?.handlers?.length > 0) {
63-
if (!req.params) {
64-
req.params = {}
88+
if (match_result?.handlers?.length > 0) {
89+
// Fast path for params assignment
90+
const params = match_result.params
91+
if (params) {
92+
// Check if params object has properties without Object.keys()
93+
let hasParams = false
94+
for (const key in params) {
95+
hasParams = true
96+
break
97+
}
98+
99+
if (hasParams) {
100+
req.params = req.params || {}
101+
// Direct property copy - faster than Object.keys() + loop
102+
for (const key in params) {
103+
req.params[key] = params[key]
104+
}
105+
} else if (!req.params) {
106+
req.params = emptyParams
107+
}
108+
} else if (!req.params) {
109+
req.params = emptyParams
65110
}
66-
Object.assign(req.params, match.params)
67111

68112
return next(
69-
match.handlers,
113+
match_result.handlers,
70114
req,
71115
0,
72-
config.defaultRoute,
73-
config.errorHandler
116+
defaultRouteHandler,
117+
errorHandlerFn,
74118
)
75119
} else {
76-
return config.defaultRoute(req)
120+
return defaultRouteHandler(req)
77121
}
78122
}
79123

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"main": "index.js",
66
"scripts": {
77
"lint": "prettier --check **/*.js",
8-
"test": "bun test",
8+
"test": "bun --coverage test",
99
"bench": "bun run bench.js",
1010
"format": "prettier --write **/*.js"
1111
},

test/config.test.js

Lines changed: 0 additions & 48 deletions
This file was deleted.

0 commit comments

Comments
 (0)