RFC-0024: UI build migration to Vite #5600
Replies: 2 comments 1 reply
-
|
I'm massively in favour of switching to vite + the plan looks good. I definitely think we should just bite the bullet and convert to vite entirely rather than any of the alternatives. One thing - you say we "retire embedMinimalSourceMap plugin entirely" - surely we still need this, we just need to port it to vite (trivial). |
Beta Was this translation helpful? Give feedback.
-
|
Having hacked a bit on the UI this weekend, hugely in favor of this. This is a signifcant improvement to developer productivity and we should get it in ASAP. We should also consider for dev builds testing with Typescript 7.0 optionally (https://devblogs.microsoft.com/typescript/announcing-typescript-7-0-beta/) which will further speed up the build. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
📄 RFC Doc: 0024-ui-vite-migration.md
UI build migration to Vite
Authors: @primiano
Status: Proposal
POC branch: dev/primiano/vite
Problem
The Perfetto UI build is driven by a hand-rolled
ui/build.js(1177 LoC)that orchestrates
rollup,tsc,sass,pbjs/pbts, ninja, and a customHTTP server.
Pain points:
ui/src/frontend/sidebar.tstriggers a
tscrecompile (incremental, OK) followed by a rollupre-bundle of the whole frontend, ~3–5s per cycle once warm.
copying, watch debouncing, build-lock, signal handling, all hand-rolled
on top of node
http,fs.watch, andchild_process.Opportunities and risks
Opportunities:
Move over to industry standard. Vite seems to have caught a lot of traction
lately. It solves the problems we care about and has a large ecosystem.
The speed up on dev cycle is incredible, it retains tsc debug output for
type-related issues (altough does it asynchronously).
More importantly works better with AI agents because there is no need to
"wait for build", as the synchronization as transpilation happens on-demand
when requesting a module from the HTTP server.
It improves massively the dev workflow experience, as it doesn't require any
bundling at all for dev.
Risks:
There is a lot of complexity behind Vite, as it uses a miture of rollup
bundles for production but for dev server, it pushes ES modules down to the
browser and transpiles them on demand. Although this complexity is supposed to
be dealt with by Vite.
Realistically we are going to spend probably a few weeks shaking bugs here and
there that will unavoidably come up.
Proposal
Replace the entire
ui/build.js+ rollup orchestration with Vite:ui/build): onevite.build()per IIFE bundle.Same output layout as before; same
manifest.json+ duplicatedindex.htmlpost-build steps. Still bundles with rollup.No behavioral change at the user/serving layer.
ui/run-dev-server): use Vite's dev server for thefrontend (native ES modules, esbuild per-file transforms, browser
refresh in ~2s), and
vite.build()for the worker / SW / chromeextension bundles (one-shot for the rarely-edited ones,
watch:{}forthe small ones we sometimes touch).
Hand-rolled bits we retire entirely:
/live_reloadSSE, mtime 304s, gzip middleware(Vite dev server replaces them).
tsc --watchas a JS emitter — Vite/esbuild transform.tsdirectly.re,sourcemaps2, our ownembedMinimalSourceMap).We keep:
pbjs/pbtsinvocations (with one option flip:-w es6so the devserver can serve the generated
protos.jsas a native module).tools/gen_ui_imports,tools/write_version_header.py,tools/gen_stdlib_docs_json.py— invoked by the new orchestrator.tools/gn+tools/ninjafor wasm. Output staging unchanged.manifest.json+ duplicatedindex.htmlmechanism (required bythe offline service worker).
ui/src/assets/index.html— atiny middleware in dev rewrites one line of it on the wire to load
the frontend as a module.
Design
Repository layout
ui/src/gen/is now a real directory (no longer a symlink) and receivesall generated files (codegen + Emscripten glue) directly. This sidesteps
a class of
pnpmsymlink interactions with@rollup/plugin-commonjsthat broke ajv resolution under
preserveSymlinks: true.Production pipeline (
scripts/build.mjs)PERFETTO_UI_*,~/.config/perfetto/ui-dev-server.env).CLI flags always win; precedence is documented in the parser.
<outDir>/ui/for a deterministic build; recreate the layout.ui/src/gen/protos.{js,d.ts}; version header;plugin import index; stdlib docs JSON.
gn gen+ninja <mod>_wasmfor each module, thenstage
.wasmintodist/<ver>/and.{js,d.ts}glue intoui/src/gen/.ui/src/assets/index.htmlintodist/<ver>/index.htmlverbatim.ui/src/assets/perfetto.scss→dist/<ver>/perfetto.cssvia the
sassCLI.vite.build()call per bundle — sequentially, sharing theoutput dir via
emptyOutDir: false. Output is forced to<name>_bundle.js(no hashing) so the existing service-workersubresource-integrity scheme keeps working.
dist/<ver>/, sha256 each non-.map/non-index.htmlfile, write
manifest.json. Duplicatedist/<ver>/index.htmltodist/index.htmlwithdata-perfetto_versionpatched to a single-channel JSON map (
{"stable":"<ver>"}) and optional<title>override.
The output tree is byte-equivalent to what the old rollup pipeline
produced (modulo bundle minifier output differences); the service worker
needs no changes.
Dev pipeline (
scripts/dev.mjs)When a worker bundle changes on disk a Vite
ws.send({type:'full-reload'})is pushed so the browser refreshes automatically.
Edit-to-refresh budget on a warm cache (Linux, Node 20):
.ts, hit reloadengine/index.ts, watcher signalsHTML transform
The production bootstrap in
ui/src/assets/index.htmlends withroughly:
In dev, the dev server's middleware reads this same file and rewrites
just that line:
The error handler, the timeout, the CSS/font preloads, the
data-perfetto_versionchannel map — all run unchanged. The middlewarealso injects a single inline script in
<head>settingwindow.__PERFETTO_ASSET_ROOT__ = '/v<ver>/'so thatassetSrc()(which historically derived the asset root from
document.currentScript.src) still works when the frontend is loadedas a module (where
document.currentScriptisnull). Theproduction HTML is not modified on disk.
The only source-side change required is in
ui/src/base/http_utils.ts::getServingRoot()— five lines that consultthe global before falling back to the existing
currentScript-basedlogic.
Custom Vite plugins (
ui/vite/plugins.mjs)pluginProtoFixup(build-only safety net): replaceseval(...moduleName);withundefined;in any.jswe transform,and the
process.env.NODE_ENVreferences that immer expects. Thisused to be
rollup-plugin-re; same intent, more focused.pluginTraceProcessor32Alias: when the 32-bittrace_processorglue exists in
ui/src/gen/, redirect./trace_processor_32_stubimports to it; otherwise leave the stub in place (which throws at
runtime if the user's browser doesn't support memory64).
pluginLezerGrammarAlias:import {parser} from './foo.grammar';is rewritten to
./foo.grammar.js. Necessary because Vite resolves./foo.grammarto the literal text-format grammar file, then chokesparsing it as JS.
pluginEmscriptenGlueToEsm(dev-server only): theMODULARIZE=1Emscripten glue ends with a CommonJS/AMD exporttrailer (
module.exports = X). Invite buildthe rollupcommonjs plugin handles it; in dev-serve there is no such plugin in
the hot path, so we append
export default <name>_wasm;to theserved file. The trailing CJS/AMD branch becomes dead code.
tsconfig change
tsconfig.base.jsonswitched:module: commonjs→module: esnextmoduleResolution: node→moduleResolution: bundlertscis no longer used for emit (Vite/esbuild do the transforms);these settings only affect type-check resolution, which is now aligned
with what the bundlers actually do. Two minor source fixups in
com.google.PerfettoMcp/{tracetools,uitools}.tsadd explicit.jssuffixes to the MCP SDK imports as the
bundlerresolver requires.Why
vite-plugin-checkerIn dev mode Vite only transforms files that the browser actually
imports. A type error in an unreached file (a disabled plugin, a new
file not yet wired up) would silently sit there. The plugin runs
tsc --noEmitover the whole project in a worker, surfaces errorsboth in the terminal and as a click-through browser overlay. Verified
end-to-end by adding a synthetic
_test_checker_error.tscontainingconst x: number = "string";— the dev server logged the TS2322 eventhough no file imported it.
Alternatives considered
A. Stay on rollup, retrofit watch-mode
Pros: smallest change.
Cons: doesn't address the "edit a frontend file → ~5s rebuild" pain.
Modern alternatives (Vite, rspack, esbuild) were designed exactly to
fix this; sticking with rollup leaves us with the slow path forever.
B.
vite build --watchfor everythingThis was the original attempt. It failed: editing the frontend
.tstriggered[commonjs] Cannot read properties of undefined (reading 'resolved')from@rollup/plugin-commonjs@28's rebuild-cachepath on the second build, leaving the watcher wedged. We tried two
escalations:
vite build --watchas a subprocess per bundle and respawn onthat specific error code (worked but every frontend edit took ~21 s
for a full cold rebuild — unusable).
meta.commonjs.resolvedaccess with a?.guard inline innode_modules/.vite/.../dep-XXXX.jsand@rollup/plugin-commonjs,reapplied via a
postinstallhook (worked, no more crashes, butthe rebuild stayed at ~15 s because rollup was still re-bundling
the whole frontend on every edit).
Both approaches were rejected for the chosen design (dev-server +
selective watch). The patches and the per-bundle subprocess scaffolding
have been removed.
C. Webpack / rspack / turbopack
Pros: also have fast dev modes.
Cons: bigger config surfaces, less idiomatic for SPA + classic-script
workers.
Also the ecosystem seems to be shifting in favor of Vite.
D. ESBuild directly (no bundler framework on top)
Pros: fastest possible builds.
Cons:
extractor + dep-pre-bundling all over again. That's exactly what we
left behind.
Implementation notes / non-goals
frontenddoesnew Worker(assetSrc('engine_bundle.js'))(no{type: 'module'}).The engine bundle is built as IIFE in both prod and dev. Switching
to module workers would simplify some things but require source
changes and has its own caveats (some Emscripten module-worker bugs
in the past).
.tssource unmodified to the browser, so devtools can map line/colnatively without us emitting
.mapfiles. This kept dev rebuildtimes tight; can be re-enabled per-bundle if desired.
the old
embedMinimalSourceMapmechanism (a compact map registry onself.__SOURCEMAPS[<bundleName>], consumed byui/src/base/source_map_utils.tsfor stack-trace decoration onerror reports) was not ported. The runtime tolerates the
registry being absent. Re-adding it as a Vite
generateBundleplugin is left as a TODO that should be addressed.
(
server.ws.send({type:'full-reload'})) rather than the old SSEchannel. The frontend's
core/live_reload.tswas deleted alongwith its callers; the dev server still serves a no-op SSE stub at
/live_reloadfor any out-of-tree consumer that pings it.Open questions / follow-ups
isolatedModules: trueintsconfig.base.json. Would surfacethe handful of mixed value/type re-exports left in the codebase
(
histogram.ts,track_helper.ts). Currently both prod and devbuild clean without it; it's a tightening choice, not a correctness
one.
prod sizes? Bring back the embedded minimal-map registry?
bigtraceandopen_perfetto_tracesmoke tests in a realbrowser. Plumbed through both pipelines but not visually verified
post-migration.
💬 Discussion Guidelines:
Beta Was this translation helpful? Give feedback.
All reactions