ui: Add rollup-plugin-visualizer option
diff --git a/ui/config/rollup.config.js b/ui/config/rollup.config.js
index 7eea8ad..1851f2d 100644
--- a/ui/config/rollup.config.js
+++ b/ui/config/rollup.config.js
@@ -25,6 +25,11 @@
 const OUT_SYMLINK = path.join(ROOT_DIR, 'ui/out');
 const NO_SOURCE_MAPS = process.env['NO_SOURCE_MAPS'] === 'true';
 const NO_TREESHAKE = process.env['NO_TREESHAKE'] === 'true';
+const BUNDLE_STATS = process.env['BUNDLE_STATS'] === 'true';
+
+const visualizer = BUNDLE_STATS
+  ? require('rollup-plugin-visualizer').visualizer
+  : null;
 
 // Plugin to embed minimal source maps directly into bundles
 function embedMinimalSourceMap() {
@@ -161,7 +166,9 @@
       // a minimal source map for error reporting. Skip both when source
       // maps are disabled.
       ...(NO_SOURCE_MAPS ? [] : [sourcemaps(), embedMinimalSourceMap()]),
-    ].concat(maybeUglify()),
+    ]
+    .concat(maybeUglify())
+    .concat(maybeVisualizer(bundle, distDir)),
     onwarn: function (warning, warn) {
       if (warning.code === 'CIRCULAR_DEPENDENCY') {
         // Ignore circular dependency warnings coming from third party code.
@@ -211,6 +218,22 @@
   return [terser(opts)];
 }
 
+// When BUNDLE_STATS=true, write an HTML treemap report next to each bundle
+// (e.g. dist_version/frontend_bundle.stats.html) with per-module + per-package
+// raw and gzipped sizes.
+function maybeVisualizer(bundle, distDir) {
+  if (!visualizer) return [];
+  return [
+    visualizer({
+      filename: `${OUT_SYMLINK}/${distDir}/${bundle}_bundle.stats.html`,
+      title: `${bundle} bundle stats`,
+      gzipSize: true,
+      brotliSize: true,
+      template: 'treemap',
+    }),
+  ];
+}
+
 const maybeBigtrace = process.env['ENABLE_BIGTRACE']
   ? [defBundle('tsc/bigtrace', 'bigtrace', 'dist_version/bigtrace')]
   : [];
diff --git a/ui/package.json b/ui/package.json
index 667fc2d..ba9cb47 100644
--- a/ui/package.json
+++ b/ui/package.json
@@ -76,6 +76,7 @@
     "rollup": "^4.60.1",
     "rollup-plugin-re": "^1.0.7",
     "rollup-plugin-sourcemaps2": "^0.5.6",
+    "rollup-plugin-visualizer": "^5.12.0",
     "sass": "^1.77.6",
     "source-map": "^0.7.6",
     "tslib": "^2.6.3",
diff --git a/ui/pnpm-lock.yaml b/ui/pnpm-lock.yaml
index 85cda4f..d14b9a7 100644
--- a/ui/pnpm-lock.yaml
+++ b/ui/pnpm-lock.yaml
@@ -208,6 +208,9 @@
   rollup-plugin-sourcemaps2:
     specifier: ^0.5.6
     version: 0.5.6(@types/node@20.14.9)(rollup@4.60.1)
+  rollup-plugin-visualizer:
+    specifier: ^5.12.0
+    version: 5.14.0(rollup@4.60.1)
   sass:
     specifier: ^1.77.6
     version: 1.77.6
@@ -2925,6 +2928,11 @@
     engines: {node: '>=0.10.0'}
     dev: true
 
+  /define-lazy-prop@2.0.0:
+    resolution: {integrity: sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==}
+    engines: {node: '>=8'}
+    dev: true
+
   /degenerator@5.0.1:
     resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==}
     engines: {node: '>= 14'}
@@ -4018,6 +4026,12 @@
       has: 1.0.3
     dev: true
 
+  /is-docker@2.2.1:
+    resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
+    engines: {node: '>=8'}
+    hasBin: true
+    dev: true
+
   /is-extglob@2.1.1:
     resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==}
     engines: {node: '>=0.10.0'}
@@ -4090,6 +4104,13 @@
       has-tostringtag: 1.0.0
     dev: false
 
+  /is-wsl@2.2.0:
+    resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
+    engines: {node: '>=8'}
+    dependencies:
+      is-docker: 2.2.1
+    dev: true
+
   /isexe@2.0.0:
     resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==}
 
@@ -5111,6 +5132,15 @@
       mimic-fn: 2.1.0
     dev: true
 
+  /open@8.4.2:
+    resolution: {integrity: sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==}
+    engines: {node: '>=12'}
+    dependencies:
+      define-lazy-prop: 2.0.0
+      is-docker: 2.2.1
+      is-wsl: 2.2.0
+    dev: true
+
   /optionator@0.8.3:
     resolution: {integrity: sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==}
     engines: {node: '>= 0.8.0'}
@@ -5613,6 +5643,26 @@
       rollup: 4.60.1
     dev: true
 
+  /rollup-plugin-visualizer@5.14.0(rollup@4.60.1):
+    resolution: {integrity: sha512-VlDXneTDaKsHIw8yzJAFWtrzguoJ/LnQ+lMpoVfYJ3jJF4Ihe5oYLAqLklIK/35lgUY+1yEzCkHyZ1j4A5w5fA==}
+    engines: {node: '>=18'}
+    hasBin: true
+    peerDependencies:
+      rolldown: 1.x
+      rollup: 2.x || 3.x || 4.x
+    peerDependenciesMeta:
+      rolldown:
+        optional: true
+      rollup:
+        optional: true
+    dependencies:
+      open: 8.4.2
+      picomatch: 4.0.4
+      rollup: 4.60.1
+      source-map: 0.7.6
+      yargs: 17.7.2
+    dev: true
+
   /rollup-pluginutils@2.8.2:
     resolution: {integrity: sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ==}
     dependencies: