Feedback came in from a review of the current Vite and Rsbuild collectors, ahead of another round of data collection. I verified both claims against master by instrumenting real builds. The first claim is correct. The second is wrong as stated, but there is a real dev-startup asymmetry underneath it — pointing the other way. I also found a related bug in the Vite prebundled flag while checking.
Versions used: agoda-devfeedback-vite2@2.1.0, agoda-devfeedback-rsbuild@2.0.9, vite@5.4.14, @rsbuild/core@1.3.5, @rspack/core@1.3.4.
1. Production build time: confirmed, Vite systematically under-reports
The Vite plugin reports buildEnd - buildStart:
https://github.com/agoda-com/devfeedback-js/blob/master/packages/vite-plugin/src/lib/vite-build-stats-plugin.ts#L329
Rollup's buildEnd fires when the module graph is complete — before renderStart, renderChunk (minification), generateBundle, and writeBundle. So everything after the transform phase is excluded, even though the value is sent later from closeBundle.
I timed the hooks on a 301-module synthetic project:
| minifier |
reported (buildEnd - buildStart) |
actual (through closeBundle) |
excluded |
esbuild (Vite default) |
235 ms |
285 ms |
51 ms (18%) |
terser |
230 ms |
631 ms |
401 ms (64%) |
Hook timeline for the terser run, ms from buildStart:
0 buildStart
230 buildEnd <- reporting stops here
232 renderStart
624 generateBundle
626 writeBundle
630 closeBundle <- vite itself printed "built in 633ms"
Real apps will sit above the 18% floor, since CSS, sourcemaps, multiple chunks, and gzip-size reporting all land in the excluded phases.
Rsbuild has no equivalent gap. It uses stats.endTime - stats.startTime:
https://github.com/agoda-com/devfeedback-js/blob/master/packages/rspack-plugin/src/lib/rsbuild-stats-plugin.ts#L169-L171
Rspack stamps compilation.endTime after the native build() completes, which covers code generation, minification, and emitting assets. Verified in onAfterBuild: at the moment endTime was stamped, the output was already minified and already on disk.
stats.endTime - stats.startTime: 97 ms
wall clock to onAfterBuild: 150 ms
assets already written to disk when endTime was stamped: true ["index.588ce43e.js"]
output is minified: true (63185 bytes)
Net effect: for the same work, type: 'vite' covers transform only and type: 'rsbuild' covers the full pipeline. Any dashboard comparing them flatters Vite by a bundler-config-dependent margin.
Suggested fix
Measure the Vite production build through closeBundle, i.e. Date.now() - buildStart at report time rather than buildEnd - buildStart. If the transform/render split is worth keeping, send it as an extra field instead of narrowing the headline number.
2. Dev startup: the claim as written is incorrect, but the spans still aren't comparable
The claim was that Rsbuild reports the first full compilation while Vite reports no equivalent server-ready or first-app-ready event. Vite does report both, and has since #48:
The real problems are different, and one of them is a gap on the Rsbuild side:
(a) phase: 'devserver' means "socket is up" on both sides, and that is almost nothing on Vite. I measured Vite at 21–25 ms to listening, and Rsbuild's onAfterStartDevServer at 129 ms — which also fires before the first compile finishes, not after:
66 ms onBeforeStartDevServer
129 ms onAfterStartDevServer <- devserver event
162 ms startDevServer() resolved
217 ms onDevCompileDone (stats span = 87 ms) <- rsbuild build event
So the devserver phase is roughly like-for-like in definition, but it captures a meaningful share of Rsbuild's startup cost and essentially none of Vite's, because Vite defers all compilation until a request arrives. Comparing bundlers on devserver alone will read as a ~5x win for Vite that reflects nothing a developer waits for.
(b) Rsbuild reports the first full compilation; Vite structurally cannot. onDevCompileDone emits a type: 'rsbuild' build event for the first compile. Vite has no whole-app compile in dev, so there is no counterpart and there should not be one. The honest cross-bundler measure of "time until the app is usable" is clientready.
(c) clientready only exists as a first-class event on the Vite side, and the two time origins differ. Rsbuild pushes clientReady into devFeedback[] inside RspackBuildData rather than emitting a CommandBuildData with phase: 'clientready':
https://github.com/agoda-com/devfeedback-js/blob/master/packages/rspack-plugin/src/lib/rsbuild-stats-plugin.ts#L154
Worse, the reference points don't match even after you dig the value out:
- Vite
clientready timeTaken = Date.now() - serverStart, i.e. from dev-server start to browser idle.
- Rsbuild
clientReady elapsedMs = performance.now(), i.e. from page navigation start to browser idle — it excludes everything before the browser opened the page.
These two numbers cannot be compared, which leaves no usable apples-to-apples dev-startup metric today.
Suggested fix
- Emit
phase: 'clientready' from the Rsbuild plugin as a CommandBuildData, with timeTaken measured from devServerStart so it matches Vite's origin. Keep the raw performance.now() value as domContentLoadedMs / firstContentfulPaintMs detail rather than as the headline number.
- Treat
clientready as the canonical cross-bundler dev-startup metric and stop comparing devserver across bundlers, or document clearly that devserver is "socket up" and not a dev-startup number.
3. Bug found while verifying: Vite's prebundled flag can never report true
prebundled is computed by comparing node_modules/.vite/deps/_metadata.json mtime before config resolution against its mtime at listening:
https://github.com/agoda-com/devfeedback-js/blob/master/packages/vite-plugin/src/lib/vite-build-stats-plugin.ts#L147-L161
Dependency prebundling is request-triggered, so it hasn't run yet at listening. With no browser request at all, the metadata file was never written even after 15 s of waiting. With a request, it lands well after the span closed:
0 ms configureServer <- depsMetaMtimeBefore read here
21 ms httpServer listening <- prebundled computed here
69 ms browser requested / (index.html)
281 ms browser requested /src/main.js
333 ms deps/_metadata.json written <- prebundling actually finished
So on a warm cache both reads match and it reports false; on a cold cache both reads are undefined and it reports nothing. The comment in the code says prebundling is "usually the dominant cold-start cost", which is exactly the cost this flag is currently blind to.
Suggested fix
Sample the mtime later — on the clientready report, or after the first successful transform request — rather than at listening.
Repro
Each number above came from a small standalone probe rather than from the plugins themselves, to keep the measurement independent of the code under test:
- Vite production hooks: a plugin logging
Date.now() in buildStart, buildEnd, renderStart, generateBundle, writeBundle, closeBundle, on 300 generated modules, run once with minify: 'esbuild' and once with minify: 'terser'.
- Rsbuild production:
onAfterBuild reading stats.endTime - stats.startTime, then checking dist/static/js on disk and whether the emitted file is minified.
- Rsbuild dev: logging
onBeforeStartDevServer, onAfterStartDevServer, and onDevCompileDone.
- Vite dev: logging
configureServer and httpServer listening, then polling for node_modules/.vite/deps/_metadata.json, with and without an HTTP request to the server.
Happy to push the probes into the repo as a benchmarks/ or test fixture if that's useful for guarding the fix.
Feedback came in from a review of the current Vite and Rsbuild collectors, ahead of another round of data collection. I verified both claims against
masterby instrumenting real builds. The first claim is correct. The second is wrong as stated, but there is a real dev-startup asymmetry underneath it — pointing the other way. I also found a related bug in the Viteprebundledflag while checking.Versions used:
agoda-devfeedback-vite2@2.1.0,agoda-devfeedback-rsbuild@2.0.9,vite@5.4.14,@rsbuild/core@1.3.5,@rspack/core@1.3.4.1. Production build time: confirmed, Vite systematically under-reports
The Vite plugin reports
buildEnd - buildStart:https://github.com/agoda-com/devfeedback-js/blob/master/packages/vite-plugin/src/lib/vite-build-stats-plugin.ts#L329
Rollup's
buildEndfires when the module graph is complete — beforerenderStart,renderChunk(minification),generateBundle, andwriteBundle. So everything after the transform phase is excluded, even though the value is sent later fromcloseBundle.I timed the hooks on a 301-module synthetic project:
buildEnd - buildStart)closeBundle)esbuild(Vite default)terserHook timeline for the terser run, ms from
buildStart:Real apps will sit above the 18% floor, since CSS, sourcemaps, multiple chunks, and gzip-size reporting all land in the excluded phases.
Rsbuild has no equivalent gap. It uses
stats.endTime - stats.startTime:https://github.com/agoda-com/devfeedback-js/blob/master/packages/rspack-plugin/src/lib/rsbuild-stats-plugin.ts#L169-L171
Rspack stamps
compilation.endTimeafter the nativebuild()completes, which covers code generation, minification, and emitting assets. Verified inonAfterBuild: at the momentendTimewas stamped, the output was already minified and already on disk.Net effect: for the same work,
type: 'vite'covers transform only andtype: 'rsbuild'covers the full pipeline. Any dashboard comparing them flatters Vite by a bundler-config-dependent margin.Suggested fix
Measure the Vite production build through
closeBundle, i.e.Date.now() - buildStartat report time rather thanbuildEnd - buildStart. If the transform/render split is worth keeping, send it as an extra field instead of narrowing the headline number.2. Dev startup: the claim as written is incorrect, but the spans still aren't comparable
The claim was that Rsbuild reports the first full compilation while Vite reports no equivalent server-ready or first-app-ready event. Vite does report both, and has since #48:
phase: 'devserver'onhttpServerlistening— https://github.com/agoda-com/devfeedback-js/blob/master/packages/vite-plugin/src/lib/vite-build-stats-plugin.ts#L146-L165phase: 'clientready'from the browser idle callback, withdomContentLoadedMsandfirstContentfulPaintMs— https://github.com/agoda-com/devfeedback-js/blob/master/packages/vite-plugin/src/lib/vite-build-stats-plugin.ts#L253The real problems are different, and one of them is a gap on the Rsbuild side:
(a)
phase: 'devserver'means "socket is up" on both sides, and that is almost nothing on Vite. I measured Vite at 21–25 ms tolistening, and Rsbuild'sonAfterStartDevServerat 129 ms — which also fires before the first compile finishes, not after:So the
devserverphase is roughly like-for-like in definition, but it captures a meaningful share of Rsbuild's startup cost and essentially none of Vite's, because Vite defers all compilation until a request arrives. Comparing bundlers ondevserveralone will read as a ~5x win for Vite that reflects nothing a developer waits for.(b) Rsbuild reports the first full compilation; Vite structurally cannot.
onDevCompileDoneemits atype: 'rsbuild'build event for the first compile. Vite has no whole-app compile in dev, so there is no counterpart and there should not be one. The honest cross-bundler measure of "time until the app is usable" isclientready.(c)
clientreadyonly exists as a first-class event on the Vite side, and the two time origins differ. Rsbuild pushesclientReadyintodevFeedback[]insideRspackBuildDatarather than emitting aCommandBuildDatawithphase: 'clientready':https://github.com/agoda-com/devfeedback-js/blob/master/packages/rspack-plugin/src/lib/rsbuild-stats-plugin.ts#L154
Worse, the reference points don't match even after you dig the value out:
clientreadytimeTaken=Date.now() - serverStart, i.e. from dev-server start to browser idle.clientReadyelapsedMs=performance.now(), i.e. from page navigation start to browser idle — it excludes everything before the browser opened the page.These two numbers cannot be compared, which leaves no usable apples-to-apples dev-startup metric today.
Suggested fix
phase: 'clientready'from the Rsbuild plugin as aCommandBuildData, withtimeTakenmeasured fromdevServerStartso it matches Vite's origin. Keep the rawperformance.now()value asdomContentLoadedMs/firstContentfulPaintMsdetail rather than as the headline number.clientreadyas the canonical cross-bundler dev-startup metric and stop comparingdevserveracross bundlers, or document clearly thatdevserveris "socket up" and not a dev-startup number.3. Bug found while verifying: Vite's
prebundledflag can never reporttrueprebundledis computed by comparingnode_modules/.vite/deps/_metadata.jsonmtime before config resolution against its mtime atlistening:https://github.com/agoda-com/devfeedback-js/blob/master/packages/vite-plugin/src/lib/vite-build-stats-plugin.ts#L147-L161
Dependency prebundling is request-triggered, so it hasn't run yet at
listening. With no browser request at all, the metadata file was never written even after 15 s of waiting. With a request, it lands well after the span closed:So on a warm cache both reads match and it reports
false; on a cold cache both reads areundefinedand it reports nothing. The comment in the code says prebundling is "usually the dominant cold-start cost", which is exactly the cost this flag is currently blind to.Suggested fix
Sample the mtime later — on the
clientreadyreport, or after the first successful transform request — rather than atlistening.Repro
Each number above came from a small standalone probe rather than from the plugins themselves, to keep the measurement independent of the code under test:
Date.now()inbuildStart,buildEnd,renderStart,generateBundle,writeBundle,closeBundle, on 300 generated modules, run once withminify: 'esbuild'and once withminify: 'terser'.onAfterBuildreadingstats.endTime - stats.startTime, then checkingdist/static/json disk and whether the emitted file is minified.onBeforeStartDevServer,onAfterStartDevServer, andonDevCompileDone.configureServerandhttpServerlistening, then polling fornode_modules/.vite/deps/_metadata.json, with and without an HTTP request to the server.Happy to push the probes into the repo as a
benchmarks/or test fixture if that's useful for guarding the fix.