Conversation
…server transformMain built a second descriptor for every SFC, solely for HMR, on any build: the only guard was fs.existsSync(filename), with no check for a dev server. The result goes into hmrCache, whose only reader is handleHotUpdate, which never runs on a build. Both caches are unbounded Maps that live until the process exits, so the cost is linear in the number of components and nothing reads it. The two parse calls happen back to back, so with identical text the second one hits the compiler-sfc parse cache and costs nothing - which is why a plain project shows no difference. The cost appears when an enforce: 'pre' plugin rewrites .vue before plugin-vue sees it (auto test-id injection, compile-time env substitution in templates, i18n extraction): the two calls then parse different text and a second descriptor is really built and retained. Only the first conjunct of the neighbouring condition is used deliberately. The others (hmr !== false, !ssr, !isProduction) govern emitting HMR code, whereas the question here is whether hmrCache has a reader at all - and it has one exactly when a dev server exists. handleHotUpdate returns silently when the descriptor is missing, so an extra conjunct would break HMR with no error.
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.
fixes #841
Description
transformMainbuilds a second descriptor for every.vue, solely for HMR, on any build. The onlyguard is
fs.existsSync(filename)— no check for build mode, no check for a dev server. The resultgoes into
hmrCache, whose only reader ishandleHotUpdate, which never runs on a build. Bothcaches are unbounded
Maps that live until the process exits, so the cost is linear in the numberof components and nothing reads it.
This PR gates that one call on
devServer.It does not undo #227 / #232. The HMR descriptor is still built from the raw fs read exactly as
those PRs intended, and #236's
fs.existsSyncguard for virtual files stays; this only skipsseeding the cache when there is no dev server to consume it.
Why it is invisible in an ordinary project
The two
parsecalls happen back to back in the same tick, and@vue/compiler-sfccaches bygenCacheKey(source, options)— with identical text the second call is a guaranteed cache hit andcosts nothing. The cost appears when an
enforce: 'pre'plugin rewrites.vuebeforeplugin-vuesees it (auto test-id injection, compile-time env substitution in templates, i18n extraction): the
two calls then parse different text and a second descriptor really is built and retained. Same
divergence as #301, seen from the build side.
Reproduction, driving the plugin hooks directly, one arm per process:
https://github.com/cainrus/vite-plugin-vue-hmr-descriptor-repro — 500 components, distinct
descriptors 500 → 1000 once a pre-plugin is present. On a production app (~1963 components, library
-mode build of a large monorepo) this gate measured -192 MB of live set on average over two arm
pairs with the order swapped, peak RSS -450/-536 MB, 7-12 s off the build, emitted JS byte-for-byte
identical.
vite build --watchAlso has no reader, so the gate is safe there: Vite invokes the hook from
handleHMRUpdate, whoseonly call site is inside the dev server (
onHMRUpdatein_createServer), and the plugin's ownhandleHotUpdateopens withctx.server.ws.send(...).Why only the first conjunct
The neighbouring HMR condition is
devServer && devServer.config.server.hmr !== false && !ssr && !isProduction. The others governemitting HMR code; the question here is whether
hmrCachehas a reader at all, and it has oneexactly when a dev server exists. The asymmetry matters:
handleHotUpdatereturns silently when thedescriptor is missing, so an extra conjunct would break HMR with no error, while a missing one would
only leave today's waste.
Tests
packages/plugin-vue/__tests__/hmr-descriptor-cache.spec.tsdrivestransformMainand assertshmrCachedirectly: empty after a build, populated when a dev server is present. Verified it failswithout the change — with the gate removed,
is not populated on a buildfails on thetoBeUndefinedassertion while the dev-server case stays green.eslintandoxfmt --checkareclean on both files.
The test writes a real file to a temp dir on purpose: the branch is guarded by
fs.existsSync, soagainst a non-existent path it would be skipped for the wrong reason and could not fail.
What is the purpose of this pull request?