Skip to content

perf(ui): Shrink the consumer plugin's render path and check the plugins in CI - #606

Merged
kevwilliams merged 6 commits into
mainfrom
perf/consumer-plugin-render-path
Nov 4, 2026
Merged

kevwilliams merged 6 commits into
mainfrom
perf/consumer-plugin-render-path

Conversation

@kevwilliams

Copy link
Copy Markdown
Contributor

Summary

Cuts what the cloud portal downloads to render the compute consumer plugin by about two thirds per page, aligns the plugin's dependencies with the host, and adds the CI checks the plugins never had.

  • Minify the bundle, with source maps. minify: false shipped every chunk unminified. Source maps keep it readable in devtools.
  • Drop zod. schema.ts built Zod schemas that nothing parsed with — adapter.ts builds every value and only the inferred types were used. They are now plain interfaces of the same shape, which takes zod (~360KB unminified) out of the chunk every page loads.
  • Conditional requests in serve.ts. The portal's asset proxy serves remoteEntry.js and the manifests as no-cache and forwards the browser's validators, but serve.ts sent none, so every page load downloaded them in full. It now sends a weak ETag and Last-Modified and answers a GET/HEAD match with 304. serve.ts is now part of the plugin's typecheck.
  • Dependencies aligned with cloud-portal. Packages the host provides at runtime are pinned to the versions cloud-portal resolves (datum-ui 3.6.0, react-query 5.102.8; react 19.3.0 and react-router 8.3.1 already matched), so the plugin type-checks against the APIs that actually run. Patch and minor updates elsewhere; motion stays below 14 (datum-ui's peer range).
  • CI. New UI Plugins workflow typechecks ui/consumer and ui/provider and runs the consumer's unit tests on changes under ui/. Until now publish.yaml only built the images.
  • README. Fixes the stale claim that datum-ui isn't shared, documents the version-pinning rule and caching, and updates the file layout.

First-load JS per page

Sync chunks per exposed module, from mf-manifest.json (an upper bound: it includes fallback copies of host-shared packages, which the portal doesn't load).

Exposed module Before After
WorkloadList 1,992 KB 680 KB
WorkloadDetail 3,154 KB 1,102 KB
InstanceDetail 3,123 KB 1,086 KB
InstanceShellWindow 1,238 KB 602 KB
WorkloadsHomeColumn 583 KB 146 KB
remoteEntry.js 77 KB, refetched every load 23 KB, 304 on revalidation
All JS, gzipped 1,335 KB 748 KB

Verification

  • npm run typecheck, npm test (21 files, 353 tests) and npm run build pass in ui/consumer.
  • The new workflow's steps (npm ci --ignore-scripts, typecheck, tests) pass on clean copies of ui/consumer and ui/provider without node_modules; the workflow passes a YAML lint.
  • serve.ts exercised over TLS against the built dist/: a matching If-None-Match returns 304 with no body, a stale one returns 200, a stale If-None-Match with a valid If-Modified-Since returns 200, If-Modified-Since alone returns 304, HEAD returns 200 (and 304 on a match), a conditional POST gets no 304, and path traversal still returns 404.
  • An independent review compared the old Zod-inferred types with the new interfaces under tsc (assignable both ways, same optionality), confirmed zod is absent from the build output, and confirmed the shared packages match cloud-portal's resolved versions.
  • Source maps sit under the proxy's year-long immutable caching; a source change that minifies to identical output keeps the chunk's name, so devtools could show a stale map. Runtime is unaffected.
  • Not yet loaded in a running cloud-portal.

Follow-ups

  • Share the rest of the datum-ui subpaths cloud-portal already provides (the plugin imports ~27 and shares 8), and lazy-load or move the charts off recharts, which currently lands in the chunk the workload detail page loads first.
  • Move both plugins to pnpm; the new workflow then only changes its install and cache steps.
  • Vite 8 and @vitejs/plugin-react 6.

Nothing parsed with the Zod schemas; adapter.ts builds every value and
only the inferred types were used. Declaring them as plain types keeps
zod (~360KB unminified) out of the chunk every page loads.
Pin datum-ui (3.6.0) and react-query (5.102.8) to the versions
cloud-portal resolves: the host's copies are what run, so the local
ones should type-check against the same APIs. Take patch and minor
updates elsewhere, keep motion below 14 (datum-ui's peer range), and
drop zod, which nothing imports now.
The portal downloads and parses every page's chunks on first visit.
Minifying cuts the bundle from ~6.0MB to ~2.5MB (~1.3MB to ~0.75MB
gzipped); source maps keep it readable in devtools.
The portal's asset proxy serves remoteEntry.js and the manifests as
no-cache and forwards the browser's validators, but serve.ts sent none,
so every page load downloaded them again. Send a weak ETag and
Last-Modified and answer a match with 304.
Describe the datum-ui subpaths shared from the host, the rule for
pinning host-provided packages, serving and caching, and the current
file layout.
publish.yaml only builds the plugin images, so neither plugin's
typecheck nor the consumer's unit tests ever ran in CI.
@kevwilliams
kevwilliams requested a review from a team as a code owner November 3, 2026 21:27
@kevwilliams kevwilliams self-assigned this Nov 3, 2026

@ecv ecv left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve.

Minification, source maps, ETag support and the Zod removal are safe at the head, and the module-federation sharing is unchanged by this PR.

  • shared in ui/consumer/vite.config.ts is untouched: react, react-dom, react-router and react-query stay singletons with host-major requiredVersion, and the datum-ui subpaths stay requiredVersion: false, so the host's copy wins and no second React loads. The bumps (datum-ui 3.6.0, react-query 5.102.8) only change the local install; I could not confirm them against cloud-portal's lockfile from here.
  • schema.ts keeps every field of both resources. .default([]) becomes a required array and deleting a required boolean, which is safe only because adapter.ts builds every value, and tsc --noEmit passes in the new job. No remaining zod or *Schema imports anywhere in ui/consumer/src.
  • serve.ts adds only ETag/Last-Modified and 304 handling; no CORS or other header changes. .map was already in the MIME table, so maps are served from dist/ to anyone who can reach the portal's asset proxy. ui/provider already ships sourcemap: true, so this matches existing practice; just confirm that exposing readable source is intended for the consumer plugin.
  • ui-plugins.yml runs npm ci, typecheck and (consumer only) vitest for both plugins, with least-privilege permissions: contents: read. Actions are pinned to major tags (actions/checkout@v7, setup-node@v7), consistent with the repo's other workflows rather than SHAs.

Follow-ups:

  • The new workflow typechecks but never runs vite build. The build (and so the minify and module-federation chunking change) is only exercised by the image publish jobs, so a build-only break is caught after the fact. Adding npm run build to the matrix would close that.
  • Minifying a module-federation remote changes chunk shapes, so check the portal in staging with a real plugin load (not just a green build) before it reaches production.

@kevwilliams
kevwilliams merged commit ca870aa into main Nov 4, 2026
14 checks passed
@kevwilliams
kevwilliams deleted the perf/consumer-plugin-render-path branch November 4, 2026 00:17
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.

2 participants