
Thatopen Errors Performance
- 5 installs
- 17 repo stars
- Updated July 8, 2026
- openaec-foundation/thatopen-claude-skill-package
Helps with ai & agent building tasks.
About
thatopen-errors-performance is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted development.
- thatopen-errors-performance
- AI & Agent Building
- AI-coding skill
Thatopen Errors Performance by the numbers
- 5 all-time installs (skills.sh)
- Ranked #13,065 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 26, 2026 (Skillselion catalog sync)
npx skills add https://github.com/openaec-foundation/thatopen-claude-skill-package --skill thatopen-errors-performanceAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 5 |
|---|---|
| repo stars | ★ 17 |
| Last updated | July 8, 2026 |
| Repository | openaec-foundation/thatopen-claude-skill-package ↗ |
What it does
Helps with ai & agent building tasks.
Files
ThatOpen Performance and Memory Management
Overview
BIM models are large. A typical building model contains millions of triangles, thousands of IFC elements, and dozens of material definitions. Without proper memory management, a single model can consume gigabytes of browser memory and crash the tab. This skill covers every performance-critical pattern for ThatOpen applications: disposal chains, memory budgets, worker offloading, GPU optimization, and strategies for handling models that exceed normal limits.
Critical Rules
1. ALWAYS dispose resources when done. Every FragmentsModel, THREE.Mesh, THREE.Material, and THREE.Texture holds GPU memory. The browser garbage collector does NOT free GPU resources — you must call .dispose() explicitly.
2. ALWAYS dispose in the correct order. components.dispose() handles ordering internally (FragmentsManager last). When disposing manually, ALWAYS dispose dependents before their dependencies.
3. NEVER forget to dispose Three.js objects created outside components. Custom meshes, materials, geometries, and textures added to the scene are YOUR responsibility. components.dispose() only cleans component-managed resources.
4. ALWAYS initialize FragmentsManager with a worker URL. The worker offloads raycasting, data queries, and model loading from the main thread. Without it, large model operations freeze the UI.
5. NEVER load raw IFC repeatedly. Convert IFC to fragments once, store the binary, reload from fragments. IFC parsing is 10-100x slower than fragment loading.
Disposal Patterns
Full Application Disposal
When tearing down the entire viewer (route change, component unmount):
// This is the ONLY call needed for full cleanup.
// components.dispose() walks all registered components and disposes them.
// It disposes FragmentsManager LAST (it depends on other components).
components.dispose();What `components.dispose()` does internally: 1. Sets components.enabled = false (stops the render loop) 2. Iterates all components in components.list 3. Calls .dispose() on each Disposable component 4. Disposes FragmentsManager last (ensures dependent components clean up first) 5. Fires components.onDisposed event
Per-Model Disposal
When removing a single model while the viewer stays active:
const fragments = components.get(OBC.FragmentsManager);
// Dispose a specific model by its ID
fragments.disposeModel(modelId);What `disposeModel()` does: 1. Fires onBeforeDispose event with the model 2. Removes all fragments (InstancedMesh instances) from the scene 3. Disposes GPU buffers (geometry, materials) for each fragment 4. Terminates worker state for that model 5. Removes the model from fragments.list
Three.js Manual Disposal
For custom Three.js objects you create yourself:
// Geometry — ALWAYS dispose
geometry.dispose();
// Material — ALWAYS dispose, including its maps
material.dispose();
if (material.map) material.map.dispose();
if (material.normalMap) material.normalMap.dispose();
if (material.aoMap) material.aoMap.dispose();
if (material.envMap) material.envMap.dispose();
// Render target — if you create custom render targets
renderTarget.dispose();
// Remove from scene BEFORE disposing
scene.remove(mesh);
mesh.geometry.dispose();
mesh.material.dispose();Disposal Checklist
Use this checklist when implementing cleanup logic:
- [ ]
components.dispose()called on application teardown - [ ]
fragments.disposeModel(id)called for individual model removal - [ ] Custom geometries disposed via
geometry.dispose() - [ ] Custom materials disposed via
material.dispose() - [ ] Texture maps disposed (map, normalMap, aoMap, envMap, etc.)
- [ ] Custom meshes removed from scene before disposal
- [ ] Event listeners removed (
removeEventListener) - [ ]
requestAnimationFrameloops cancelled - [ ] DOM references cleared (container elements)
Memory Leak Detection
Symptoms of Memory Leaks
| Symptom | Likely Cause |
|---|---|
| Tab crashes after loading/unloading models | Undisposed fragment models |
| Memory grows on every model load cycle | Missing disposeModel() calls |
| GPU process crashes | Undisposed geometries or textures |
| Gradual slowdown over time | Accumulating event listeners or meshes |
| Memory never decreases after unload | Three.js objects not disposed |
DevTools Memory Profiling
1. Open Chrome DevTools > Memory tab
2. Take heap snapshot BEFORE loading a model
3. Load the model — note memory increase
4. Dispose the model
5. Force garbage collection (trash can icon)
6. Take heap snapshot AFTER disposal
7. Compare: search for "BufferGeometry", "Material", "Texture"
8. Any remaining instances = leakKey objects to watch in heap snapshots:
BufferGeometry— GPU geometry buffersInstancedBufferAttribute— per-instance transformsMeshLambertMaterial/MeshBasicMaterial— materialsTexture/DataTexture— texture dataWebGLProgram— compiled shaders
Memory Budget Guidelines
| Model Size | Elements | Approx. Memory | Strategy |
|---|---|---|---|
| Small | < 10,000 | < 200 MB | Full load, no special handling |
| Medium | 10,000 - 100,000 | 200 - 800 MB | Full load, filter IFC classes |
| Large | 100,000 - 500,000 | 800 MB - 2 GB | Filter aggressively, ALWAYS use streaming |
| Very Large | > 500,000 | > 2 GB | Fragment streaming mandatory, federate |
Browser memory limits:
- Chrome: ~4 GB per tab (64-bit), ~1 GB (32-bit)
- Firefox: ~4 GB per tab
- Safari: ~3 GB per tab (more aggressive about killing tabs)
- Mobile browsers: ~1-2 GB total
Worker Thread Usage
Why Workers Matter
Fragment operations that run in the worker (off main thread):
- FlatBuffers deserialization (model loading)
- Raycast intersection calculations
- Property data extraction (
getData) - Position and bounding box calculations
- GUID-to-ID mapping lookups
Without the worker, ALL of these block the main thread. For a 100K-element model, a single getData call can freeze the UI for several seconds.
Worker Initialization
const fragments = components.get(OBC.FragmentsManager);
// ALWAYS init with worker URL matching your installed fragments version
fragments.init(
"https://unpkg.com/@thatopen/fragments@3.3.6/dist/Worker/worker.mjs"
);ALWAYS match the worker URL to your installed `@thatopen/fragments` version. A version mismatch between the main thread library and worker script causes deserialization failures and silent data corruption.
Main Thread vs Worker Thread
| Operation | Thread | Blocking? |
|---|---|---|
| Model loading (FlatBuffers) | Worker | No |
| Raycasting | Worker | No |
| getData / getPositions / getBBoxes | Worker | No |
| GUID mapping | Worker | No |
| Scene graph updates | Main | Yes (fast) |
| Highlight / resetHighlight | Main | Yes (fast) |
| Coordinate alignment | Main | Yes (fast) |
| Three.js rendering | Main | Yes (per frame) |
BVH Raycasting
ThatOpen uses three-mesh-bvh for accelerated raycasting. This is auto-patched onto THREE.BufferGeometry in the Components constructor.
No manual setup needed. When you call components.get(OBC.Components) or create a new Components(), the BVH extension is automatically applied.
Performance impact: Without BVH, raycasting a 100K-triangle model tests every triangle (O(n)). With BVH, it traverses a bounding volume hierarchy (O(log n)) — typically 100-1000x faster.
NEVER remove or override the BVH patch. If you import Three.js separately, ensure the BVH patch is applied before creating geometries.
Polygon Offset (Z-Fighting Prevention)
When two surfaces overlap at the same depth (e.g., a floor slab and a floor finish), the GPU cannot determine which is in front. This causes z-fighting: flickering pixels alternating between the two surfaces.
Solution: Polygon Offset
// Apply polygon offset to materials that overlap other surfaces
material.polygonOffset = true;
material.polygonOffsetFactor = 1;
material.polygonOffsetUnits = 1;When to use polygon offset:
- Highlight overlays on existing geometry
- Section fill materials on clipping planes
- Floor finish layers on structural slabs
- Any custom overlay material
ALWAYS apply polygon offset to highlight and overlay materials. ThatOpen's built-in highlight system handles this automatically, but custom materials you create need manual polygon offset.
Large Model Strategies
Strategy 1: IFC Class Filtering
Reduce memory by loading only the IFC classes you need:
const ifcLoader = components.get(OBC.IfcLoader);
await ifcLoader.setup();
// The default import configuration already excludes some classes
// for memory efficiency. You can further restrict it.
// Only configure the classes you actually need in your application.Default excluded classes (not imported unless explicitly added): Some geometry-heavy classes like IFCOPENINGELEMENT and IFCSPACE are excluded by default to save memory. Check ifcLoader.settings for the current default import list.
Strategy 2: Convert Once, Load Fragments
// FIRST TIME: Convert IFC (slow, memory-intensive)
const model = await ifcLoader.load(ifcBytes, true, "Building");
// STORE: Export the FragmentsModel binary to IndexedDB or server
// SUBSEQUENT LOADS: Skip IFC entirely (10-100x faster)
// Load the pre-converted .frag binary through FragmentsManagerThis is the single most impactful performance optimization. IFC parsing requires WASM, schema interpretation, and geometry tessellation. Fragment loading is just FlatBuffers deserialization — nearly instant.
Strategy 3: Model Federation
Instead of loading one massive model, split into discipline-specific models:
| Discipline | Typical Size | Load Priority |
|---|---|---|
| Architecture | Large | First (base model) |
| Structure | Medium | On demand |
| MEP / HVAC | Large | On demand |
| Site | Small | Optional |
// Load the base architecture model first
const archModel = await loadFragments("architecture.frag");
fragments.baseCoordinationModel = archModel.modelId;
fragments.baseCoordinationMatrix = archModel.coordinationMatrix;
// Load additional disciplines on demand
const structModel = await loadFragments("structure.frag");
fragments.applyBaseCoordinateSystem(structModel, structModel.coordinationMatrix);ALWAYS set the base coordination model from the first loaded model. Subsequent models must be aligned to this base.
Strategy 4: Selective Loading and Disposal
Load models as users navigate, dispose when they leave the area:
// User opens floor 3 → load floor 3 model
const floor3 = await loadFragments("floor3.frag");
// User navigates to floor 7 → dispose floor 3, load floor 7
fragments.disposeModel(floor3.modelId);
const floor7 = await loadFragments("floor7.frag");Performance Optimization Checklist
Before Loading
- [ ] Worker initialized with correct version URL
- [ ] Using pre-converted fragments (not raw IFC for repeat loads)
- [ ] IFC class filtering configured to exclude unnecessary types
- [ ] Memory budget estimated for target model size
During Runtime
- [ ] No unnecessary re-renders (check
renderer.update()frequency) - [ ] Raycasting uses BVH (automatic, but verify no override)
- [ ] Polygon offset applied to overlay materials
- [ ] Heavy operations (getData, getPositions) called sparingly
On Cleanup
- [ ]
components.dispose()called on teardown - [ ] Individual models disposed when no longer needed
- [ ] Custom Three.js objects disposed manually
- [ ] Event listeners removed
- [ ] DOM references cleared
Common Performance Problems
Problem: Browser Tab Crashes
Cause: Memory exhaustion from loading too-large models.
Solution: 1. Check model element count before loading 2. Filter IFC classes to reduce geometry 3. Use model federation (split by discipline/floor) 4. Monitor performance.memory.usedJSHeapSize (Chrome only)
Problem: UI Freezes During Operations
Cause: Heavy operations running on main thread without worker.
Solution: 1. Verify worker is initialized: fragments.initialized === true 2. Use async APIs: await fragments.getData(items) (runs in worker) 3. Batch operations instead of per-element calls
Problem: Gradual Slowdown
Cause: Accumulating undisposed resources across load/unload cycles.
Solution: 1. Profile with DevTools Memory tab 2. Ensure disposeModel() is called before loading replacement 3. Check for custom Three.js objects not being disposed 4. Verify event listeners are properly removed
Problem: Z-Fighting Flickering
Cause: Overlapping surfaces at same depth without polygon offset.
Solution: 1. Apply polygonOffset = true to overlay materials 2. Use polygonOffsetFactor = 1 and polygonOffsetUnits = 1 3. ThatOpen highlights handle this automatically — check custom materials
Related Skills
thatopen-core-fragments— Fragment system, worker init, model lifecyclethatopen-core-architecture— Component system, disposal via componentsthatopen-syntax-ifc-loading— IfcLoader settings, class filteringthatopen-errors-loading— Loading failures, WASM issues, error recovery
References
- references/methods.md — Disposal methods, memory APIs, performance-related APIs
- references/examples.md — Proper disposal, worker setup, memory monitoring patterns
- references/anti-patterns.md — Memory leaks, missing disposal, main thread blocking
Performance Anti-Patterns
Anti-Pattern 1: Missing Disposal on Route Change
WRONG:
// SPA navigation — user leaves the viewer page
function onRouteChange() {
// Just hide the container... memory still held!
viewerContainer.style.display = "none";
}RIGHT:
function onRouteChange() {
components.dispose();
viewerContainer.innerHTML = "";
}Why: Hiding the container does not free GPU memory. Every BufferGeometry, Material, Texture, and InstancedMesh remains in VRAM. After several navigations, the tab crashes from memory exhaustion.
---
Anti-Pattern 2: Loading Without Disposing Previous Model
WRONG:
async function handleFileUpload(file: File) {
const bytes = new Uint8Array(await file.arrayBuffer());
// Loads a new model every time — old models accumulate!
const model = await ifcLoader.load(bytes, true, file.name);
}RIGHT:
let currentModelId: string | null = null;
async function handleFileUpload(file: File) {
// Dispose previous model first
if (currentModelId) {
fragments.disposeModel(currentModelId);
}
const bytes = new Uint8Array(await file.arrayBuffer());
const model = await ifcLoader.load(bytes, true, file.name);
currentModelId = model.modelId;
}Why: Each model load allocates new GPU buffers. Without disposing the previous model, memory grows linearly with each upload. After 3-4 large models, the browser tab crashes.
---
Anti-Pattern 3: Missing Worker Initialization
WRONG:
const components = new OBC.Components();
const fragments = components.get(OBC.FragmentsManager);
// Forgot fragments.init(workerURL) — operations block main thread or fail
const data = await fragments.getData(items);RIGHT:
const components = new OBC.Components();
const fragments = components.get(OBC.FragmentsManager);
fragments.init("https://unpkg.com/@thatopen/fragments@3.3.6/dist/Worker/worker.mjs");
// Now getData runs in worker, off main thread
const data = await fragments.getData(items);Why: Without the worker, heavy operations like getData, raycast, and model loading either run on the main thread (causing UI freezes) or fail silently. ALWAYS verify fragments.initialized === true before operations.
---
Anti-Pattern 4: Repeatedly Parsing Raw IFC
WRONG:
// Every page load re-parses the IFC from scratch
async function initViewer() {
const response = await fetch("/models/building.ifc");
const bytes = new Uint8Array(await response.arrayBuffer());
const model = await ifcLoader.load(bytes, true, "Building");
}RIGHT:
// First time: convert and store as fragments
async function convertAndStore(ifcBytes: Uint8Array) {
const model = await ifcLoader.load(ifcBytes, true, "Building");
// Store the fragment binary (IndexedDB, server, etc.)
await storeBinary(model);
}
// Subsequent loads: load pre-converted fragments (10-100x faster)
async function loadStored() {
const fragBytes = await retrieveBinary("Building");
// Load fragment binary directly — skips WASM parsing entirely
}Why: IFC parsing involves WASM initialization, schema interpretation, and geometry tessellation. This is expensive: a 50 MB IFC file can take 10-30 seconds. Fragment loading is FlatBuffers deserialization — typically under 1 second for the same model.
---
Anti-Pattern 5: Forgetting Three.js Object Disposal
WRONG:
// Create helpers and overlays
const box = new THREE.BoxHelper(mesh, 0xffff00);
scene.add(box);
// Later, "remove" it by just taking it off screen
scene.remove(box);
// GPU memory for geometry and material still allocated!RIGHT:
scene.remove(box);
box.geometry.dispose();
box.material.dispose();Why: scene.remove() only detaches the object from the scene graph. The GPU buffers remain allocated until .dispose() is called. This is a Three.js fundamental — the browser GC does NOT free GPU resources.
---
Anti-Pattern 6: Missing Polygon Offset on Overlays
WRONG:
// Overlay material without polygon offset
const sectionFill = new THREE.MeshBasicMaterial({
color: 0x0000ff,
transparent: true,
opacity: 0.3,
side: THREE.DoubleSide,
});
// Result: z-fighting flickering where fill overlaps model geometryRIGHT:
const sectionFill = new THREE.MeshBasicMaterial({
color: 0x0000ff,
transparent: true,
opacity: 0.3,
side: THREE.DoubleSide,
polygonOffset: true,
polygonOffsetFactor: 1,
polygonOffsetUnits: 1,
});Why: Two surfaces at the same depth cause z-fighting — the depth buffer cannot resolve which is in front, resulting in flickering pixels. Polygon offset biases the depth of one surface to resolve the ambiguity.
---
Anti-Pattern 7: Synchronous Bulk Operations
WRONG:
// Process every element one at a time, blocking main thread
for (const elementId of allElementIds) {
const data = await fragments.getData({
[modelId]: new Set([elementId])
});
processElement(data);
}RIGHT:
// Batch all elements into one worker call
const allItems: ModelIdMap = {
[modelId]: new Set(allElementIds)
};
const allData = await fragments.getData(allItems);
// Process results after single round-trip
for (const [id, items] of Object.entries(allData)) {
items.forEach(processElement);
}Why: Each getData call is a round-trip to the worker. For 1000 elements, that is 1000 message-passing operations instead of 1. Batch into a single ModelIdMap for one worker call.
---
Anti-Pattern 8: Not Monitoring Memory in Development
WRONG:
// No memory monitoring — crashes discovered by users in productionRIGHT:
// Development-only memory monitoring
if (process.env.NODE_ENV === "development") {
setInterval(() => {
const mem = (performance as any).memory;
if (mem && mem.usedJSHeapSize > 2 * 1024 * 1024 * 1024) {
console.warn("Memory usage exceeds 2 GB — check for leaks");
}
const info = renderer.info;
console.debug(
`GPU: ${info.memory.geometries} geometries, ` +
`${info.memory.textures} textures, ` +
`${info.render.triangles} triangles/frame`
);
}, 5000);
}Why: Memory leaks are silent until the tab crashes. Proactive monitoring during development catches leaks early, before they reach production.
---
Anti-Pattern 9: Worker Version Mismatch
WRONG:
// package.json has @thatopen/fragments@3.3.6
// but worker URL points to different version
fragments.init(
"https://unpkg.com/@thatopen/fragments@3.2.0/dist/Worker/worker.mjs"
);RIGHT:
// ALWAYS match worker URL to installed package version
fragments.init(
"https://unpkg.com/@thatopen/fragments@3.3.6/dist/Worker/worker.mjs"
);Why: The worker and main thread share a binary protocol (FlatBuffers schema). Version mismatches cause deserialization failures, corrupted data, or silent wrong results. ALWAYS keep them in sync.
---
Anti-Pattern 10: Loading All IFC Classes for Large Models
WRONG:
// Force-load every IFC class including heavy ones
// This loads IFCOPENINGELEMENT, IFCSPACE, and other geometry-heavy classes
// that are excluded by default for good reasonRIGHT:
// Use default settings — they already exclude heavy classes
const ifcLoader = components.get(OBC.IfcLoader);
await ifcLoader.setup();
// Only override if you specifically need excluded classes
// and have verified the memory impactWhy: Some IFC classes (IFCOPENINGELEMENT, IFCSPACE) generate significant geometry that is rarely needed for visualization. The default import settings are optimized for the common case. Loading everything can double or triple memory usage.
---
Summary: Rules to Prevent Performance Issues
| Rule | Category |
|---|---|
ALWAYS call components.dispose() on teardown | Disposal |
ALWAYS call disposeModel() before loading replacement | Disposal |
| ALWAYS dispose custom Three.js objects manually | Disposal |
| ALWAYS initialize worker before fragment operations | Worker |
| ALWAYS match worker URL to installed fragments version | Worker |
| NEVER re-parse IFC on every load — use fragments | Loading |
| NEVER load all IFC classes for large models | Loading |
| NEVER process elements one-at-a-time — batch via ModelIdMap | Operations |
| ALWAYS use polygon offset for overlay materials | Rendering |
| ALWAYS monitor memory during development | Monitoring |
Performance Examples
Example 1: Proper Full Application Disposal
// SPA route change or component unmount
function teardownViewer(components: OBC.Components) {
// Single call handles everything
components.dispose();
// Clear DOM reference
const container = document.getElementById("viewer-container");
if (container) {
container.innerHTML = "";
}
}Example 2: Per-Model Load/Unload Cycle
const fragments = components.get(OBC.FragmentsManager);
let currentModelId: string | null = null;
async function loadModel(fragBytes: Uint8Array, world: OBC.World) {
// ALWAYS dispose previous model before loading new one
if (currentModelId) {
fragments.disposeModel(currentModelId);
currentModelId = null;
}
// Load the new model
const model = await loadFragmentBinary(fragBytes, world);
currentModelId = model.modelId;
}
async function unloadModel() {
if (currentModelId) {
fragments.disposeModel(currentModelId);
currentModelId = null;
}
}Example 3: Worker Initialization with Version Matching
import * as OBC from "@thatopen/components";
const components = new OBC.Components();
const fragments = components.get(OBC.FragmentsManager);
// Match worker URL to your installed @thatopen/fragments version
// Check package.json for the exact version
fragments.init(
"https://unpkg.com/@thatopen/fragments@3.3.6/dist/Worker/worker.mjs"
);
// Verify initialization
console.log("Worker ready:", fragments.initialized); // trueExample 4: Memory Monitoring During Development
function logMemoryUsage(label: string) {
const mem = (performance as any).memory;
if (!mem) {
console.log(`[${label}] performance.memory not available (non-Chrome)`);
return;
}
console.log(`[${label}] Heap: ${(mem.usedJSHeapSize / 1048576).toFixed(1)} MB`);
console.log(`[${label}] Total: ${(mem.totalJSHeapSize / 1048576).toFixed(1)} MB`);
console.log(`[${label}] Limit: ${(mem.jsHeapSizeLimit / 1048576).toFixed(1)} MB`);
}
// Usage: track memory across load/unload cycles
logMemoryUsage("Before load");
const model = await loadModel(data, world);
logMemoryUsage("After load");
fragments.disposeModel(model.modelId);
logMemoryUsage("After dispose");Example 5: Three.js Renderer Info for GPU Monitoring
function logGPUStats(renderer: THREE.WebGLRenderer) {
const info = renderer.info;
console.log(`Geometries in GPU: ${info.memory.geometries}`);
console.log(`Textures in GPU: ${info.memory.textures}`);
console.log(`Draw calls/frame: ${info.render.calls}`);
console.log(`Triangles/frame: ${info.render.triangles}`);
}
// Call periodically or after load/unload
logGPUStats(world.renderer.three);Example 6: Disposing Custom Three.js Objects
// Custom overlay mesh added to scene
const overlayGeometry = new THREE.PlaneGeometry(10, 10);
const overlayMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000,
transparent: true,
opacity: 0.3,
polygonOffset: true, // Prevent z-fighting
polygonOffsetFactor: -1,
polygonOffsetUnits: -1,
});
const overlayMesh = new THREE.Mesh(overlayGeometry, overlayMaterial);
world.scene.three.add(overlayMesh);
// Cleanup — YOUR responsibility (components.dispose() won't handle this)
function disposeOverlay() {
world.scene.three.remove(overlayMesh);
overlayGeometry.dispose();
overlayMaterial.dispose();
// If material had textures:
// overlayMaterial.map?.dispose();
}Example 7: IFC Class Filtering for Memory Reduction
const ifcLoader = components.get(OBC.IfcLoader);
await ifcLoader.setup();
// The default configuration already excludes some heavy classes
// like IFCOPENINGELEMENT and IFCSPACE for memory efficiency.
// Review ifcLoader.settings to understand what is included by default.
// Load with defaults (already optimized)
const model = await ifcLoader.load(ifcBytes, true, "Building");Example 8: Multi-Model Federation with Proper Disposal
const fragments = components.get(OBC.FragmentsManager);
const loadedModels: string[] = [];
async function loadDiscipline(
fragBytes: Uint8Array,
name: string,
world: OBC.World
): Promise<string> {
const model = await loadFragmentBinary(fragBytes, world);
// Set first model as coordination base
if (loadedModels.length === 0) {
fragments.baseCoordinationModel = model.modelId;
fragments.baseCoordinationMatrix = model.coordinationMatrix;
} else {
// Align subsequent models to base
fragments.applyBaseCoordinateSystem(model, model.coordinationMatrix);
}
loadedModels.push(model.modelId);
return model.modelId;
}
function disposeAllModels() {
// Dispose in reverse order (last loaded first)
for (let i = loadedModels.length - 1; i >= 0; i--) {
fragments.disposeModel(loadedModels[i]);
}
loadedModels.length = 0;
}Example 9: Polygon Offset for Custom Highlight Material
// Custom highlight material — MUST have polygon offset
const highlightMaterial = new THREE.MeshBasicMaterial({
color: 0x00ff00,
transparent: true,
opacity: 0.4,
depthTest: true,
polygonOffset: true,
polygonOffsetFactor: 1,
polygonOffsetUnits: 1,
});
// ThatOpen's built-in highlight system handles polygon offset automatically.
// Only apply manually for custom materials YOU create.Example 10: Memory-Safe Model Swap Pattern
class ModelManager {
private fragments: OBC.FragmentsManager;
private activeModels = new Map<string, string>(); // name → modelId
constructor(components: OBC.Components) {
this.fragments = components.get(OBC.FragmentsManager);
}
async swap(name: string, newFragBytes: Uint8Array, world: OBC.World) {
// Step 1: Dispose old model if exists
const oldId = this.activeModels.get(name);
if (oldId) {
this.fragments.disposeModel(oldId);
this.activeModels.delete(name);
}
// Step 2: Load new model
const model = await loadFragmentBinary(newFragBytes, world);
this.activeModels.set(name, model.modelId);
return model;
}
disposeAll() {
for (const [name, modelId] of this.activeModels) {
this.fragments.disposeModel(modelId);
}
this.activeModels.clear();
}
}Performance Methods Reference
Disposal Methods
components.dispose()
Full application disposal. Disposes all registered components in dependency order, with FragmentsManager disposed last.
components.dispose(): void- Sets
components.enabled = false(stops render loop) - Iterates
components.listand callsdispose()on each Disposable - Disposes FragmentsManager last
- Fires
components.onDisposedevent - After calling, the Components instance is no longer usable
FragmentsManager.disposeModel(modelId)
Dispose a single model while keeping the viewer active.
fragments.disposeModel(modelId: string): void- Fires
onBeforeDisposewith the FragmentsModel - Removes all Fragment meshes from the scene
- Disposes GPU geometry buffers for each fragment
- Cleans up worker state for the model
- Removes model from
fragments.list
Three.js Disposal Methods
| Object | Method | Notes |
|---|---|---|
BufferGeometry | geometry.dispose() | Frees GPU vertex/index buffers |
Material | material.dispose() | Frees compiled shader program |
Texture | texture.dispose() | Frees GPU texture memory |
WebGLRenderTarget | target.dispose() | Frees framebuffer |
WebGLRenderer | renderer.dispose() | Frees WebGL context |
InstancedMesh | mesh.dispose() | Disposes geometry + material |
Material texture maps that need disposal:
material.map(diffuse)material.normalMapmaterial.aoMapmaterial.emissiveMapmaterial.metalnessMapmaterial.roughnessMapmaterial.envMapmaterial.lightMapmaterial.bumpMapmaterial.displacementMapmaterial.alphaMap
Scene Removal
ALWAYS remove objects from the scene graph before disposing:
scene.remove(object); // Remove from parent
object.geometry.dispose(); // Then dispose GPU resources
object.material.dispose();Memory Monitoring APIs
performance.memory (Chrome only)
interface MemoryInfo {
jsHeapSizeLimit: number; // Maximum heap size (bytes)
totalJSHeapSize: number; // Total allocated heap (bytes)
usedJSHeapSize: number; // Currently used heap (bytes)
}
// Usage
const mem = (performance as any).memory;
console.log(`Used: ${(mem.usedJSHeapSize / 1048576).toFixed(1)} MB`);
console.log(`Total: ${(mem.totalJSHeapSize / 1048576).toFixed(1)} MB`);
console.log(`Limit: ${(mem.jsHeapSizeLimit / 1048576).toFixed(1)} MB`);Note: Only available in Chrome/Chromium. Returns undefined in Firefox and Safari. Use for development profiling, not production monitoring.
performance.measureUserAgentSpecificMemory() (Cross-browser)
// Requires cross-origin isolation headers
const result = await performance.measureUserAgentSpecificMemory();
console.log(`Total: ${(result.bytes / 1048576).toFixed(1)} MB`);WebGL Memory Estimation
// Get GPU memory info (WEBGL_debug_renderer_info extension)
const gl = renderer.getContext();
const ext = gl.getExtension("WEBGL_debug_renderer_info");
if (ext) {
console.log("GPU:", gl.getParameter(ext.UNMASKED_RENDERER_WEBGL));
}
// Count active GPU resources via Three.js renderer info
const info = renderer.info;
console.log(`Geometries: ${info.memory.geometries}`);
console.log(`Textures: ${info.memory.textures}`);
console.log(`Programs: ${info.programs?.length}`);
console.log(`Draw calls: ${info.render.calls}`);
console.log(`Triangles: ${info.render.triangles}`);FragmentsManager State Properties
| Property | Type | Purpose |
|---|---|---|
list | Map<string, FragmentsModel> | All loaded models |
initialized | boolean | Whether worker is ready |
baseCoordinationModel | string | ID of the base model |
baseCoordinationMatrix | THREE.Matrix4 | Base model's world matrix |
FragmentsManager Events
| Event | Payload | When |
|---|---|---|
onFragmentsLoaded | FragmentsModel | After a model finishes loading |
onBeforeDispose | FragmentsModel | Before a model is disposed |
onDisposed | void | After FragmentsManager itself is disposed |
BVH Raycasting
Applied automatically in the Components constructor via three-mesh-bvh. Patches THREE.BufferGeometry.prototype to support BVH acceleration.
No API surface — fully automatic. The patch adds:
boundsTreeproperty on BufferGeometry- Modified
Raycaster.intersectObjectthat uses BVH when available
Performance: O(log n) intersection tests instead of O(n).
Polygon Offset Properties
// Available on all Three.js Material subclasses
material.polygonOffset: boolean; // Enable offset (default: false)
material.polygonOffsetFactor: number; // Scale factor (default: 0)
material.polygonOffsetUnits: number; // Constant offset (default: 0)Standard values for overlay prevention:
polygonOffset = truepolygonOffsetFactor = 1polygonOffsetUnits = 1
Negative values push surfaces closer to camera. Positive values push away.