Skip to content

Commit 2b55a5e

Browse files
Kikobeatsclaudecursoragent
authored
perf(headers): precompute per-browser header order sets (#25)
* perf(headers): precompute per-browser header order sets sortHeaders built `new Set(order)` from the browser's header order on every call, and getHeader allocated an `[key, value]` pair per header through Object.entries. The order Sets now get built once when the module loads, and getHeader walks Object.keys and returns on the first match. Output is unchanged: same key order and values as master on 80,024 checks (bench fixtures, 20k random header sets with mixed-case and numeric keys, missing or empty user agents, null-prototype and inherited-property objects, null and undefined input). Local A/B, node v24.16.0 arm64, 6 alternating runs per side, median of medians: | case | base | change | delta | | ----------------------- | ------ | ------ | ------ | | sortHeaders chrome | 90.3ms | 50.4ms | -44.2% | | sortHeaders firefox | 79.9ms | 42.9ms | -46.3% | | sortHeaders safari | 66.1ms | 40.6ms | -38.6% | | getBrowser (unchanged) | 155.5ms | 156.4ms | +0.6% | | tls options (unchanged) | 103.9ms | 104.8ms | +0.9% | Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(headers): lock order and the lookup branches ava's deepEqual ignores key order, so the sort tests stayed green when sorting was replaced with a copy. Assert key order, and cover the no-browser fallback, inherited keys, and the hook's header lookup. Co-authored-by: Cursor <cursoragent@cursor.com> --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 0007bab commit 2b55a5e

3 files changed

Lines changed: 159 additions & 30 deletions

File tree

‎src/headers.js‎

Lines changed: 16 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,33 @@ const getBrowser = require('./browser')
44

55
const HEADERS_ORDER = require('./headers-order.json')
66

7-
const getHeader = (key, headers) => {
8-
let result
7+
const NO_ORDER = { keys: [], set: new Set() }
98

10-
for (const [header, value] of Object.entries(headers)) {
11-
if (header.toLowerCase() === key) {
12-
result = value
13-
break
14-
}
15-
}
9+
const ORDERS = Object.fromEntries(
10+
Object.entries(HEADERS_ORDER).map(([browser, keys]) => [browser, { keys, set: new Set(keys) }])
11+
)
1612

17-
return result
13+
const getHeader = (key, headers) => {
14+
const headerKeys = Object.keys(headers)
15+
for (let i = 0; i < headerKeys.length; i++) {
16+
if (headerKeys[i].toLowerCase() === key) return headers[headerKeys[i]]
17+
}
1818
}
1919

2020
const sortHeaders = headers => {
2121
const userAgent = getHeader('user-agent', headers)
22-
const browser = getBrowser(userAgent)
23-
const order = HEADERS_ORDER[browser] || []
22+
const { keys, set } = ORDERS[getBrowser(userAgent)] || NO_ORDER
2423
const orderedHeaders = {}
2524

26-
// Pre-calculate order as a Set for O(1) lookup
27-
const orderSet = new Set(order)
28-
const headerKeys = Object.keys(headers)
29-
const headersLength = headerKeys.length
30-
const orderLength = order.length
31-
let i = 0
32-
let key
33-
34-
// Add ordered headers
35-
for (; i < orderLength; i++) {
36-
key = order[i]
25+
for (let i = 0; i < keys.length; i++) {
26+
const key = keys[i]
3727
if (key in headers) orderedHeaders[key] = headers[key]
3828
}
3929

40-
// Add remaining headers
41-
for (i = 0; i < headersLength; i++) {
42-
key = headerKeys[i]
43-
if (!orderSet.has(key)) orderedHeaders[key] = headers[key]
30+
const headerKeys = Object.keys(headers)
31+
for (let i = 0; i < headerKeys.length; i++) {
32+
const key = headerKeys[i]
33+
if (!set.has(key)) orderedHeaders[key] = headers[key]
4434
}
4535

4636
return orderedHeaders

‎test/headers.js‎

Lines changed: 131 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,32 @@ const { sortHeaders, getHeader } = require('..')
66

77
const { UA } = require('./helpers')
88

9+
const assertHeaders = (t, actual, expected) => {
10+
t.deepEqual(Object.keys(actual), Object.keys(expected))
11+
t.deepEqual(actual, expected)
12+
}
13+
914
test('.getHeader', t => {
1015
t.is(getHeader('user-agent', { 'user-agent': 'googlebot' }), 'googlebot')
1116
t.is(getHeader('user-agent', { 'User-Agent': 'googlebot' }), 'googlebot')
17+
t.is(
18+
getHeader('user-agent', {
19+
accept: 'a',
20+
'User-Agent': 'first',
21+
'user-agent': 'second'
22+
}),
23+
'first'
24+
)
25+
t.is(getHeader('user-agent', { accept: 'a', host: 'h' }), undefined)
26+
t.is(getHeader('User-Agent', { 'user-agent': 'googlebot' }), undefined)
27+
t.is(getHeader('user-agent', { accept: 'a', 'user-agent': '' }), '')
28+
t.throws(() => getHeader('user-agent', null), { instanceOf: TypeError })
29+
t.throws(() => getHeader('user-agent', undefined), { instanceOf: TypeError })
1230
})
1331

1432
test('.sortHeaders', t => {
15-
t.deepEqual(
33+
assertHeaders(
34+
t,
1635
sortHeaders({
1736
'x-random': 'foo',
1837
'user-agent': UA.CHROME,
@@ -23,24 +42,132 @@ test('.sortHeaders', t => {
2342
{
2443
Host: 'foo',
2544
'user-agent': UA.CHROME,
26-
referer: 'bar',
2745
accept: 'foo',
46+
referer: 'bar',
2847
'x-random': 'foo'
2948
}
3049
)
3150

32-
t.deepEqual(
51+
assertHeaders(
52+
t,
3353
sortHeaders({
3454
'x-random': 'foo',
3555
'user-agent': 'safari',
3656
referer: 'bar',
3757
accept: 'foo'
3858
}),
3959
{
40-
accept: 'foo',
4160
referer: 'bar',
61+
accept: 'foo',
4262
'user-agent': 'safari',
4363
'x-random': 'foo'
4464
}
4565
)
4666
})
67+
68+
test('sortHeaders orders firefox headers', t => {
69+
assertHeaders(
70+
t,
71+
sortHeaders({
72+
'x-random': 'foo',
73+
'User-Agent': UA.FIREFOX,
74+
accept: 'foo',
75+
Host: 'foo'
76+
}),
77+
{
78+
Host: 'foo',
79+
'User-Agent': UA.FIREFOX,
80+
accept: 'foo',
81+
'x-random': 'foo'
82+
}
83+
)
84+
})
85+
86+
test('sortHeaders keeps insertion order when no browser matches', t => {
87+
const headers = { 'z-last': '1', accept: 'a', Host: 'h' }
88+
const sorted = sortHeaders(headers)
89+
assertHeaders(t, sorted, { 'z-last': '1', accept: 'a', Host: 'h' })
90+
t.not(sorted, headers)
91+
92+
assertHeaders(t, sortHeaders({ 'user-agent': '', accept: 'a', host: 'h' }), {
93+
'user-agent': '',
94+
accept: 'a',
95+
host: 'h'
96+
})
97+
assertHeaders(t, sortHeaders({}), {})
98+
})
99+
100+
test('sortHeaders ignores an inherited user-agent', t => {
101+
const headers = Object.create({ 'user-agent': UA.CHROME, accept: 'inherited' })
102+
headers['x-own'] = 'yes'
103+
104+
assertHeaders(t, sortHeaders(headers), { 'x-own': 'yes' })
105+
})
106+
107+
test('sortHeaders copies inherited ordered headers and skips inherited extras', t => {
108+
const headers = Object.create({ accept: 'inherited', 'x-inherited': 'no' })
109+
headers['user-agent'] = UA.CHROME
110+
headers['x-own'] = 'yes'
111+
112+
assertHeaders(t, sortHeaders(headers), {
113+
'user-agent': UA.CHROME,
114+
accept: 'inherited',
115+
'x-own': 'yes'
116+
})
117+
})
118+
119+
test('sortHeaders keeps a casing that is not in the browser order', t => {
120+
assertHeaders(
121+
t,
122+
sortHeaders({
123+
host: 'lower',
124+
'user-agent': UA.CHROME,
125+
Host: 'upper'
126+
}),
127+
{
128+
Host: 'upper',
129+
'user-agent': UA.CHROME,
130+
host: 'lower'
131+
}
132+
)
133+
})
134+
135+
test('sortHeaders reads an ordered header once', t => {
136+
let reads = 0
137+
const headers = { 'user-agent': UA.CHROME }
138+
Object.defineProperty(headers, 'accept', {
139+
enumerable: true,
140+
get () {
141+
reads += 1
142+
return 'foo'
143+
}
144+
})
145+
headers['x-random'] = 'z'
146+
147+
assertHeaders(t, sortHeaders(headers), {
148+
'user-agent': UA.CHROME,
149+
accept: 'foo',
150+
'x-random': 'z'
151+
})
152+
t.is(reads, 1)
153+
})
154+
155+
test('sortHeaders orders a null-prototype map', t => {
156+
const headers = Object.create(null)
157+
headers.Host = 'h'
158+
headers['x-random'] = 'z'
159+
headers['user-agent'] = UA.CHROME
160+
161+
const sorted = sortHeaders(headers)
162+
assertHeaders(t, sorted, {
163+
Host: 'h',
164+
'user-agent': UA.CHROME,
165+
'x-random': 'z'
166+
})
167+
t.not(sorted, headers)
168+
})
169+
170+
test('sortHeaders throws on null and undefined', t => {
171+
t.throws(() => sortHeaders(null), { instanceOf: TypeError })
172+
t.throws(() => sortHeaders(undefined), { instanceOf: TypeError })
173+
})

‎test/hook.js‎

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ const userAgents = require('top-user-agents')
55
const test = require('ava').default
66

77
const decorate = require('../hook')
8+
const { UA } = require('./helpers')
89

910
const randomUserAgent = uniqueRandomArray(userAgents)
1011

@@ -29,3 +30,14 @@ test('does nothing if user agent is not present', t => {
2930

3031
t.deepEqual(Object.keys(options.https), [])
3132
})
33+
34+
test('reads a mixed-case user-agent that is not the first header', t => {
35+
const options = {
36+
https: {},
37+
headers: { accept: 'text/html', 'User-Agent': UA.CHROME }
38+
}
39+
40+
decorate(options)
41+
42+
t.is(options.https.minVersion, 'TLSv1')
43+
})

0 commit comments

Comments
 (0)