Skip to content

perf(headers): precompute per-browser header order sets - #25

Merged
Kikobeats merged 2 commits into
masterfrom
Kikobeats/perf-sort-headers
Oct 27, 2026
Merged

Kikobeats merged 2 commits into
masterfrom
Kikobeats/perf-sort-headers

Conversation

@Kikobeats

@Kikobeats Kikobeats commented Oct 27, 2026 •

Copy link
Copy Markdown
Owner

What changed

sortHeaders (src/headers.js) built new Set(order) from the browser's header-order list, 31 to 43 entries, on every call. getHeader went through Object.entries, which allocates a [key, value] pair for every header just to find user-agent. The fix:

  • The per-browser { keys, set } pairs get built once, when the module loads.
  • getHeader walks Object.keys and returns on the first match.

The loops still use key in headers for the ordered pass and Object.keys for the remaining pass, the same as master. Own and inherited properties, and throwing on null/undefined, behave as before.

A/B (local)

node benchmark/index.js, base = origin/master from git archive, 6 runs per side alternating, median of each run's median, node v24.16.0, darwin arm64 (12 cores), load average 5 to 13 (interleaved).

case base change delta
sortHeaders chrome (60k calls, 17 headers) 90.3ms (88.2-91.7) 50.4ms (49.7-50.8) -44.2%
sortHeaders firefox (60k calls, 16 headers) 79.9ms (78.7-80.9) 42.9ms (42.3-44.6) -46.3%
sortHeaders safari (60k calls, 16 headers) 66.1ms (64.3-66.4) 40.6ms (40.3-42.6) -38.6%
getBrowser top 100 user agents (not touched) 155.5ms 156.4ms +0.6% (noise)
tls options top 100 user agents (not touched) 103.9ms 104.8ms +0.9% (noise)

Behavior changes

None. The test suite passes unchanged. The benchmark's own order assertions pass. A fuzz comparison against master gave identical key order and values in 80,024 of 80,024 checks. It covered:

  • the bench fixtures
  • 20k random header sets with mixed-case, upper-case and numeric keys
  • missing or empty user agents
  • null-prototype objects and objects with inherited properties
  • null and undefined input

How tested

  • npm test: standard-markdown, standard, ava (11 passed).
  • node benchmark/index.js: 0 errors on both sides.

🤖 Generated with Claude Code


Note

Low Risk
Internal performance refactor in header utilities with expanded regression tests; intended behavior is unchanged aside from documented edge-case coverage.

Overview
Speeds up header sorting and lookup by building each browser’s ordered header list and a matching Set once at module load (ORDERS), instead of allocating a new Set on every sortHeaders call.

getHeader now scans Object.keys(headers) for a case-insensitive match instead of iterating Object.entries, avoiding per-header pair allocation.

Tests add an assertHeaders helper (key order + values), broaden coverage for getHeader/sortHeaders (Firefox, unknown browsers, inherited/null-prototype objects, getters, null/undefined), and assert the hook reads a mixed-case User-Agent when it is not the first header.

Reviewed by Cursor Bugbot for commit c98ee70. Bugbot is set up for automated code reviews on this repo. Configure here.

Summary by CodeRabbit

  • Bug Fixes
    • Header lookup now handles case-insensitive names even when the matching header appears after other headers.
    • HTTPS configuration is applied when the User-Agent header uses mixed casing or appears later in the header list.
  • Refactor
    • Browser-specific header ordering now uses a fallback for unrecognized browsers and preserves remaining headers.

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>
@coderabbitai

coderabbitai Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ddb12321-977d-49c9-ba23-502b74cf200f
Commits

Reviewing files that changed from the base of the PR and between 5e99a4f and c98ee70.

Files selected for processing: 2
  • test/headers.js
  • test/hook.js

Included review availability: This review used your included allowance. Your plan provides up to 1 included review per hour; 0 remain after this review.

Walkthrough

The header utilities now precompute browser-specific header orders and membership sets at module load. getHeader scans keys by index. sortHeaders uses cached data to order known headers before appending remaining headers. Tests cover lookup, sorting, edge cases, and hook decoration.

Changes: Header ordering

Layer / File(s) Summary
Cached header lookup and sorting
src/headers.js
The module precomputes header orders and membership sets for each browser. getHeader scans header keys by index, and sortHeaders uses the cached order and set, with empty values as the fallback for unrecognized browsers.
Header behavior tests
test/headers.js, test/hook.js
Tests cover lookup casing and missing values, browser-specific sort order, inherited and duplicate-case headers, getter access, nullish inputs, and hook decoration with a mixed-case User-Agent header.

Priority: Low

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Refactor

Possibly related PRs

  • Kikobeats/https-tls#24: Adds benchmarks for sortHeaders across Chrome, Firefox, and Safari. The benchmarks measure the sorting behavior changed here.
Merge Risk: Minimal · up to c98ee

The header optimization appears mergeable after normal checks; no actionable behavior regression was identified.

Pre-merge checks: Passed 5
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title "perf(headers): precompute per-browser header order sets" matches the main change. The PR builds each browser's header-order keys and Set once at module load in src/headers.js, and the title…
Docstring Coverage Passed No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within survi…
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Title check

Explanation

The title "perf(headers): precompute per-browser header order sets" matches the main change. The PR builds each browser's header-order keys and Set once at module load in src/headers.js, and the title names that change. It also names the performance goal. The PR also changes getHeader to scan keys instead of entries, and adds tests, but those changes are secondary to the main point. The title is specific, short, and follows a conventional commit style.

Full details: Docstring Coverage

Explanation

No eligible functions found in the changed files. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Documentation-only edits and deletions within surviving functions are included; unchanged diff context and fully deleted functions are excluded. Analyzed 0 functions across 3 files.

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https-tls

Details
Benchmark suite Current: c98ee70 Previous: 42f2730 Ratio
sortHeaders chrome 102.9 ms (102.2-108.2) 190.2 ms (188.8-210.1) 0.54
sortHeaders firefox 90.3 ms (89.9-93.4) 170.4 ms (166.6-172.1) 0.53
sortHeaders safari 87.1 ms (86.7-88.1) 141.9 ms (138.0-147.8) 0.61
getBrowser top 100 user agents 328.7 ms (327.6-334.4) 324.8 ms (323.7-326.7) 1.01
tls options top 100 user agents 220.2 ms (218.8-224.0) 215 ms (211.7-221.6) 1.02

This comment was automatically generated by workflow using github-action-benchmark.

@coveralls

coveralls commented Oct 27, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 40589939009

Warning

Build has drifted: This PR's base is out of sync with its target branch, so coverage data may include unrelated changes.
Quick fix: rebase this PR. Learn more →

Warning

No base build found for commit 6d89373 on master.
Coverage changes can't be calculated without a base build.
If a base build is processing, this comment will update automatically when it completes.

Coverage: 98.502%

Details

  • Patch coverage: 16 of 16 lines across 1 file are fully covered (100%).

Uncovered Changes

No uncovered changes found.

Coverage Regressions

Requires a base build to compare against. How to fix this →


Coverage Stats

Coverage Status
Relevant Lines: 236
Covered Lines: 234
Line Coverage: 99.15%
Relevant Branches: 31
Covered Branches: 29
Branch Coverage: 93.55%
Branches in Coverage %: Yes
Coverage Strength: 9.08 hits per line

💛 - Coveralls

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>
@Kikobeats
Kikobeats merged commit 2b55a5e into master Oct 27, 2026
4 checks passed
@Kikobeats
Kikobeats deleted the Kikobeats/perf-sort-headers branch October 27, 2026 21:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants