fix(docs): serialize prerender so large pages aren't truncated#44
Merged
Conversation
The SPA prerender defaults to os.cpus().length concurrency. Several routes race through the in-process render server at once, and under that load a large response body stream is cut at the first ~64KiB chunk before res.text() drains it. Any page over 64KiB (e.g. /islands/charts, /reference/manifest) is then written truncated, losing its trailing hydration <script> and shipping as dead, non-interactive HTML. Which page loses the race varies per build and worsens on high-core CD runners, so it slipped past local builds into production. Set prerender.concurrency to 1 to remove the race, and add a postbuild guard that fails the build if any prerendered page is missing its closing </html>.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Set the docs SPA prerender to
concurrency: 1, and add a postbuild guard that fails the build if any prerendered page is truncated.The bug
Visiting a docs page over ~64KiB directly (cold load, e.g.
https://openislands.sh/islands/charts/#divergencebars) shows the static HTML but nothing hydrates — charts are empty, the mobile sidebar drawer won't open, links full-reload. It only reproduces on a direct visit (SPA navigation from another page renders client-side and never re-fetches), and it's most visible on mobile because the sidebar is a JS drawer; on desktop the sidebar is CSS-visible so the page looks fine.Root cause
The TanStack Start SPA prerender defaults to
os.cpus().lengthconcurrency (@tanstack/start-plugin-core→prerender.js):Several routes race through the in-process render server at once. Under that load a large response's body stream is cut at the first ~64KiB chunk before
res.text()drains it, so any page over 64KiB is written truncated — losing the trailing hydration<script>at the end of<body>— and ships as dead, non-interactive HTML.Which page loses the race varies per build, and high-core CD runners make it more likely, so it slipped past local builds into production. Evidence:
/islands/charts/served exactly 65,536 bytes, no</html>, no hydration bootstrap;/reference/manifest/too..txtboth serve whole in prod → no Cloudflare/serving size limit.reference/manifest) while charts was fine — non-deterministic, the race signature.Fix
vite.config.ts—prerender: { concurrency: 1 }removes the race. Serial prerender of 21 pages is a negligible build-time cost.scripts/postbuild.mjs— guard that fails the build if any prerenderedindex.htmlis missing its closing</html>, so a truncated page can never reach a deploy again (it reached prod silently and was caught by a user, not CI).Verification
Before: local build → 1 truncated page. After:
pnpm build→ 21/21 pages complete, 0 truncated; all 13 pages over 64KiB (manifest 194KB, charts 143KB, …) end with</html>and keep their entry script. Lint + typecheck pass.