Adding Tree-Shakeable Features
This guide is for contributors adding or modifying Babylon.js framework code. Application developers should start with Using Pure Imports and Registering Side Effects.
The File Pattern
Most files in Babylon.js have no side effects. These use a regular filename, such as feature.ts, and do not need any special treatment.
The multi-file pattern only applies to modules with side effects. Those are split into up to three files:
feature.pure.ts -> Implementation and registration functionfeature.types.ts -> TypeScript module augmentations, if anyfeature.ts -> Backward-compatible wrapperNot every side-effect module needs all three files. The .types.ts file is only needed when the feature adds methods or properties to another class.
No Side Effects: Regular File
If your feature has no side effects at all, use the regular filename: feature.ts. No .pure.ts, wrapper, or .types.ts file is needed. This is the most common case.
Side Effects Without Type Augmentations: Two Files
If your feature only has RegisterClass calls or static assignments, with no prototype augmentation:
feature.pure.tscontains the implementation andRegisterFeature()function.feature.tsis the wrapper that callsRegisterFeature().
No .types.ts file is needed if there are no declare module blocks.
Side Effects With Type Augmentations: Three Files
You need all three if your feature:
- Adds methods or properties to existing classes.
- Uses
declare moduleto tell TypeScript about those added members.
Step-by-Step: Adding a New Prototype Augmentation
This example adds doSomething() to Scene.
1. Create the Types File
// myFeatureSceneComponent.types.tsimport type { Scene } from "../scene.pure";
declare module "../scene.pure" { export interface Scene { /** * Does something useful. * @returns The result. */ doSomething(): string; }}Rules:
- Use type-only imports.
- Target the
.puremodule indeclare module. - Document augmented public members with JSDoc.
2. Create the Pure Implementation
// myFeatureSceneComponent.pure.ts/** This file must only contain pure code and pure imports */
export * from "./myFeatureSceneComponent.types";
import { Scene } from "../scene.pure";import { RegisterClass } from "../Misc/typeStore";
export class MyFeatureSceneComponent { // Implementation.}
let _registered = false;export function RegisterMyFeatureSceneComponent(): void { if (_registered) { return; } _registered = true;
Scene.prototype.doSomething = function (): string { return "something"; };
RegisterClass("BABYLON.MyFeatureSceneComponent", MyFeatureSceneComponent);}Rules:
- Start
.pure.tsfiles with/** This file must only contain pure code and pure imports */. - Re-export the
.typesmodule when the feature has type augmentations. - Put all side effects inside the
RegisterXxx()function. - Guard registration with an idempotency flag.
- Import from
.purespecifiers when a pure module exists. - Keep
RegisterClasscalls inside the registration function.
3. Create the Wrapper
// myFeatureSceneComponent.tsexport * from "./myFeatureSceneComponent.pure";import { RegisterMyFeatureSceneComponent } from "./myFeatureSceneComponent.pure";
RegisterMyFeatureSceneComponent();The wrapper preserves existing behavior: importing the non-pure module still runs the compatibility side effects.
4. Update Barrels
Add the pure export to the directory's pure barrel:
export * from "./myFeatureSceneComponent.pure";Add the existing wrapper export to the standard barrel:
export * from "./myFeatureSceneComponent";5. Generate Side-Effect Stubs
If your feature augments prototypes, regenerate the side-effect stubs:
npm run generate:side-effect-stubsThis adds a lightweight missing-side-effect stub to the relevant pure class.
Step-by-Step: Adding a RegisterClass-Only Module
If a feature only needs deserialization support and does not augment another class, create a pure implementation and wrapper:
1. Create the Pure Implementation
// myNewMesh.pure.ts/** This file must only contain pure code and pure imports */
import { Mesh } from "./mesh.pure";import { RegisterClass } from "../Misc/typeStore";
export class MyNewMesh extends Mesh { // Full implementation.
public static Parse(parsedMesh: unknown, scene: Scene): MyNewMesh { return new MyNewMesh("name", scene); }}
let _registered = false;export function RegisterMyNewMesh(): void { if (_registered) { return; } _registered = true;
RegisterClass("BABYLON.MyNewMesh", MyNewMesh);}2. Create the Wrapper
// myNewMesh.tsexport * from "./myNewMesh.pure";import { RegisterMyNewMesh } from "./myNewMesh.pure";
RegisterMyNewMesh();Naming Conventions
Registration Function Naming
Registration functions always use Register plus the PascalCase file name:
| Filename | Function |
|---|---|
standardMaterial.pure.ts | RegisterStandardMaterial() |
engine.multiRender.pure.ts | RegisterEngineMultiRender() |
WebXRHandTracking.pure.ts | RegisterWebXRHandTracking() |
joinedPhysicsEngineComponent.pure.ts | RegisterJoinedPhysicsEngineComponent() |
Import Specifiers in Pure Files
When a .pure.ts module imports another module that also has a pure version, import the pure specifier:
// Correctimport { Scene } from "../scene.pure";import { Vector3 } from "../Maths/math.vector.pure";import { Mesh } from "../Meshes/mesh.pure";
// Wrong: pulls in side effectsimport { Scene } from "../scene";import { Vector3 } from "../Maths/math.vector";Do not import the non-pure module from a pure file, because that can pull side effects back into the pure graph.
Exception: type-only imports are erased at compile time, so they do not create runtime side effects:
import type { Scene } from "../scene";Using the pure path is still the clearest convention for new pure code.
Static Method Extraction
When adding static methods to a class in a .pure.ts file, consider whether the method can be exported as a standalone function. This can improve tree-shaking for utility-style operations.
Methods That Stay as Class Statics
Keep a method as a class static when it needs private or protected class members, when it is a static getter or setter, or when class identity is part of the public API.
Methods That Should Be Standalone Functions
Prefer a standalone function when the method is pure computation, a simple factory, or a utility operating on structural types.
Example: if an existing class exposes MyAsset.Parse(...), keep the parse implementation available from the pure module as a top-level function. Add the class static only in the wrapper so importing the pure module does not perform a static assignment side effect.
// myAsset.pure.ts/** This file must only contain pure code and pure imports */
export class MyAsset { public name: string;
public constructor(name: string) { this.name = name; }}
export function MyAssetParse(parsedAsset: { name: string }): MyAsset { return new MyAsset(parsedAsset.name);}// myAsset.types.tsimport type { MyAssetParse } from "./myAsset.pure";
declare module "./myAsset.pure" { namespace MyAsset { export let Parse: typeof MyAssetParse; }}// myAsset.tsexport * from "./myAsset.pure";export * from "./myAsset.types";
import { MyAsset, MyAssetParse } from "./myAsset.pure";
MyAsset.Parse = MyAssetParse;Users who import from myAsset.pure can tree-shake MyAssetParse independently from the compatibility static. Users who import from myAsset get the legacy MyAsset.Parse(...) API and its matching type augmentation.
Automatic Dependencies
If a registration function depends on another registered feature, call it inside your function:
export function RegisterMyMaterial(): void { if (_registered) { return; } _registered = true;
// Register dependencies needed for loading saved scenes. RegisterTexture(); RegisterImageProcessingConfiguration();
RegisterClass("BABYLON.MyMaterial", MyMaterial); SerializationHelper._MyMaterialParser = MyMaterial.Parse;}Add automatic dependencies when:
- Your feature's
Parse()method will trigger another missing-registration error. - Your feature's constructor always creates instances of another registered class.
- Your feature is unusable without the dependency.
Do not add transitive registration when:
- The dependency is optional.
- Adding it would pull in a very large module from a small utility.
The #__PURE__ Annotation
TypeScript decorators compile to module-scope __decorate(...) calls, which can block tree-shaking. The build pipeline injects /*#__PURE__*/ annotations into compiled .pure.js files automatically.
If your .pure.ts file uses decorators, no manual annotation is needed. Make sure the file uses the .pure.ts suffix so the post-build script can process it.
Validation Checklist
Before submitting a PR that adds or modifies tree-shakeable code:
- TypeScript compiles:
npm run build:es6with 0 errors. - ESLint passes:
npm run lint:check. Two custom ESLint rules enforce tree-shaking correctness. - Tree-shaking verification passes:
npm run check:treeshaking, which verifies manifest drift, pure barrels, and side-effect stubs. - Side-effects sync check passes:
npm run check:side-effects-sync, which verifies thesideEffectsarray in@babylonjs/core/package.jsonmatches the manifest. - Bundle smoke tests pass:
npm run test:treeshaking.
@babylonjs/require-pure-annotation requires /*#__PURE__*/ on module-scope calls and static field initializers in .pure.ts files. It is auto-fixable.
@babylonjs/no-side-effect-imports-in-pure disallows side-effect-only imports in .pure.ts files and checks that pure.ts barrels only re-export side-effect-free sources.
The full CI check runs all of these together:
npm run check:treeshaking-allIf check:treeshaking fails, the error message tells you which fix command to run:
# If manifest drift is detected:npm run update:manifest
# If sideEffects array is out of sync:node scripts/treeshaking/syncSideEffects.mjs
# If pure barrels are out of date:npm run generate:pure-barrels
# If side-effect stubs are out of date:npm run generate:side-effect-stubsAutomation Scripts
| Script | npm Command | Purpose |
|---|---|---|
verifyTreeShaking.mjs | npm run check:treeshaking | Unified CI check for manifest, barrels, and stubs |
checkManifestDrift.mjs | npm run check:manifest-drift | Verify manifest matches source |
generatePureBarrels.mjs | npm run generate:pure-barrels | Regenerate pure.ts barrel files |
generateSideEffectStubs.mjs | npm run generate:side-effect-stubs | Generate prototype warning stubs |
auditSideEffects.mjs | npm run audit:side-effects | Scan for all side effects |
syncSideEffects.mjs | (manual) | Sync manifest to package metadata |
injectPureAnnotations.mjs | (runs automatically during build) | Inject /*#__PURE__*/ in compiled JS |
bundleSmokeTest.mjs | npm run test:treeshaking | Run tree-shaking bundle tests |
fixPureImports.mjs | (manual) | Rewrite imports in .pure.ts to .pure specifiers |
Both generatePureBarrels.mjs and generateSideEffectStubs.mjs support --check mode, which is used by verifyTreeShaking.mjs to compare expected output to committed files without modifying anything.
syncSideEffects.mjs also supports --check mode, used by npm run check:side-effects-sync in CI.
Pre-Commit Hook
The Babylon.js repository uses a .githooks/pre-commit hook. When core source files are staged, the hook automatically:
- Runs
npm run precommit. - Regenerates the side-effects manifest with
npm run update:manifest. - Regenerates pure barrels with
npm run generate:pure-barrels. - Regenerates side-effect stubs with
npm run generate:side-effect-stubs. - Stages the updated manifest, package metadata, and generated barrels.
After editing .pure.ts files, the hook handles the common generation steps. You can still run the individual commands manually to verify before committing.
Quick Reference: File Header Convention
// .pure.ts files:/** This file must only contain pure code and pure imports */
// .types.ts files:// No special header needed; these only contain declare module blocks.
// .ts wrapper files:// No special header needed; these are thin wrappers.Further reading
Tree-Shaking with Pure Imports
Use the Babylon.js pure barrel to keep a single import surface while preserving tree-shaking.

Registering Side Effects
Activate Babylon.js side-effect features explicitly when using pure imports.

Start Contributing to Babylon.js
Learn how to start contributing to Babylon.js.
