Skip to content

PoC: perf(context): defer method binding in Context to reduce init cost#4755

Draft
usualoma wants to merge 1 commit intohonojs:mainfrom
usualoma:refactor-context
Draft

PoC: perf(context): defer method binding in Context to reduce init cost#4755
usualoma wants to merge 1 commit intohonojs:mainfrom
usualoma:refactor-context

Conversation

@usualoma
Copy link
Copy Markdown
Member

What is this?

This PR reduces Context initialization overhead by deferring method-function creation from instance field initializers to getters.

There is no intended behavioral change; this is a performance-focused refactor targeting lower new Context(req) cost.

A more radical approach

In Hono, we can now call methods by destructuring them.

https://github.com/orgs/honojs/discussions/812

If we remove support for calls via destructuring, we could optimize it a bit more.

main...usualoma:refactor-context-2

Benchmark

Benchmarks were taken in the following three patterns

  • main
  • refactor-context : This PR's branch
  • refactor-context-2 : A more radical approach

These are the execution results for new Context(req) and new Context(req).text('hello').
(Since .text() or .json() calls typically occur at most once per request, this benchmark reflects real-world conditions)

While this isn't the application's bottleneck, the localized benchmark shows new Context(req) is over 20 times faster.

Benchmark script
import { resolve, toFileUrl } from 'jsr:@std/path'
import { bench, group, run } from 'npm:mitata'

type ContextCtor = new (req: Request, options?: unknown) => unknown

const parseArg = (name: string): string | undefined => {
  const prefix = `--${name}=`
  const hit = Deno.args.find((arg) => arg.startsWith(prefix))
  return hit ? hit.slice(prefix.length) : undefined
}

const loadContextCtor = async (root: string): Promise<ContextCtor> => {
  const contextPath = resolve(root, 'src/context.ts')
  try {
    await Deno.stat(contextPath)
  } catch {
    console.error(`Context source was not found: ${contextPath}`)
    console.error(
      'If --main-root points to a worktree path, create it first (e.g. `git worktree add /tmp/hono-main origin/main`).'
    )
    Deno.exit(1)
  }

  const modPath = toFileUrl(contextPath).href
  const mod = (await import(modPath)) as { Context: ContextCtor }
  return mod.Context
}

const mainRoot = parseArg('main-root')
const refactorRoot = parseArg('refactor-root')
const refactorMoreRoot = parseArg('refactor-more-root')

if (!mainRoot || !refactorRoot || !refactorMoreRoot) {
  console.error(
    'Usage: deno run -A benchmarks/context-new.mitata.ts --main-root=/path/to/main --refactor-root=/path/to/refactor-context --refactor2-root=/path/to/refactor2-context'
  )
  Deno.exit(1)
}

const MainContext = await loadContextCtor(mainRoot as string)
const RefactorContext = await loadContextCtor(refactorRoot as string)
const RefactorMoreContext = await loadContextCtor(refactorMoreRoot as string)
const req = new Request('http://localhost/')

console.log(`main-root: ${resolve(mainRoot)}`)
console.log(`refactor-root: ${resolve(refactorRoot)}`)
console.log(`refactor-more-root: ${resolve(refactorMoreRoot)}`)

group('new Context(req)', () => {
  bench('main: new Context(req)', () => {
    void new MainContext(req)
  })

  bench('refactor-context: new Context(req)', () => {
    void new RefactorContext(req)
  })

  bench('refactor-more-context: new Context(req)', () => {
    void new RefactorMoreContext(req)
  })
})

group('new Context(req).text("hello")', () => {
  bench('main: new Context(req).text("hello")', () => {
    void (new MainContext(req) as any).text("hello")
  })

  bench('refactor-context: new Context(req).text("hello")', () => {
    void (new RefactorContext(req) as any).text("hello")
  })

  bench('refactor-more-context: new Context(req).text("hello")', () => {
    void (new RefactorMoreContext(req) as any).text("hello")
  })
})

await run()
Command $ git clone 'git@github.com:honojs/hono.git' /tmp/hono-main $ git clone -b refactor-context 'git@github.com:usualoma/hono.git' /tmp/usualoma-refactor-context $ git clone -b refactor-context-2 'git@github.com:usualoma/hono.git' /tmp/usualoma-refactor-context-2 $ deno run -A --sloppy-imports --node-modules-dir=auto context-new.ts --main-root=/tmp/hono-main --refactor-root=/tmp/usualoma-refactor-context --refactor-more-root=/tmp/usualoma-refactor-context-2
clk: ~3.21 GHz
cpu: Apple M2 Pro
runtime: deno 2.6.7 (aarch64-apple-darwin)

benchmark                                            avg (min … max) p75 / p99    (min … top 1%)
-------------------------------------------------------------------- -------------------------------
• new Context(req)
-------------------------------------------------------------------- -------------------------------
main: new Context(req)                                 23.54 ns/iter  27.57 ns             █        
                                               (6.30 ns … 548.03 ns)  31.01 ns             █        
                                             (  0.18  b …   1.28 kb) 604.68  b ▁▁▁▁▁▁▁▁▁▁▁▁█▃▁▁▁█▃▁▁

refactor-context: new Context(req)                    952.60 ps/iter 935.79 ps  █▃                  
                                             (874.76 ps … 182.09 ns)   1.33 ns  ██▂                 
                                             (  0.09  b … 426.56  b)   0.19  b ▁███▄▂▁▁▁▁▁▁▂▁▁▁▁▁▁▁▁

