Summary
@niivue/niivue/webgl2 and @niivue/niivue/webgpu exist so a consumer can ship
one backend. They do not do that. Both subpaths pull a shared 1.83 MB chunk that
contains the WebGL2 renderer and the WebGPU renderer, so picking a backend entry
point buys almost nothing.
Evidence
Chunk closure of each entry in packages/niivue/dist, reached by following the
relative imports:
niivue.webgl2.js 5 chunks, 1.99 MB
cortex-CBhDullr.js 23,401
fetchOmeZarrChunkedSource-DoKkgzlW.js 1,831,612
index-Cbb_IM7q.js 114,347
niivue.webgl2.js 7,493
slide-BFGmEger.js 8,247
niivue.webgpu.js 5 chunks, 1.99 MB
cortex-CBhDullr.js 23,401
fetchOmeZarrChunkedSource-DoKkgzlW.js 1,831,612
index-Cbb_IM7q.js 114,347
niivue.webgpu.js 7,527
slide-CIscnGdf.js 9,979
Three of the five chunks are byte-identical between the two, including the
1.83 MB one. Token counts inside fetchOmeZarrChunkedSource-DoKkgzlW.js:
GPUBufferUsage 67
createShaderModule 18
@group(0) 53
drawElements 10
texImage3D 12
Both backends, one chunk, shipped to both entry points. (The per-backend slide
renderers do split correctly: slide-BFGmEger.js vs slide-CIscnGdf.js. That
part works.)
Cause
src/control/viewBoth.ts holds two unrelated things:
-
The ViewLifecycle implementation for the universal NVControl (backend
'both'), which constructs NVViewGL or NVViewGPU on a runtime branch and
therefore legitimately imports both:
const candidate =
backend === 'webgl2'
? new NVViewGL(canvas, ctrl.model, ctrl.opts)
: new NVViewGPU(canvas, ctrl.model, ctrl.opts)
-
Backend-agnostic canvas and viewport helpers: getCanvasViewport,
setCanvasViewport, getCanvasInstances, computeBoundsPixelRect,
isIdentityViewport, registerCanvasInstance, unregister,
IDENTITY_VIEWPORT, BoundsPixelRect.
Group 2 is imported by modules on every backend path:
NVControlWebGL2.ts -> NVControlBase.ts -> viewBoth.ts -> wgpu/NVViewGPU
NVControlWebGPU.ts -> NVControlBase.ts -> viewBoth.ts -> gl/NVViewGL
control/interactions.ts -> viewBoth.ts (computeBoundsPixelRect)
control/viewWebGL2.ts -> viewBoth.ts (registerCanvasInstance)
control/viewWebGPU.ts -> viewBoth.ts (registerCanvasInstance)
ESM imports are module-granular, so importing one helper from viewBoth.ts
pulls the whole module and both renderers with it. No entry point can tree-shake
this, because the edge is static and the constructors are reachable.
Fix
The injection seam already exists and already works: NVControlBase calls
lifecycle through this._viewLifecycle, and control/viewWebGL2.ts and
control/viewWebGPU.ts are single-backend implementations of it. Only the
helper co-location breaks it.
- Move group 2 into its own module (
control/canvasRegistry.ts or similar)
that imports neither gl/ nor wgpu/.
- Repoint
NVControlBase.ts, control/interactions.ts, control/viewWebGL2.ts
and control/viewWebGPU.ts at that module.
- Leave
viewBoth.ts holding only the dual-backend lifecycle, imported solely
by NVControl.ts.
Acceptance
- The chunk closure of
dist/niivue.webgl2.js contains no WGSL or WebGPU
tokens (@group(, GPUBufferUsage, createShaderModule), and the closure of
dist/niivue.webgpu.js contains no WebGL2 tokens (drawElements,
texImage3D).
- A test asserts that, so it cannot regress the way this did. The natural home
is next to src/entryPoints.test.ts, which already pins the export side of
the entry-point contract.
packages/niivue/AGENTS.md currently documents this leak under
"The single-backend distributions are not backend-isolated". Delete that
section when it stops being true.
Context
Found while auditing the export surface on chore/export-surface-audit. Not
fixed there, because breaking the static edge is a real refactor rather than an
entry-point change.
Generated with Claude Code
https://claude.ai/code/session_01RxnaB17kqcirGHNyAjPbgG
Summary
@niivue/niivue/webgl2and@niivue/niivue/webgpuexist so a consumer can shipone backend. They do not do that. Both subpaths pull a shared 1.83 MB chunk that
contains the WebGL2 renderer and the WebGPU renderer, so picking a backend entry
point buys almost nothing.
Evidence
Chunk closure of each entry in
packages/niivue/dist, reached by following therelative imports:
Three of the five chunks are byte-identical between the two, including the
1.83 MB one. Token counts inside
fetchOmeZarrChunkedSource-DoKkgzlW.js:Both backends, one chunk, shipped to both entry points. (The per-backend slide
renderers do split correctly:
slide-BFGmEger.jsvsslide-CIscnGdf.js. Thatpart works.)
Cause
src/control/viewBoth.tsholds two unrelated things:The
ViewLifecycleimplementation for the universalNVControl(backend'both'), which constructsNVViewGLorNVViewGPUon a runtime branch andtherefore legitimately imports both:
Backend-agnostic canvas and viewport helpers:
getCanvasViewport,setCanvasViewport,getCanvasInstances,computeBoundsPixelRect,isIdentityViewport,registerCanvasInstance,unregister,IDENTITY_VIEWPORT,BoundsPixelRect.Group 2 is imported by modules on every backend path:
ESM imports are module-granular, so importing one helper from
viewBoth.tspulls the whole module and both renderers with it. No entry point can tree-shake
this, because the edge is static and the constructors are reachable.
Fix
The injection seam already exists and already works:
NVControlBasecallslifecycle through
this._viewLifecycle, andcontrol/viewWebGL2.tsandcontrol/viewWebGPU.tsare single-backend implementations of it. Only thehelper co-location breaks it.
control/canvasRegistry.tsor similar)that imports neither
gl/norwgpu/.NVControlBase.ts,control/interactions.ts,control/viewWebGL2.tsand
control/viewWebGPU.tsat that module.viewBoth.tsholding only the dual-backend lifecycle, imported solelyby
NVControl.ts.Acceptance
dist/niivue.webgl2.jscontains no WGSL or WebGPUtokens (
@group(,GPUBufferUsage,createShaderModule), and the closure ofdist/niivue.webgpu.jscontains no WebGL2 tokens (drawElements,texImage3D).is next to
src/entryPoints.test.ts, which already pins the export side ofthe entry-point contract.
packages/niivue/AGENTS.mdcurrently documents this leak under"The single-backend distributions are not backend-isolated". Delete that
section when it stops being true.
Context
Found while auditing the export surface on
chore/export-surface-audit. Notfixed there, because breaking the static edge is a real refactor rather than an
entry-point change.
Generated with Claude Code
https://claude.ai/code/session_01RxnaB17kqcirGHNyAjPbgG