From 29e52fb989c939e22b5c49325910c78230f449cd Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Wed, 17 Jun 2026 14:01:54 -0700 Subject: [PATCH 1/5] Split the bloch sphere out and lazy load it --- source/npm/qsharp/package.json | 1 + source/npm/qsharp/ux/index.ts | 1 - source/playground/src/main.tsx | 3 ++- source/vscode/build.mjs | 25 ++++++++++++++++++++++--- source/vscode/src/webview/webview.tsx | 15 +++++++++++++-- source/vscode/src/webviewPanel.ts | 2 +- 6 files changed, 39 insertions(+), 8 deletions(-) diff --git a/source/npm/qsharp/package.json b/source/npm/qsharp/package.json index 46ba3ee1dc7..694103e951b 100644 --- a/source/npm/qsharp/package.json +++ b/source/npm/qsharp/package.json @@ -24,6 +24,7 @@ "./katas-md": "./dist/katas-md.js", "./state-viz": "./ux/circuit-vis/state-viz/worker/index.ts", "./ux": "./ux/index.ts", + "./ux/bloch": "./ux/bloch.tsx", "./qdk-theme.css": "./ux/qdk-theme.css", "./rz-array.json": "./rz-array.json" }, diff --git a/source/npm/qsharp/ux/index.ts b/source/npm/qsharp/ux/index.ts index fc22331e90f..dfdb3b89c22 100644 --- a/source/npm/qsharp/ux/index.ts +++ b/source/npm/qsharp/ux/index.ts @@ -20,7 +20,6 @@ export { SpaceChart } from "./spaceChart.js"; export { ScatterChart } from "./scatterChart.js"; export { EstimatesOverview } from "./estimatesOverview.js"; export { EstimatesPanel } from "./estimatesPanel.js"; -export { BlochSphere } from "./bloch.js"; export { Circuit, CircuitPanel } from "./circuit.js"; export { setRenderer, Markdown } from "./renderers.js"; export { Atoms, type ZoneLayout, type TraceData } from "./atoms/index.js"; diff --git a/source/playground/src/main.tsx b/source/playground/src/main.tsx index 804fa55c5ee..41a8362fff4 100644 --- a/source/playground/src/main.tsx +++ b/source/playground/src/main.tsx @@ -49,7 +49,8 @@ import { // Set up the Markdown renderer with KaTeX support import mk from "@vscode/markdown-it-katex"; import markdownIt from "markdown-it"; -import { setRenderer, BlochSphere } from "qsharp-lang/ux"; +import { setRenderer } from "qsharp-lang/ux"; +import { BlochSphere } from "qsharp-lang/ux/bloch"; const md = markdownIt("commonmark"); md.use((mk as any).default, { diff --git a/source/vscode/build.mjs b/source/vscode/build.mjs index 7fee3349eed..741f06b3e15 100644 --- a/source/vscode/build.mjs +++ b/source/vscode/build.mjs @@ -32,7 +32,6 @@ const platformBuildOptions = { outbase: join(thisDir, "src"), outdir: join(thisDir, "out"), entryPoints: [ - join(thisDir, "src", "webview/webview.tsx"), join(thisDir, "src", "webview/editor.tsx"), join(thisDir, "src", "learning/webview/webview-client.tsx"), ], @@ -42,6 +41,25 @@ const platformBuildOptions = { }, // plugins added at build time (needs inlineStateComputeWorkerPlugin) }, + // The main webview bundle is built as ESM with code splitting enabled so + // that heavy, rarely-used dependencies (e.g. three.js used only by the + // Bloch sphere) are emitted as separate chunks that are loaded on demand + // via dynamic import(), rather than bloating the shared webview.js. + webview: { + ...commonBuildOptions, + format: "esm", + splitting: true, + platform: "browser", + outbase: join(thisDir, "src"), + outdir: join(thisDir, "out"), + chunkNames: "webview/chunks/[name]-[hash]", + entryPoints: [join(thisDir, "src", "webview/webview.tsx")], + define: { + "import.meta.url": "undefined", + __PLATFORM__: JSON.stringify("browser"), + }, + // plugins added at build time (needs inlineStateComputeWorkerPlugin) + }, browser: { ...commonBuildOptions, entryPoints: [ @@ -209,8 +227,8 @@ async function buildPlatform(platform) { const options = platformBuildOptions[platform]; if (!options) throw new Error(`Invalid platform: ${platform}`); - // UI build needs the inline worker plugin - if (platform === "ui") { + // UI builds need the inline worker plugin + if (platform === "ui" || platform === "webview") { options.plugins = [inlineStateComputeWorkerPlugin]; } @@ -282,6 +300,7 @@ export async function watchVsCode() { await Promise.all([ buildPlatform("ui"), + buildPlatform("webview"), buildPlatform("browser"), buildPlatform("node"), buildPlatform("node-worker"), diff --git a/source/vscode/src/webview/webview.tsx b/source/vscode/src/webview/webview.tsx index 7f7f0d0f615..aba03cf0143 100644 --- a/source/vscode/src/webview/webview.tsx +++ b/source/vscode/src/webview/webview.tsx @@ -6,12 +6,12 @@ const vscodeApi = acquireVsCodeApi(); import { render } from "preact"; +import { lazy, Suspense } from "preact/compat"; import { CircuitPanel, CircuitProps, EstimatesPanel, Histogram, - BlochSphere, setRenderer, detectThemeChange, updateStyleSheetTheme, @@ -21,6 +21,13 @@ import { HelpPage } from "./help"; import { DocumentationView, IDocFile } from "./docview"; import "./webview.css"; +// The Bloch sphere pulls in three.js, which is large and only needed when a +// Bloch sphere view is actually opened. Load it lazily via a dynamic import so +// esbuild emits it (and three.js) as a separate chunk kept out of webview.js. +const BlochSphere = lazy(() => + import("qsharp-lang/ux/bloch").then((m) => ({ default: m.BlochSphere })), +); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - there are no types for this import mk from "@vscode/markdown-it-katex"; @@ -234,7 +241,11 @@ function App({ state }: { state: State }) { case "help": return ; case "bloch": - return ; + return ( + Loading...}> + + + ); case "documentation": // Ideally we'd have this on all web views, but it makes the font a little // too large in the others right now. Something to unify later. diff --git a/source/vscode/src/webviewPanel.ts b/source/vscode/src/webviewPanel.ts index 95d5d41ddb4..afbde4d6bdf 100644 --- a/source/vscode/src/webviewPanel.ts +++ b/source/vscode/src/webviewPanel.ts @@ -357,7 +357,7 @@ export class QSharpWebViewPanel { - + From 21c4ee0af4338d80e85f5b60130294172b1bd438 Mon Sep 17 00:00:00 2001 From: Scott Carda Date: Wed, 17 Jun 2026 14:02:12 -0700 Subject: [PATCH 2/5] do the same for molecule viewer --- docs/bundle-size-optimization.md | 166 +++++++++++++++++++++++++++++++ source/npm/qsharp/package.json | 1 + source/npm/qsharp/ux/index.ts | 1 - source/widgets/js/index.tsx | 2 +- 4 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 docs/bundle-size-optimization.md diff --git a/docs/bundle-size-optimization.md b/docs/bundle-size-optimization.md new file mode 100644 index 00000000000..22209669cdf --- /dev/null +++ b/docs/bundle-size-optimization.md @@ -0,0 +1,166 @@ +# Bundle Size Optimization — Bloch Sphere PR + +## Background + +The VS Code extension ships several JavaScript bundles into its webviews. +The main one, `webview.js`, is loaded eagerly for **every** Q# output panel — +histograms, circuit diagrams, resource estimates, documentation, and so on. + +When the Bloch sphere widget was added it brought in +[three.js](https://threejs.org/) (the 3D rendering library) as a dependency. +Because of how the TypeScript barrel export worked, `three.js` was being +bundled into `webview.js` even for panels that never show a Bloch sphere. +This caused `webview.js` to grow from ~3 MB to ~4.4 MB — a concern raised +during review. + +A second library, [3Dmol](https://3dmol.org/) (used by the chemistry +`MoleculeViewer` widget), had the same problem and was also being bundled +into `webview.js` despite never being used in the VS Code webview at all. + +--- + +## What Changed + +### 1. The root cause: static barrel exports + +`qsharp-lang/ux` has a single barrel file (`ux/index.ts`) that re-exports +everything: + +```ts +// Before +export { BlochSphere } from "./bloch.js"; // pulls in three.js +export { MoleculeViewer } from "./chem/index.js"; // pulls in 3Dmol +``` + +Both `webview.tsx` (all panels) and `editor.tsx` (circuit editor) import +from this barrel. Even though neither file actually uses `BlochSphere` or +`MoleculeViewer` at runtime in most cases, esbuild must include the full +module graph of every re-export — including the heavy 3D libraries — because +it cannot know at build time which exports will be used. + +### 2. The fix: move heavy exports to dedicated subpath entries + +Both `BlochSphere` and `MoleculeViewer` were removed from the main barrel +and placed behind their own package export subpaths: + +```jsonc +// qsharp-lang/package.json +"exports": { + "./ux": "./ux/index.ts", // main barrel — no longer has 3D libs + "./ux/bloch": "./ux/bloch.tsx", // three.js lives here + "./ux/chem": "./ux/chem/index.tsx", // 3Dmol lives here +} +``` + +Consumers that actually need those widgets import from the subpath directly +(`qsharp-lang/ux/bloch`, `qsharp-lang/ux/chem`). + +### 3. `BlochSphere` is now lazy-loaded in the webview + +`webview.tsx` was changed to use a **dynamic import** (Preact `lazy` + +`Suspense`) for the Bloch sphere: + +```tsx +// Before — three.js pulled into the initial bundle +import { BlochSphere } from "qsharp-lang/ux"; + +// After — three.js only loaded when a Bloch panel is actually opened +const BlochSphere = lazy(() => + import("qsharp-lang/ux/bloch").then((m) => ({ default: m.BlochSphere })), +); +``` + +### 4. esbuild code-splitting for `webview.tsx` + +For the dynamic import to actually produce a separate file, esbuild's +`splitting` feature must be enabled. The VS Code build (`vscode/build.mjs`) +was updated so that `webview.tsx` is built as an ES module with splitting +turned on: + +```js +// vscode/build.mjs — new "webview" build target +{ + format: "esm", + splitting: true, + entryPoints: ["src/webview/webview.tsx"], + chunkNames: "webview/chunks/[name]-[hash]", + ... +} +``` + +The other two entry points (`editor.tsx` and the learning webview client) +continue to be built as CommonJS modules — no change to their behaviour. + +### 5. ` +``` + +to: + +```html + +``` + +--- + +## Results + +All sizes are **unminified** — the extension build does not currently minify +its output. + +| File | Before | After | Change | +| -------------------------------------- | ------- | ----------- | ------------------------------------ | +| `webview.js` (loaded for every panel) | ~4.4 MB | **1.2 MB** | −3.2 MB (−73%) | +| `editor.js` (circuit editor) | ~2.1 MB | **0.27 MB** | −1.8 MB (−87%) | +| `chunks/bloch-*.js` _(new — lazy)_ | — | 1.35 MB | loaded only when a Bloch panel opens | +| `chunks/chunk-*.js` _(shared — eager)_ | — | 0.04 MB | small shared utilities | + +The Bloch sphere and its 3D engine (1.35 MB) are now **only downloaded when +a user actually opens a Bloch sphere panel**, not on every extension startup. + +--- + +## How to Measure This Yourself + +After running the build: + +```powershell +python .\build.py --no-check --no-test --npm --vscode +``` + +List the output files and their sizes: + +```powershell +Get-ChildItem -Recurse source\vscode\out\webview -Filter "*.js" | + Select-Object Name, @{n='KB';e={[math]::Round($_.Length/1KB)}} | + Sort-Object KB -Descending +``` + +To verify a library is absent from a bundle, search for a symbol it exports: + +```powershell +# Should return 0 — three.js no longer in the main webview bundle +(Select-String source\vscode\out\webview\webview.js -Pattern 'WebGLRenderer').Count + +# Should return 0 — 3Dmol no longer in the main webview bundle +(Select-String source\vscode\out\webview\webview.js -Pattern 'createViewer').Count +``` + +--- + +## Files Changed + +| File | Change | +| --------------------------------------- | --------------------------------------------------------------------- | +| `source/npm/qsharp/ux/index.ts` | Removed `BlochSphere` and `MoleculeViewer` re-exports | +| `source/npm/qsharp/package.json` | Added `./ux/bloch` and `./ux/chem` subpath exports | +| `source/vscode/build.mjs` | Split `webview.tsx` into a separate ESM + code-splitting build target | +| `source/vscode/src/webview/webview.tsx` | Dynamic `import()` + `lazy`/`Suspense` for `BlochSphere` | +| `source/vscode/src/webviewPanel.ts` | ` -``` - -to: - -```html - -``` - ---- - -## Results - -All sizes are **unminified** — the extension build does not currently minify -its output. - -| File | Before | After | Change | -| -------------------------------------- | ------- | ----------- | ------------------------------------ | -| `webview.js` (loaded for every panel) | ~4.4 MB | **1.2 MB** | −3.2 MB (−73%) | -| `editor.js` (circuit editor) | ~2.1 MB | **0.27 MB** | −1.8 MB (−87%) | -| `chunks/bloch-*.js` _(new — lazy)_ | — | 1.35 MB | loaded only when a Bloch panel opens | -| `chunks/chunk-*.js` _(shared — eager)_ | — | 0.04 MB | small shared utilities | - -The Bloch sphere and its 3D engine (1.35 MB) are now **only downloaded when -a user actually opens a Bloch sphere panel**, not on every extension startup. - ---- - -## How to Measure This Yourself - -After running the build: - -```powershell -python .\build.py --no-check --no-test --npm --vscode -``` - -List the output files and their sizes: - -```powershell -Get-ChildItem -Recurse source\vscode\out\webview -Filter "*.js" | - Select-Object Name, @{n='KB';e={[math]::Round($_.Length/1KB)}} | - Sort-Object KB -Descending -``` - -To verify a library is absent from a bundle, search for a symbol it exports: - -```powershell -# Should return 0 — three.js no longer in the main webview bundle -(Select-String source\vscode\out\webview\webview.js -Pattern 'WebGLRenderer').Count - -# Should return 0 — 3Dmol no longer in the main webview bundle -(Select-String source\vscode\out\webview\webview.js -Pattern 'createViewer').Count -``` - ---- - -## Files Changed - -| File | Change | -| --------------------------------------- | --------------------------------------------------------------------- | -| `source/npm/qsharp/ux/index.ts` | Removed `BlochSphere` and `MoleculeViewer` re-exports | -| `source/npm/qsharp/package.json` | Added `./ux/bloch` and `./ux/chem` subpath exports | -| `source/vscode/build.mjs` | Split `webview.tsx` into a separate ESM + code-splitting build target | -| `source/vscode/src/webview/webview.tsx` | Dynamic `import()` + `lazy`/`Suspense` for `BlochSphere` | -| `source/vscode/src/webviewPanel.ts` | `