refactor-more-context: new Context(req)               943.10 ps/iter 935.79 ps  ▂█                  
                                             (874.76 ps … 174.26 ns)   1.21 ns  ██▆▇                
                                             (  0.09  b … 358.15  b)   0.14  b ▁████▆▂▂▁▁▁▁▁▁▁▁▂▁▁▁▁

• new Context(req).text("hello")
-------------------------------------------------------------------- -------------------------------
main: new Context(req).text("hello")                  119.46 ns/iter 125.09 ns  █                   
                                             (113.21 ns … 189.82 ns) 141.69 ns  ██      ▂           
                                             (  1.88 kb …   2.38 kb)   2.09 kb ▁██▅▂▂▂▂▅█▃▂▂▂▁▁▁▁▁▁▁

refactor-context: new Context(req).text("hello")       88.48 ns/iter  86.47 ns  █                   
                                                (79.36 ns … 1.06 µs) 135.26 ns  █                   
                                             (  1.05 kb …   1.28 kb)   1.17 kb ▄█▇▃▄▄▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁

refactor-more-context: new Context(req).text("hello")  82.69 ns/iter  83.66 ns  █                   
                                              (77.71 ns … 161.35 ns) 106.84 ns  █▃                  
                                             (882.52  b …   1.18 kb)   1.08 kb ▆██▆▄▃▃▂▄▃▂▂▂▁▁▁▁▁▁▁▁

Size

It will be about 185 bytes larger.

% git checkout main && npx esbuild --minify src/context.ts | wc
Switched to branch 'main'
Your branch is up to date with 'origin/main'.
       1      74    2631
% git checkout refactor-context && npx esbuild --minify src/context.ts | wc
Switched to branch 'refactor-context'
Your branch is up to date with 'usualoma/refactor-context'.
       1      93    2816

The author should do the following, if applicable

  • Add tests
  • Run tests
  • bun run format:fix && bun run lint:fix to format the code

@codecov
Copy link
Copy Markdown

codecov bot commented Feb 22, 2026

Codecov Report

❌ Patch coverage is 92.62295% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.91%. Comparing base (2de30d7) to head (81d6060).

Files with missing lines Patch % Lines
src/context.ts 92.62% 9 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4755      +/-   ##
==========================================
- Coverage   91.48%   90.91%   -0.58%     
==========================================
  Files         177      177              
  Lines       11556    11662     +106     
  Branches     3357     3373      +16     
==========================================
+ Hits        10572    10602      +30     
- Misses        983     1059      +76     
  Partials        1        1              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@usualoma
Copy link
Copy Markdown
Member Author

Hi @yusukebe

(Slightly) larger in size, but (though only slightly) it represents a clear performance improvement, so I think this PR approach is preferable.
What do you think?

@EdamAme-x
Copy link
Copy Markdown
Contributor

EdamAme-x commented Feb 23, 2026

I applied this and ran the benchmark script on WSL and Apple silicon, and both results were great.

WSL Result

app.get(c => c.text())

Node (1.1x faster)

runtime: node 24.13.1 (x64-linux)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
latest  app.fetch(req)         1.01 µs/iter   1.02 µs    █▆
                    (648.00 ns … 457.66 µs)   1.91 µs   ████▅
                    (  1.48 kb …   2.99 mb)   5.82 kb ▁▅██████▅▃▂▂▂▂▂▂▂▁▁▁▁

refactor app.fetch(req)      931.02 ns/iter 954.00 ns   ▃█▄
                    (687.00 ns … 254.58 µs)   1.62 µs   ███▇▃
                    (512.00  b … 418.49 kb)   4.93 kb ▂███████▆▄▃▂▂▁▁▁▁▁▁▁▁

summary
  refactor app.fetch(req)
   1.09x faster than latest  app.fetch(req)

Bun (2.7x faster)

runtime: bun 1.3.9 (x64-linux)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
latest  app.fetch(req)         1.29 µs/iter   1.06 µs █
                    (677.00 ns … 668.63 µs)   6.01 µs █▃
                    (  0.00  b … 192.00 kb) 434.56  b ██▃▂▃▃▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁

refactor app.fetch(req)      481.63 ns/iter 352.00 ns ██
                    (217.00 ns … 643.98 µs)   2.60 µs ██
                    (  0.00  b … 192.00 kb) 111.24  b ██▅▃▂▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

summary
  refactor app.fetch(req)
   2.68x faster than latest  app.fetch(req)

(Note: These are micro-benchmark results; when tested with Bombardier, i see an improvement of about 30%)

Deno (1.2x faster)

runtime: deno 2.6.10 (x86_64-unknown-linux-gnu)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
latest  app.fetch(req)       333.68 ns/iter 349.00 ns   █▆
                    (201.00 ns … 182.87 µs) 615.00 ns   ███▇█▇
                    (  2.59 kb … 386.27 kb)   2.66 kb ▁▃███████▅▄▄▃▃▃▂▂▂▁▁▁

refactor app.fetch(req)      273.12 ns/iter 294.00 ns   █
                    (172.00 ns … 168.74 µs) 561.00 ns   █▄
                    (  1.84 kb … 270.59 kb)   1.89 kb ▁▃██▆▄▅▆▄▂▂▂▂▂▁▁▁▁▁▁▁

summary
  refactor app.fetch(req)
   1.22x faster than latest  app.fetch(req)

@usualoma
Copy link
Copy Markdown
Member Author

@EdamAme-x Thank you!
That's a great result!

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