Skip to content

perf(prerender): reuse each page's HTML render for its navigation data - #501

Merged
Brooooooklyn merged 3 commits into
voidzero-dev:mainfrom
yangshun:perf/prerender-single-request
Oct 20, 2026
Merged

Brooooooklyn merged 3 commits into
voidzero-dev:mainfrom
yangshun:perf/prerender-single-request

Conversation

@yangshun

@yangshun yangshun commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Problem

Static prerendering made two requests per page: one for the HTML, and a second with X-VoidPages to write _void/pages/*.json. The loader and middleware ran twice for every page.

Fix

Fixes #500

The HTML already embeds the page object in __VOID_PAGE_DATA__, so the prerenderer now writes the JSON from that script and skips the second request.

  • Uses extractPageDataFromHtml from @void/isr/core, which ISR already uses to build JSON from page HTML.
  • Pages with deferred props still make the second request. Their HTML has those props missing, because the values are streamed after the page data script. The prefetch request returns them filled in.
    • In practice, defer() is mainly useful for server-rendered pages. A fully static site rarely needs it, so on most static sites the two implementations behave the same. IMO, this is an acceptable fix. The alternative is to
  • Island pages and non-page routes have no __VOID_PAGE_DATA__ script, so they still get no JSON file.
  • The JSON is parsed and stringified again, so the files are byte-identical to before. The embedded copy escapes <, > and &.

Results

I ran two benchmarks, once with a 1000-page Void app (no actual network requests), another with a remote PayloadCMS database.

1000-page Void app

Measured with my own 1000-page Void app benchmark, static output. Old and new code ran alternately in one process on the same build:

Before After
Requests 2,002 1,001
Prerender phase, Node 422–477 ms 380–396 ms (−10% to −19%)
Prerender phase, Cloudflare 848–1,660 ms 630–1,149 ms (−26% to −31%)

On Node, the second request was an in-process call, so a full build barely changes: 4.99 s before vs 4.64 s after, median of 3, within noise. The gain is larger on Cloudflare, where each request goes through the local Workers runtime.

The built output (1,001 HTML and 1,001 JSON files) is byte-identical to before on both Node and Cloudflare.

100-page blog backed by remote Payload CMS

Tested in a more realistic scenario of a 100-post blog backed by a remote Payload CMS database, where each pre-rendered page fetches from Payload CMS:

Before After
Build time 265.00s 133.31s
Payload operations 1,439 722

Tests

  • Updated two existing tests that served page data only through the second request.
  • Added one test for deferred pages, which still need the second request, and for HTML without page data, which gets no JSON file.
  • static-pages and node-static integration tests pass.

Alternative considered

Another approach handles deferred props in the same single request. During prerendering, the page handler copies each deferred value into the page object as it streams, then appends the completed object to the end of the HTML as a build-only script.

The prerenderer removes that script and writes it as the JSON file. This avoids the second request even for deferred pages, but it changes handlePageGet, which runs on every page request in every app, and the appended script has to pass through the app's HTML middleware unchanged.

I prefer to keep the change in build-time code instead. Deferred props are uncommon in fully static pages, so those pages keeping the second request costs little.

Static prerendering fetched every page twice: once for the HTML and again
with X-VoidPages for `_void/pages/*.json`. The HTML already embeds the same
page object in `__VOID_PAGE_DATA__`, so read the JSON from it instead.

Pages with deferred props embed them unresolved, so they keep the second
request, which resolves them inline. Output is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@yangshun yangshun changed the title perf(prerender): write page data from the rendered HTML perf(prerender): write page data from the rendered HTML, reduces pre-render duration by around 50% Oct 20, 2026
Comment thread packages/void/src/prerender-render.ts
Comment thread packages/void/src/prerender-render.ts
@yangshun yangshun changed the title perf(prerender): write page data from the rendered HTML, reduces pre-render duration by around 50% perf(prerender): reduce pre-render duration by around 50% by extracting page data from the rendered HTML Oct 20, 2026
@yangshun yangshun changed the title perf(prerender): reduce pre-render duration by around 50% by extracting page data from the rendered HTML perf(prerender): reuse each page's HTML render for its navigation data Oct 20, 2026
yangshun and others added 2 commits October 20, 2026 16:56
The tests now fail if prerendering sends the X-VoidPages request again for
a page whose HTML embeds its data: they record every page-data request and
expect none, except for a deferred page, which still needs it after its
HTML is written. One page uses characters the embedded script escapes, and
its JSON file must match JSON.stringify of the page object. The comment
says why the embedded text is parsed and stringified again.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Brooooooklyn

Copy link
Copy Markdown
Member

@codex review

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-20T09:22:02.077668Z 846205a Manual request
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Delightful!

Reviewed commit: 846205ab0c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@Brooooooklyn
Brooooooklyn merged commit 6f78ab6 into voidzero-dev:main Oct 20, 2026
18 checks passed
@yangshun
yangshun deleted the perf/prerender-single-request branch October 20, 2026 10:53
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.

Static prerendering renders every page twice

2 participants