Repository navigation
fix(client): avoid retaining onContentUpdated callbacks during ssr - #1722
Merged
Merged
Conversation
`onContentUpdated` adds the callback to a module-level Set and relies on `onUnmounted` to remove it. During `vuepress build`, every page is rendered with the same server app, and components are never unmounted in SSR, so every callback registered while rendering stays in the Set until the process exits. Each callback closure usually captures component state, which keeps the component instance and, through `parent` / `subTree`, the whole component and vnode tree of the rendered page alive. As a result, the memory used by the build grows linearly with the number of pages, and large sites may run out of memory while rendering. Content updated callbacks are only invoked on client side, so skip the registration in SSR. The rendered HTML is unchanged.
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
No unresolved issues were identified, and client-side behavior remains unchanged.
Review effort: Lite
Findings: None
What changed in this PR
Fixes SSR memory growth by preventing onContentUpdated callbacks from being retained during server-side rendering.
Changes:
- Skips callback registration in SSR.
- Preserves client-side callback behavior.
| File | Description |
|---|---|
packages/client/src/composables/onContentUpdated.ts |
Avoids registering callbacks during SSR. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This was referenced Sep 27, 2026
Mister-Hope
approved these changes
Sep 27, 2026
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.
Before submitting the PR, please make sure you do the following
close #123).What is the purpose of this pull request?
Description
Symptom
On large sites,
vuepress buildmemory usage grows linearly with the number of pages during the "Rendering N pages" step, until the build runs out of memory.Root cause
onContentUpdatedadds the callback to the module-levelcontentUpdatedCallbacksSet and relies ononUnmountedto remove it. During build, all pages are rendered with the same server app (renderPageToString→renderToString), and components are never unmounted in SSR. So every callback registered while rendering stays in the Set until the process exits.A callback closure usually captures component state (for example,
vuepress-theme-hope'sNavbarDropdowncallsonContentUpdated(() => { open.value = false })), which keeps the component instance alive, and throughparent/subTreethe whole component and vnode tree of the rendered page.Reproduction
onContentUpdatedinsetup, e.g. the e2e site'sOnContentUpdatedroot component, or a theme likevuepress-theme-hope.vuepress buildand inspectcontentUpdatedCallbacks.sizeafter rendering (or watch the heap during "Rendering N pages").With a temporary log added to the built client, the e2e site (webpack bundler, 55 pages) ends the build with 55 retained callbacks before this change and 0 after it.
Measurements on a real site
1253 pages,
vuepress@2.0.0-rc.31,vuepress-theme-hope@2.0.0-rc.109, Vite 8 (Rolldown), Node 24:--max-old-space-size.contentUpdatedCallbacksSet → callback closure →emit→ component instance →parent/subTree.onContentUpdatedreturn early in the server bundle): the heap no longer grows while rendering (sawtooth around ~1 GB), the JS heap of the whole build peaks at ~1.4 GB, and the same 6 GB-limited build succeeds.Fix
Return early from
onContentUpdatedwhen__VUEPRESS_SSR__istrue. The callbacks are only invoked from theonVnodeMounted/onVnodeUpdated/onVnodeBeforeUnmounthooks of<Content>, which never run in SSR. So registering them on the server has no effect other than retaining memory. Client-side behavior, including hydration, is unchanged.Output
The generated HTML is unchanged. With the workaround above, 1409 of the 1410 HTML files of the real site are identical to the unpatched build after normalizing hashed asset names; the remaining one is a timeline page that only differs in the order of entries with the same date, which also differs between two unpatched builds. For the e2e site (webpack), the 55 HTML files are identical except for the hashed
app.jsfile name; the content ofapp.jsonly differs in the order of entries in the generated route map, which is not affected by this change.Verification
pnpm build,pnpm lint,pnpm check-typesandpnpm test:unitpass.contentUpdatedCallbacks.sizeadded to the built client, the e2e site (webpack bundler, 55 pages) ends the build with 55 retained callbacks before this change and 0 after it, ande2e/tests/composables/on-content-updated.spec.tscovers the unchanged client-side behavior.Screenshots
N/A