Skip to content

Load the OpenAPI renderer only on pages that use it - #4472

Draft
nolannbiron wants to merge 5 commits into
mainfrom
nolann/openapi-serializable-context
Draft

Load the OpenAPI renderer only on pages that use it#4472
nolannbiron wants to merge 5 commits into
mainfrom
nolann/openapi-serializable-context

Conversation

@nolannbiron

@nolannbiron nolannbiron commented Aug 6, 2026

Copy link
Copy Markdown
Member

Pages with no OpenAPI block were still shipping the whole @gitbook/react-openapi client renderer — react-aria included — in the route entry. Next inlines a client module into the entry as soon as a server module reaches it through the graph, and tree-shaking never applies, so the only fix is to stop reaching it.

Two changes, and neither works without the other:

  • The block context is now built inside a 'use client' component loaded through next/dynamic. Functions can't cross the RSC boundary, so renderCodeBlock is rebuilt on the client, while renderHeading / renderDocument are pre-rendered on the server and passed as ReactNode, and the proxy URL is pre-signed into a string.
  • experimental.optimizePackageImports for the package, which rewrites barrel imports into deep ones. Without it, context.tsx and the resolveOpenAPI*Block.ts files kept dragging the renderer in for the sake of four helpers.

Measured

Total JS downloaded by /url/mariadb.com/docs — a page with no OpenAPI block — on the Vercel previews, summing every chunk the HTML references.

JS downloaded react-aria openapi
#4467 (Shiki split) 2170.3 KB yes yes
#4468 (lazy Mermaid) — current main 2158.2 KB yes yes
this PR 1949.5 KB no no

−208.7 KB (−9.7%), and react-aria leaves the page entirely. Neither PR gets that alone: #4468 removed the last non-OpenAPI importer (FocusScope / usePreventScroll in the Mermaid block), this one removes react-aria-components and react-stately with the renderer.

The dynamic boundary on its own is a regression: built without optimizePackageImports, the same page grows to 2162.2 KB against main's 2114.9 KB (measured locally, which reads ~4% low in absolute terms but is consistent across the three builds). The boundary makes the renderer extractable; the import rewrite is what stops the incidental imports from holding it in place.

Two bugs this surfaced

Moving the block to the client changed which code runs in the browser, and both bugs come from that.

  • Schemas blocks lost their heading. OpenAPISchemas asks the context for one, and only the operation heading was being pre-rendered. getSchemasHeading is now exported so the renderer and the host reach the same answer from one place.
  • Hydration mismatch on any date-time example. generateSchemaExample builds its placeholders from new Date() at module scope, so the server's module instance and the browser's disagree. Under force-static they are hours apart, not milliseconds. Examples are now generated once during resolution and attached to the schema keyed by variant; the browser reads them instead of regenerating, so the date is only ever evaluated on the server. precompute-examples.test.ts locks the invariant — it replays all six render call sites against JSON-round-tripped data and fails if anything regenerates.

Trade-offs

  • Code samples lose server-side syntax highlighting. The text stays in the SSR HTML, so SEO is unaffected, but it flashes unhighlighted — the same behaviour as deferred code blocks today.
  • On pages that do have an OpenAPI block, the renderer moves from the entry into a ~314 KB chunk fetched after it, and the block's subtree now hydrates instead of arriving as server-rendered HTML. Pages without OpenAPI are a clear win; API pages trade eager bytes for a deferred load and more hydration. Worth a Lighthouse pass on a large API page before this leaves draft.
  • Pre-generated examples travel in the RSC payload: +11–16% per operation, measured on petstore.
  • optimizePackageImports bails silently if packages/react-openapi/src/index.ts ever stops being pure re-exports, which would quietly bring the 209 KB back.

Not in scope

The OpenAPI stylesheet (~335 KB including Scalar) is still loaded on every page. Moving its import behind the dynamic boundary does remove it from the entry, but the App Router has no way to emit a <link> for it during SSR, so blocks would render unstyled before hydration. Doing this properly needs the stylesheet emitted as a standalone asset and referenced from the server component — best done alongside #4464, which already builds that machinery for Scalar's JS.

API

@gitbook/react-openapi keeps a single public entry; /core and /light were tried and dropped in favour of the barrel optimizer. The minor bump is for one new export, getSchemasHeading, and for the generated examples now present in resolved data.

@changeset-bot

changeset-bot Bot commented Aug 6, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 0fb4e07

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 2 packages
Name Type
gitbook Patch
@gitbook/react-openapi Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

`/core` and `/light` were named after their weight rather than their
contents, so nothing said where a new export belonged. Next's barrel
optimizer reaches the same client entry (1519.8 KB) from plain barrel
imports, and the package keeps a single public entry.

Also renders the schemas block heading again: `OpenAPISchemas` asks the
context for one, and only the operation heading was pre-rendered.
The block became a client component, so `generateSchemaExample` ran again
while hydrating — and its date placeholders come from `new Date()` at module
scope, which differs between the server's module instance and the browser's.
Under `force-static` the two are hours apart, not milliseconds.

Resolving now generates every example the renderer asks for and attaches it
to the schema, keyed by variant. The browser reads those values instead of
regenerating, so the date is only ever evaluated on the server.
@argos-ci

argos-ci Bot commented Aug 6, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Argos notifications ↗︎

Build Status Details Updated (UTC)
customers-v2-cloudflare (Inspect) ⚠️ Changes detected (Review) 8 changed, 4 ignored Aug 6, 2026, 12:32 PM
customers-v2-vercel (Inspect) ⚠️ Changes detected (Review) 12 changed, 4 removed, 1 failure, 4 ignored Aug 6, 2026, 12:30 PM
v2-cloudflare (Inspect) ⚠️ Changes detected (Review) 1 changed, 1 ignored Aug 6, 2026, 12:34 PM
v2-vercel (Inspect) ✅ No changes detected - Aug 6, 2026, 12:32 PM

The client boundary made every clock read in the render path a hydration
hazard. The date was the only one, so fixing it removes the need to carry
generated examples through the payload.
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.

1 participant