Skip to content

perf(docs): stop shipping translated docs markdown in the serverless function - #3387

Merged
steve8708 merged 4 commits into
mainfrom
steve8708/changes-1029
Aug 22, 2026
Merged

perf(docs): stop shipping translated docs markdown in the serverless function#3387
steve8708 merged 4 commits into
mainfrom
steve8708/changes-1029

Conversation

@steve8708

Copy link
Copy Markdown
Contributor

51MB → 30MB on the docs server function (1856 → 615 chunks).

docs-content.ts globs core/docs/content/locales/*/*.md{,x} with query: "?raw", so Vite emits one lazy chunk per translated file — 1251 of them. Netlify's nodeBundler: "none" + includedFiles: ["**"] ships every one, and the function unzips all of it on every cold start.

It can never serve them. Every localized docs page is prerendered, so the CDN answers those URLs from the publish directory and the render function is never invoked for them. Measured: 124 prerendered pages per locale against 125 sources, the single gap being the database redirect slug.

The globs stay — their keys still drive localizedDocKey and the locale-availability enumeration, so hreflang and the locale switcher are unaffected. All 193 English markdown chunks remain.

Safety: the pruner attributes chunks by parsing the glob map Vite emitted into the bundle itself, so it cannot drift from what was actually built. It skips redirect and draft slugs, and aborts the build if any chunk it would delete has no prerendered page — a translated doc the function is still the only renderer for fails loudly rather than 500ing in production. 4 tests cover locale-only attribution, the shared-with-English case, and both sides of the prerender assertion.

Found via a 42-agent teardown of why a docs site needs a 50MB function.

🤖 Generated with Claude Code

…tches

The share route's fix left three siblings with the same shape: a useState
initializer reading sessionStorage, so the first client render disagrees with
the server's, React discards the hydrated tree, and the page re-renders from
scratch - blank for a returning viewer.

- clips embed.$shareId: the embedded player, same saved-password read
- clips + slides access-request.approve: the saved approval token

Each starts from the value the server can also compute and adopts the stored
one after mount.
…function

docs-content.ts globs core/docs/content/locales/*/*.md{,x} with query:"?raw",
so Vite emits one lazy chunk per translated file - 1251 of them. Netlify's
nodeBundler:"none" + includedFiles:["**"] ships every one and the function
unzips all of it on each cold start.

It can never serve them: every localized docs page is prerendered, so the CDN
answers those URLs from the publish directory and the render function is never
invoked. The globs stay - their KEYS still drive localizedDocKey and the
locale-availability enumeration, so hreflang and the locale switcher are
unaffected.

Measured: docs server function 51MB -> 30MB, 1856 -> 615 chunks, with all 193
English markdown chunks intact.

The pruner refuses to delete a chunk it cannot prove is prerendered, and skips
redirect and draft slugs, so a page the function is still the only renderer for
aborts the build instead of 500ing in production.
@netlify

This comment has been minimized.

@github-actions

github-actions Bot commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Here's a visual recap of what changed:

Visual recap

Open the full interactive recap

@builder-io-integration builder-io-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Builder reviewed your changes and found 1 potential issue 🟡

Review Details

Code Review Summary

PR #3387 adds a post-build pruner that removes locale-only translated Markdown chunks from emitted docs serverless functions, while retaining the glob keys used for locale availability and hreflang behavior. It also defers sessionStorage reads in Clips and Slides approval/embed routes until after mount to avoid SSR hydration mismatches. The overall build strategy is sound: attribution is based on the emitted Vite loader map, English/shared chunks are protected, and the script refuses to prune when a static page cannot be proven.

Risk assessment: Standard (build/runtime behavior and user-facing route state changes).

Key Findings

  • 🟡 MEDIUM: Draft detection only inspects localized frontmatter; canonical English drafts can make the script abort despite their translations not being prerendered.
  • 🟢 The hydration changes preserve URL-token precedence and make unavailable storage visible as the locked/token-required state.
  • 🟢 Tests cover locale-only attribution, shared chunks, and missing/present prerender outputs.

🧪 Browser testing: Will run after this review (PR touches UI code)

Comment on lines +142 to +145
// A redirect slug never renders its own page, and a draft is not
// prerendered; both keep their chunk rather than gamble on the 301/500.
if (
keys.some((key) => redirectSlugs.has(slugFromKey(key)) || isDraft(key))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Draft check misses canonical drafts when the translation lacks frontmatter

buildPrerenderPaths excludes every locale page whose canonical document is a draft, but this pruner checks only the localized source file. If the English source is marked draft: true while a translation lacks matching frontmatter, its page is not prerendered and this code still adds the chunk to provenKeys, causing the build to abort instead of retaining the chunk. Derive draft status from the canonical source slug as the prerenderer does, and add coverage for this mismatch.

Fix in Builder

Five cuts, each measured on the netlify preset:

- @xterm/* is stubbed out of the SSR graph by default in core, not repeated in
  sixteen vite configs. It is only reachable through a React.lazy boundary whose
  module body guards on typeof window === undefined, so the server can never
  import it and the chunk was pure unpack weight everywhere.
- formatExtensionHtml (and its slides twin) load prettier/standalone plus the
  four plugins the HTML printer reaches. prettier's main entry import()s all 13
  parser plugins, so bundlers inlined ~3.5MB of flow/typescript/yaml/markdown
  parsers to format HTML.
- docs stubs the tiptap/ProseMirror/assistant-ui editor stack: it never
  server-renders the agent sidebar or resource editor. lowlight and yjs stay
  real - both genuinely run on the server.
- the docs corpus glob map is declared once instead of twice, so Rollup stops
  emitting two byte-identical copies of a 1447-entry lazy-import map.

Measured: calendar 46.7MB -> 21.1MB, docs 51MB -> 26MB (76.8MB at the start of
this work).
The background and integration-recovery clones overwrite url.pathname
unconditionally before delegating to main.mjs, so they can never route to the
SSR page/asset handlers they inherited from the server bundle. Netlify zips and
uploads every function separately, so that island was paid for on every deploy.

The pruner walks the clone's real import graph - including rolldown's backtick
dynamic imports, which a quote-only scan under-reports by tens of MB - and
refuses to prune anything when a relative dynamic import cannot be resolved
statically. It also asserts the entry actually rewrites url.pathname, which
caught this change being wired into the recurring-jobs emit by mistake.

Measured on calendar: total upload 42.2MB -> 35.8MB.

@builder-io-integration builder-io-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Builder reviewed your changes — no new findings

Review Details

Incremental Code Review Summary

The latest PR head expands the payload optimization beyond locale pruning: docs now share one lazy glob-loader map, the docs build stubs client-only editor dependencies, core defaults selected xterm packages to SSR stubs, and HTML formatting uses Prettier standalone with the required plugins. The existing Clips/Slides hydration fix remains unchanged. The approach is sound, and the returned review validation found no new confirmed regressions: localization/pruning tests, core Vite tests, and a standalone HTML-formatting smoke test passed.

Risk assessment: Standard. The previously reported canonical-draft pruning issue remains open and was intentionally not reposted. No new actionable findings were identified in this incremental review.

🧪 Browser testing: Will run after this review (PR touches UI code)

@steve8708
steve8708 merged commit 6e647cb into main Aug 22, 2026
46 checks passed
@steve8708
steve8708 deleted the steve8708/changes-1029 branch August 22, 2026 14:01
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