<pc-app> assembles the engine application from hardcoded lists: 23 component systems and 25 resource handlers, plus SoundManager, Lightmapper, BatchManager and XrManager (app.ts#L364-L419). These are named imports from playcanvas (app.ts#L2-L65), which is external in our build — so every consumer that bundles the library gets the engine's entire framework layer in their app, regardless of what their scene uses. A page that shows one glTF still ships particles, physics glue, UI layout, scroll views, lightmapping, batching and XR.
The engine is designed for this to work better: it ships unbundled ESM with a sideEffects manifest precisely so bundlers can drop unused subsystems, and AppOptions exists so an application can register only what it needs. The library currently negates that design by demanding everything.
The element side has the same shape: @playcanvas/web-components is a single side-effectful entry point that defines all 29 elements, so the library's own code is also all-or-nothing.
Measured impact
Bundled with Rollup 4.62 + terser against playcanvas@2.21.4. "Current" mirrors the value imports of app.ts (plus the invariant core the other modules need: Entity, Asset, math types). "Minimal" is what a pc-app + pc-entity + pc-camera + pc-light + pc-render procedural scene needs: camera/render/light systems, no resource handlers, no optional managers.
| Engine payload |
minified |
gzip |
| Current lists |
901 KB |
227 KB |
| Minimal subset |
858 KB |
215 KB |
| Saving |
43 KB |
12 KB |
Two honest observations from the numbers:
- The engine-side saving is real but modest, because systems and handlers are mostly thin glue over renderer code that
AppBase pulls in regardless. The deeper problem is that the mandatory floor grows with every element we add — pc-joint recently added JointComponentSystem to every consumer's bundle, whether or not they use physics.
- The library's own barrel is the larger prize:
pwc.min.mjs is 162 KB minified / 40 KB gzip for all 29 elements, and none of it is shakable today (sideEffects: true, one entry). Per-element entry points would let a minimal page keep only the handful of element classes it uses. Combined, a minimal page stands to shed roughly 35–40 KB gzip.
Proposal
An import-time registry, plus per-element entry points. Fully additive — the default entry point keeps registering everything, so nothing breaks for existing users (npm or CDN), and this can land before or after 1.0.0.
// registry.ts (new)
export const registerComponentSystems = (...systems) => { /* add to a Set */ };
export const registerResourceHandlers = (...handlers) => { /* add to a Set */ };
Each element module registers what it needs as a side effect of being imported:
// components/light-component.ts
registerComponentSystems(LightComponentSystem);
AppElement then builds createOptions.componentSystems / resourceHandlers from the registry instead of its hardcoded lists. Consumers choose their granularity:
// Today, and still the default: everything
import '@playcanvas/web-components';
// Opt-in: only what the page uses
import '@playcanvas/web-components/app';
import '@playcanvas/web-components/entity';
import '@playcanvas/web-components/camera';
import '@playcanvas/web-components/light';
import '@playcanvas/web-components/render';
Design notes
- The element → subsystem mapping is not 1:1.
pc-model/pc-node instantiate glTF hierarchies that arrive with authored render/anim components, so the model module must register those systems plus the container pipeline's handlers (container, render, texture, material, animation). Part of this work is a per-element audit of what each one actually needs to function.
pc-asset cannot know its handler statically — its type attribute is runtime data, and a conditional import would defeat static bundling. Instead, each consuming element registers the handlers for the asset types it consumes (pc-sky → texture; pc-sounds → audio; pc-element → font; pc-gsplat → gsplat), and the registry functions are exported publicly for everything else (json, binary, script-driven loads).
- Systems with no fronting element (
sprite, zone, legacy model/animation) stay registered by the default entry point; subset builds whose scripts addComponent them opt in via the public registry.
- Elements
import()ed after boot register too late for AppOptions. The engine supports late additions (ComponentSystemRegistry#add, ResourceLoader#addHandler), so a live AppElement can forward registry additions to the running app — or the first version simply documents that element modules must be imported before <pc-app> connects.
- UMD/CDN users are unaffected:
pwc.min.js continues to register everything against the global pc, which ships the full engine anyway.
Tasks
🤖 Generated with Claude Code
<pc-app>assembles the engine application from hardcoded lists: 23 component systems and 25 resource handlers, plusSoundManager,Lightmapper,BatchManagerandXrManager(app.ts#L364-L419). These are named imports fromplaycanvas(app.ts#L2-L65), which isexternalin our build — so every consumer that bundles the library gets the engine's entire framework layer in their app, regardless of what their scene uses. A page that shows one glTF still ships particles, physics glue, UI layout, scroll views, lightmapping, batching and XR.The engine is designed for this to work better: it ships unbundled ESM with a
sideEffectsmanifest precisely so bundlers can drop unused subsystems, andAppOptionsexists so an application can register only what it needs. The library currently negates that design by demanding everything.The element side has the same shape:
@playcanvas/web-componentsis a single side-effectful entry point that defines all 29 elements, so the library's own code is also all-or-nothing.Measured impact
Bundled with Rollup 4.62 + terser against
playcanvas@2.21.4. "Current" mirrors the value imports ofapp.ts(plus the invariant core the other modules need:Entity,Asset, math types). "Minimal" is what apc-app+pc-entity+pc-camera+pc-light+pc-renderprocedural scene needs: camera/render/light systems, no resource handlers, no optional managers.Two honest observations from the numbers:
AppBasepulls in regardless. The deeper problem is that the mandatory floor grows with every element we add —pc-jointrecently addedJointComponentSystemto every consumer's bundle, whether or not they use physics.pwc.min.mjsis 162 KB minified / 40 KB gzip for all 29 elements, and none of it is shakable today (sideEffects: true, one entry). Per-element entry points would let a minimal page keep only the handful of element classes it uses. Combined, a minimal page stands to shed roughly 35–40 KB gzip.Proposal
An import-time registry, plus per-element entry points. Fully additive — the default entry point keeps registering everything, so nothing breaks for existing users (npm or CDN), and this can land before or after 1.0.0.
Each element module registers what it needs as a side effect of being imported:
AppElementthen buildscreateOptions.componentSystems/resourceHandlersfrom the registry instead of its hardcoded lists. Consumers choose their granularity:Design notes
pc-model/pc-nodeinstantiate glTF hierarchies that arrive with authoredrender/animcomponents, so the model module must register those systems plus the container pipeline's handlers (container,render,texture,material,animation). Part of this work is a per-element audit of what each one actually needs to function.pc-assetcannot know its handler statically — itstypeattribute is runtime data, and a conditional import would defeat static bundling. Instead, each consuming element registers the handlers for the asset types it consumes (pc-sky→ texture;pc-sounds→ audio;pc-element→ font;pc-gsplat→ gsplat), and the registry functions are exported publicly for everything else (json,binary, script-driven loads).sprite,zone, legacymodel/animation) stay registered by the default entry point; subset builds whose scriptsaddComponentthem opt in via the public registry.import()ed after boot register too late forAppOptions. The engine supports late additions (ComponentSystemRegistry#add,ResourceLoader#addHandler), so a liveAppElementcan forward registry additions to the running app — or the first version simply documents that element modules must be imported before<pc-app>connects.pwc.min.jscontinues to register everything against the globalpc, which ships the full engine anyway.Tasks
AppOptionsfrom it inAppElementexportsmap,.d.tsper entry, publint green)🤖 Generated with Claude Code