diff --git a/source/npm/qsharp/package.json b/source/npm/qsharp/package.json
index 46ba3ee1dc7..fb76ec65d18 100644
--- a/source/npm/qsharp/package.json
+++ b/source/npm/qsharp/package.json
@@ -24,6 +24,8 @@
"./katas-md": "./dist/katas-md.js",
"./state-viz": "./ux/circuit-vis/state-viz/worker/index.ts",
"./ux": "./ux/index.ts",
+ "./ux/bloch": "./ux/bloch/index.ts",
+ "./ux/chem": "./ux/chem/index.tsx",
"./qdk-theme.css": "./ux/qdk-theme.css",
"./rz-array.json": "./rz-array.json"
},
diff --git a/source/npm/qsharp/ux/bloch/index.ts b/source/npm/qsharp/ux/bloch/index.ts
new file mode 100644
index 00000000000..209269d344b
--- /dev/null
+++ b/source/npm/qsharp/ux/bloch/index.ts
@@ -0,0 +1,7 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// Dedicated entry point for the Bloch sphere widget. Keeping this behind its
+// own package subpath (qsharp-lang/ux/bloch) isolates three.js to a lazily
+// loaded chunk instead of bundling it into the main ux barrel.
+export { BlochSphere, type BlochSphereProps } from "./bloch.js";
diff --git a/source/npm/qsharp/ux/index.ts b/source/npm/qsharp/ux/index.ts
index 709b29bb9f4..d30dd70ac6a 100644
--- a/source/npm/qsharp/ux/index.ts
+++ b/source/npm/qsharp/ux/index.ts
@@ -20,11 +20,9 @@ export { SpaceChart } from "./spaceChart.js";
export { ScatterChart } from "./scatterChart.js";
export { EstimatesOverview } from "./estimatesOverview.js";
export { EstimatesPanel } from "./estimatesPanel.js";
-export { BlochSphere } from "./bloch/bloch.js";
export { Circuit, CircuitPanel } from "./circuit.js";
export { setRenderer, Markdown } from "./renderers.js";
export { Atoms, type ZoneLayout, type TraceData } from "./atoms/index.js";
-export { MoleculeViewer } from "./chem/index.js";
export { Entanglement, type EntanglementProps } from "./entanglement.js";
export {
ensureTheme,
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..cead55a5365 100644
--- a/source/vscode/build.mjs
+++ b/source/vscode/build.mjs
@@ -11,7 +11,13 @@ import { build as esbuildBuild, context } from "esbuild";
const thisDir = dirname(fileURLToPath(import.meta.url));
const libsDir = join(thisDir, "..", "..", "node_modules");
-// ── Shared esbuild options ──────────────────────────────────────────
+// Watch builds skip minification so rebuilds stay fast and stack traces stay
+// readable during development. One-shot builds (CI and `build.py`) minify to
+// keep the shipped webview/extension bundles small. Linked source maps are
+// emitted either way, so minified output remains debuggable.
+const isWatch = process.argv.includes("--watch");
+
+// ── Shared esbuild options ──────────────────────────────────────
/** @type {import("esbuild").BuildOptions} */
const commonBuildOptions = {
@@ -20,6 +26,7 @@ const commonBuildOptions = {
format: "cjs",
target: ["es2022"],
sourcemap: "linked",
+ minify: !isWatch,
};
// ── Per-platform build options ──────────────────────────────────────
@@ -32,7 +39,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 +48,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 +234,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];
}
@@ -272,8 +297,6 @@ export async function watchVsCode() {
(async () => {
const thisFilePath = resolve(fileURLToPath(import.meta.url));
if (thisFilePath === resolve(process.argv[1])) {
- const isWatch = process.argv.includes("--watch");
-
if (isWatch) {
await watchVsCode();
} else {
@@ -282,6 +305,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 58ab0159754..dfda7a5e864 100644
--- a/source/vscode/src/webviewPanel.ts
+++ b/source/vscode/src/webviewPanel.ts
@@ -363,7 +363,7 @@ export class QSharpWebViewPanel {
-
+
diff --git a/source/widgets/js/index.tsx b/source/widgets/js/index.tsx
index 012fd0676dd..da8cdbe5fb5 100644
--- a/source/widgets/js/index.tsx
+++ b/source/widgets/js/index.tsx
@@ -15,10 +15,10 @@ import {
Atoms,
type ZoneLayout,
type TraceData,
- MoleculeViewer,
Entanglement,
type EntanglementProps,
} from "qsharp-lang/ux";
+import { MoleculeViewer } from "qsharp-lang/ux/chem";
import markdownIt from "markdown-it";
import "./widgets.css";