Skip to content

Sccarda/webview file size#3356

Draft
ScottCarda-MS wants to merge 4 commits into
sccarda/blochfrom
sccarda/WebviewFileSize
Draft

Sccarda/webview file size#3356
ScottCarda-MS wants to merge 4 commits into
sccarda/blochfrom
sccarda/WebviewFileSize

Conversation

@ScottCarda-MS

@ScottCarda-MS ScottCarda-MS commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Reduce VS Code webview bundle size

Summary

The Bloch sphere widget pulled three.js into the
shared webview.js bundle, growing it from ~3 MB to ~4.4 MB. That bundle is
loaded eagerly for every Q# output panel (histograms, circuit diagrams,
resource estimates, documentation, etc.), so every panel paid for three.js
even though only the Bloch sphere needs it. The chemistry MoleculeViewer
widget was bundling 3Dmol into the same bundle in the
same way, despite never being used in the VS Code webview at all.

This PR removes those heavy libraries from the eagerly-loaded bundle by
lazy-loading them, and additionally enables minification for the VS Code
build (which previously never minified its output).

Result: the eagerly-loaded webview.js drops from ~4.4 MB to 0.56 MB
(−87%). three.js is now downloaded only when a user actually opens a Bloch
sphere panel.

Motivation

webview.js is shared by all Q# webview panels and is loaded on first use of
any of them. Heavy, rarely-used 3D libraries do not belong in that shared
bundle — they should load on demand, only when their feature is opened.

Changes

1. Root cause: static barrel exports

qsharp-lang/ux has a single barrel file (ux/index.ts) that re-exports
everything, including the 3D widgets:

// 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 uses BlochSphere or MoleculeViewer at
runtime in most cases, esbuild must include the full module graph of every
re-export — including the 3D libraries — because it cannot know at build time
which exports will be used.

2. Move heavy exports to dedicated subpath entries

BlochSphere and MoleculeViewer were removed from the main barrel and
placed behind their own package export subpaths:

// 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. Lazy-load BlochSphere in the webview

webview.tsx now uses a dynamic import (Preact lazy + Suspense):

// 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 produce a separate file, esbuild's splitting
feature must be enabled. The VS Code build (vscode/build.mjs) now builds
webview.tsx as an ES module with splitting turned on:

// 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)
remain CommonJS — no change to their behaviour.

5. <script type="module"> in the webview HTML

Because webview.js is now an ES module, the <script> tag in the
extension's webview HTML was updated from:

<script src="${webviewJs}"></script>

to:

<script type="module" src="${webviewJs}"></script>

6. Minify the VS Code bundles

The VS Code esbuild build previously never minified its output — unlike
the playground, which minifies behind a (rarely-used) --release flag.
Minification is now enabled for normal one-shot builds (CI and build.py)
but stays off during watch mode, so iterative development keeps fast
rebuilds and readable stack traces:

// vscode/build.mjs
const isWatch = process.argv.includes("--watch");

const commonBuildOptions = {
  bundle: true,
  // ...
  sourcemap: "linked",
  minify: !isWatch,
};

Linked source maps are still emitted for minified builds, so production stack
traces remain debuggable.

Results

"Lazy-load" is the barrel/code-splitting work (steps 1–5); "+ minify" adds
step 6. All numbers are bundle sizes on disk.

File Original Lazy-load + Minify
webview.js (loaded for every panel) ~4.4 MB 1.21 MB 0.56 MB
editor.js (circuit editor) ~2.1 MB 0.27 MB 0.13 MB
chunks/bloch-*.js (lazy) 1.35 MB 0.64 MB
chunks/chunk-*.js (shared, eager) 0.04 MB 0.02 MB

The Bloch sphere's 3D engine is now both minified and only downloaded when
a user actually opens a Bloch sphere panel
, rather than on every extension
startup. Minification also applies to the extension-host bundles
(extension.js, workers), which were never minified before either.

Testing

  • Full build passes (python ./build.py --no-check --no-test --npm --vscode --play),
    including all three VS Code typechecks (main / view / learning).

  • Manually verified in the Extension Development Host that the Bloch sphere,
    circuit editor, and state visualizer panels all render correctly.

  • Confirmed three.js and 3Dmol are absent from the eagerly-loaded bundle:

    # Both return 0
    (Select-String source\vscode\out\webview\webview.js -Pattern 'WebGLRenderer').Count
    (Select-String source\vscode\out\webview\webview.js -Pattern 'createViewer').Count
  • Confirmed linked source maps are still emitted alongside the minified
    bundles.

Risks / Notes

  • First-open latency: opening a Bloch sphere (or chemistry viewer) now
    fetches its chunk on demand, a small one-time delay in exchange for faster
    startup of every other panel.
  • External import paths: any external code importing BlochSphere or
    MoleculeViewer from qsharp-lang/ux must switch to qsharp-lang/ux/bloch
    or qsharp-lang/ux/chem. All in-repo consumers have been updated.
  • Newly minified host bundles: the extension-host bundles (extension.js,
    workers) are minified for the first time; activation was sanity-checked in
    the Extension Development Host.

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; enable minification for non-watch builds
source/vscode/src/webview/webview.tsx Dynamic import() + lazy/Suspense for BlochSphere
source/vscode/src/webviewPanel.ts <script type="module"> to load the ESM bundle
source/playground/src/main.tsx Import BlochSphere from qsharp-lang/ux/bloch subpath
source/widgets/js/index.tsx Import MoleculeViewer from qsharp-lang/ux/chem subpath

@ScottCarda-MS ScottCarda-MS changed the base branch from main to sccarda/bloch June 18, 2026 16:51
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.

1 participant