Skip to content

fix: make Vite and Rsbuild build-time metrics apples-to-apples - #54

Merged
joeldickson merged 4 commits into
masterfrom
fix/apples-to-apples-build-metrics
Aug 7, 2026
Merged

fix: make Vite and Rsbuild build-time metrics apples-to-apples#54
joeldickson merged 4 commits into
masterfrom
fix/apples-to-apples-build-metrics

Conversation

@joeldickson

Copy link
Copy Markdown
Contributor

Fixes #53. Also undoes the #51 workaround, as agreed.

Undoing the #51 workaround (first commit hunk)

packages/rspack-plugin/package.json was pinned to the published 2.0.9 so changesets would skip it while NPM_TOKEN returned E403. The token is fixed — the master run at 31151648453 published common@2.1.0, vite2@2.1.0 and webpack@2.0.10 — and the only thing the pin still does is keep agoda-devfeedback-rsbuild off npm while its CHANGELOG already claims 2.1.0.

Restored to 2.1.0. Note it will land on npm as 2.2.0, since the changeset in this PR bumps it before it ever gets published; 2.1.0 is skipped on the registry.

1. Vite production builds under-report

The reported timeTaken was buildEnd - buildStart. Rollup's buildEnd fires when the module graph is complete — before renderStart, renderChunk, generateBundle and writeBundle — so everything after transform was excluded, even though the value is sent later from closeBundle. Rsbuild's stats.endTime - stats.startTime already covered the full pipeline, so type: 'vite' and type: 'rsbuild' were never measuring the same span.

Now measured through closeBundle (Date.now() - buildStart at report time). The transform phase is kept as a new transformTimeMs field on ViteBuildData rather than being narrowed into the headline number.

2. Rsbuild clientready

clientReady was only ever an entry inside devFeedback[] on the RspackBuildData payload, timed with performance.now() — i.e. from page navigation, excluding everything before the browser opened the page. Vite's phase: 'clientready' runs from dev server start. The two numbers could not be compared even after digging the value out.

The Rsbuild plugin now emits a CommandBuildData with phase: 'clientready', timeTaken measured from devServerStart, and the browser-relative values attached as domContentLoadedMs / firstContentfulPaintMs. Reported once per dev server run — a page reload is not a new start.

The client script now sends one consolidated message instead of three, but the server still pushes the same three clientReady / domContentLoaded / firstContentfulPaint entries into devFeedback[], so nothing that consumes the existing payload changes shape.

Did not touch the devserver phase or add a Vite counterpart to Rsbuild's first full dev compile — per the issue, Vite has no whole-app compile in dev and should not grow a fake one. Documented in the README instead that clientready is the cross-bundler dev-startup metric and devserver ("socket is up") is not comparable across bundlers.

3. Vite prebundled could never be true

The flag compared the mtime of node_modules/.vite/deps/_metadata.json before config resolution against its mtime at listening. Prebundling is request-triggered, so at listening it has not run: warm cache reported false, cold cache reported nothing.

Sampled on the clientready report instead, which moves the field from the devserver event to the clientready event. Cases where the browser never connects now report nothing rather than a confident false.

Tests

Two new tests, both verified to fail when the corresponding fix is reverted:

  • reports client ready from dev server start, exactly once per run — connects a real WebSocket client to the plugin's server through the injected script's port, sends the message twice, and asserts one command event plus unchanged devFeedback[] entries.
  • reports prebundling on client ready, not on the dev server event — cold metadata at startup, present at client ready.

The three existing Vite build tests now mock a third Date.now() (buildStart 0, buildEnd 100, closeBundle 150) and assert timeTaken 150 with transformTimeMs 100.

pnpm test and pnpm check-types pass. The two pre-existing Prettier warnings (README.md, packages/vite-plugin/src/index.ts) are on master already and left alone.

🤖 Generated with Claude Code

dicko2 and others added 4 commits August 7, 2026 12:56
Vite production builds reported `buildEnd - buildStart`. Rollup's buildEnd
fires when the module graph is complete, before renderChunk, generateBundle
and writeBundle, so minification and emitting assets were excluded entirely
— 18% of the build under esbuild, 64% under terser. Rsbuild's
`stats.endTime - stats.startTime` already covered the full pipeline, so any
chart comparing the two flattered Vite. Measure through closeBundle, and
keep the transform phase as a new `transformTimeMs` field rather than as the
headline number.

Rsbuild's clientReady existed only as an entry in `devFeedback[]`, timed with
performance.now() — from page navigation, excluding everything before the
browser opened the page. Vite's `clientready` runs from dev server start.
Emit clientready as a first-class command event on Vite's origin; the
browser-relative values stay in `devFeedback[]` and ride along as
domContentLoadedMs / firstContentfulPaintMs.

Vite's `prebundled` flag sampled the deps metadata mtime at `listening`, but
prebundling is triggered by the first browser request and has not run by
then — it reported false on a warm cache and nothing on a cold one, never
observing the cost it exists to measure. Sample it on the clientready report
instead, where it can actually be true.

Also restores agoda-devfeedback-rsbuild to its real version. It was pinned to
the published 2.0.9 so changesets would skip it while NPM_TOKEN returned E403
(#51); the token is fixed and the other packages published successfully.

Fixes #53

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The repo had no contributor-facing docs for changesets at all, so the
two-merge release flow (PR -> Version Packages PR -> npm) was folklore, and
the generated `adjective-noun-verb` filenames read as if they meant
something. They don't — human-id picks them to avoid filename collisions.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
It explains why any of these metrics are collected, so it belongs before the
bundler setup instructions rather than below them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ratchapol-an

Copy link
Copy Markdown
Collaborator

So a Vite number could look much faster simply because it stopped the clock earlier, while Rsbuild’s clock kept running through the actual end of the build.

PR #54 makes the comparisons apple-to-apple:

  • Production builds: Vite now measures from build start until closeBundle, after rendering, minification, and file output finish—matching Rsbuild’s full-pipeline span. The old transform-only duration remains available separately as transformTimeMs.

  • Dev clientready: Rsbuild now emits a first-class clientready command event timed from dev-server start, just like Vite. Browser-only timings (DOMContentLoaded/FCP) remain as details, rather than being used as the headline duration.

  • Vite prebundling: the prebundled flag is now checked at client-ready, after the first browser request can actually trigger prebundling. Previously it was checked when the server began listening—too early—so it could never reliably report true.

In short: the headline metrics now answer the same question:

  • Build: “How long from starting the build until the build is truly finished?”

  • Dev startup: “How long from starting the dev server until the app is ready in the browser?”

@joeldickson
joeldickson merged commit 83413f6 into master Aug 7, 2026
4 checks passed
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.

Vite and Rsbuild build-time metrics are not apples-to-apples

3 participants