| name | thatopen-errors-performance |
| description | Use when experiencing memory leaks, slow rendering, browser tab crashes, or performance issues with large BIM models. Prevents memory leaks from undisposed resources and main thread blocking. Covers disposal patterns, memory management, worker usage, BVH raycasting, polygon offset z-fighting, fragment optimization, large model strategies. Keywords: performance, memory, leak, dispose, slow, crash, large model, worker, optimization, z-fighting, disposal, gpu, browser, laggy, low FPS, browser freezes, out of memory.
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires @thatopen/components 3.3.x. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
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
-
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.
-
ALWAYS dispose in the correct order. components.dispose() handles
ordering internally (FragmentsManager last). When disposing manually,
ALWAYS dispose dependents before their dependencies.
-
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.
-
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.
-
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):
components.dispose();
What components.dispose() does internally:
- Sets
components.enabled = false (stops the render loop)
- Iterates all components in
components.list
- Calls
.dispose() on each Disposable component
- Disposes
FragmentsManager last (ensures dependent components clean up first)
- Fires
components.onDisposed event
Per-Model Disposal
When removing a single model while the viewer stays active:
const fragments = components.get(OBC.FragmentsManager);
fragments.disposeModel(modelId);
What disposeModel() does:
- Fires
onBeforeDispose event with the model
- Removes all fragments (InstancedMesh instances) from the scene
- Disposes GPU buffers (geometry, materials) for each fragment
- Terminates worker state for that model
- Removes the model from
fragments.list
Three.js Manual Disposal
For custom Three.js objects you create yourself:
geometry.dispose();
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();
renderTarget.dispose();
scene.remove(mesh);
mesh.geometry.dispose();
mesh.material.dispose();
Disposal Checklist
Use this checklist when implementing cleanup logic:
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 = leak
Key objects to watch in heap snapshots:
BufferGeometry — GPU geometry buffers
InstancedBufferAttribute — per-instance transforms
MeshLambertMaterial / MeshBasicMaterial — materials
Texture / DataTexture — texture data
WebGLProgram — 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);
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
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();
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
const model = await ifcLoader.load(ifcBytes, true, "Building");
This 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 |
const archModel = await loadFragments("architecture.frag");
fragments.baseCoordinationModel = archModel.modelId;
fragments.baseCoordinationMatrix = archModel.coordinationMatrix;
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:
const floor3 = await loadFragments("floor3.frag");
fragments.disposeModel(floor3.modelId);
const floor7 = await loadFragments("floor7.frag");
Performance Optimization Checklist
Before Loading
During Runtime
On Cleanup
Common Performance Problems
Problem: Browser Tab Crashes
Cause: Memory exhaustion from loading too-large models.
Solution:
- Check model element count before loading
- Filter IFC classes to reduce geometry
- Use model federation (split by discipline/floor)
- Monitor
performance.memory.usedJSHeapSize (Chrome only)
Problem: UI Freezes During Operations
Cause: Heavy operations running on main thread without worker.
Solution:
- Verify worker is initialized:
fragments.initialized === true
- Use async APIs:
await fragments.getData(items) (runs in worker)
- Batch operations instead of per-element calls
Problem: Gradual Slowdown
Cause: Accumulating undisposed resources across load/unload cycles.
Solution:
- Profile with DevTools Memory tab
- Ensure
disposeModel() is called before loading replacement
- Check for custom Three.js objects not being disposed
- Verify event listeners are properly removed
Problem: Z-Fighting Flickering
Cause: Overlapping surfaces at same depth without polygon offset.
Solution:
- Apply
polygonOffset = true to overlay materials
- Use
polygonOffsetFactor = 1 and polygonOffsetUnits = 1
- ThatOpen highlights handle this automatically — check custom materials
Related Skills
thatopen-core-fragments — Fragment system, worker init, model lifecycle
thatopen-core-architecture — Component system, disposal via components
thatopen-syntax-ifc-loading — IfcLoader settings, class filtering
thatopen-errors-loading — Loading failures, WASM issues, error recovery
References