Skip to content

tsgo causes major issues with @hono/zod-openapi route chain accumulation -- documentation update recommended? #1918

Description

@daviduzumeri

Which middleware is the feature for?

@hono/zod-openapi

What is the feature you are proposing?

Hey, sorry. Claude got a little zealous and posted a comment without the ability for me to write a human forward and explain where this came from.

I've converted an almost comically large API surface from express-openapi-validator and spec-first to Hono using @hono/zod-openapi (we have a Swift client!) and Hono RPC, and am moving everything into a monorepo structure, just as part of a general modernization effort (and also since it's worth doing while tokens are cheap). We've been on tsgo and oxlint-tsgolint for a while, and have very inference-heavy Kysely usage, so I figured going all-in on TS inference with Hono wouldn't be that big of a deal. Instead, it made my M1 Air basically cry itself to sleep, took way longer than expected on my Core i9, and required significantly increasing our GHA runner resource class for CI.

So I spent a bunch of time trying to figure out what was going on, since while I was expecting some tradeoff, this seemed orders of magnitude worse than what I expected. I'll let Claude take it from here, but I've got numbers that have been independently verified, and some reproduction repos. I don't think this is necessarily Hono's problem to solve, and I'll also be sharing these results with the tsgo team, but in case this is too architecturally deep to be fixed in time for 7.0 GA I figured I'd post about it here with the amelioration tactic that's been successful for us in case y'all want to update the best-practices guidelines so other people don't run into this same footgun when they upgrade to tsgo and discover that it's somehow slower.

Claude from here:
Some data that may be useful here: the "excessively deep" cost from .openapi() is largely an accumulation effect, and there's a behavior-preserving way to sidestep it.

Building an OpenAPIHono by chaining .openapi(route, handler) accumulates the route-schema generic at every link. For the same routes, handlers, and emitted OpenAPI spec, registering each route on its own single-route app and combining with .route() is dramatically cheaper to type-check.

Type instantiations (tsc --extendedDiagnostics), N=60 routes, realistic schemas — tsc 5.9.3 and @typescript/native-preview (tsgo) 7.0.0-dev:

variant tsc tsgo
chained .openapi() 4.71M 4.69M
.route()-merged 1.50M 1.49M
chained + hc<> client 5.13M 9.80M
merged + hc<> client 1.74M 3.23M

Two takeaways:

  • App side: .route()-merging is ~3× cheaper than chaining, under both compilers.
  • Client side: the hc<> instantiation increment over a chained app is ~+0.4M under tsc but ~+5.1M under tsgo (~12×). As the native port approaches GA, zod-openapi RPC consumers are where this will bite hardest — and the merge cuts it ~3× there too.

(Plain zValidator .get/.post RPC doesn't show this — it's already cheap and merging makes it worse; the effect is specific to .openapi()'s accumulation.)

Workaround — identical routing and OpenAPI output:

export const app = new OpenAPIHono()
  .route("/", new OpenAPIHono().openapi(routeA, handlerA))
  .route("/", new OpenAPIHono().openapi(routeB, handlerB));

Minimal repro (one command, runs tsc + tsgo per variant): https://github.com/daviduzumeri/hono-zod-openapi-rpc-typecheck-perf

Could .openapi() accumulate via a flatter representation, and/or could the docs recommend the .route()-merge pattern for large RPC apps ahead of tsgo GA?

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions