optimize-scene
Optimizing Decentraland Scenes
Scene Limits (Per Parcel Count)
All limits scale with parcel count n. Triangles, entities, and bodies scale linearly. Materials, textures, and height scale logarithmically.
| Resource | Formula | 1 parcel | 2 parcels | 4 parcels | 9 parcels | 16 parcels |
|---|---|---|---|---|---|---|
| Triangles | n x 10,000 | 10,000 | 20,000 | 40,000 | 90,000 | 160,000 |
| Entities | n x 200 | 200 | 400 | 800 | 1,800 | 3,200 |
| Physics bodies | n x 300 | 300 | 600 | 1,200 | 2,700 | 4,800 |
| Materials | log2(n+1) x 20 | 20 | 31 | 46 | 66 | 81 |
| Textures | log2(n+1) x 10 | 10 | 15 | 23 | 33 | 40 |
| Height limit | log2(n+1) x 20m | 20m | 31m | 46m | 66m | 81m |
File limits: 15 MB per parcel, 300 MB max total, 200 files per parcel, 50 MB max per individual file.
Entity Count Optimization
Reuse Entities
// BAD: Creating new entity each time
function spawnBullet() {
const bullet = engine.addEntity() // Creates entity every call
// ...
}
// GOOD: Object pooling
const bulletPool: Entity[] = []
function getBullet(): Entity {
const existing = bulletPool.find(e => !ActiveBullet.has(e))
if (existing) return existing
const newBullet = engine.addEntity()
bulletPool.push(newBullet)
return newBullet
}
Remove Unused Entities
engine.removeEntity(entity) // Frees the entity slot
Use Parenting
Instead of separate transforms for each child, use entity hierarchy:
const parent = engine.addEntity()
Transform.create(parent, { position: Vector3.create(8, 0, 8) })
// Children inherit parent transform
const child1 = engine.addEntity()
Transform.create(child1, { position: Vector3.create(0, 1, 0), parent })
const child2 = engine.addEntity()
Transform.create(child2, { position: Vector3.create(1, 1, 0), parent })
Triangle Count Optimization
Use Lower-Poly Models
- Small props: 100-500 triangles
- Medium objects: 500-1,500 triangles
- Large buildings: 1,500-5,000 triangles
- Hero pieces: Up to 10,000 triangles
Use LOD (Level of Detail)
Show simpler models at distance:
engine.addSystem(() => {
// Check distance to player and swap models
const playerPos = Transform.get(engine.PlayerEntity).position
const objPos = Transform.get(myEntity).position
const distance = Vector3.distance(playerPos, objPos)
const gltf = GltfContainer.getMutable(myEntity)
if (distance > 30) {
gltf.src = 'models/building_lod2.glb' // Low poly
} else if (distance > 15) {
gltf.src = 'models/building_lod1.glb' // Medium poly
} else {
gltf.src = 'models/building_lod0.glb' // High poly
}
})
Use Primitives Instead of Models
For simple shapes, MeshRenderer is lighter than loading a .glb:
MeshRenderer.setBox(entity) // Very cheap
MeshRenderer.setSphere(entity) // Cheap
MeshRenderer.setPlane(entity) // Very cheap
Texture Optimization
- Dimensions must be power-of-two: 256, 512, 1024, 2048
- Recommended sizes: 512x512 for most objects, 1024x1024 max for hero pieces
- Avoid textures over 2048x2048 — they consume excessive memory and often exceed limits
- Use
.pngfor UI/sprites with transparency - Use
.jpgfor photos and textures without transparency - Prefer compressed formats (WebP) over raw PNG where possible
- Use texture atlases (combine multiple textures into one image) to reduce draw calls and material count
- Share texture references across materials — do not duplicate texture files
- Reuse materials across entities:
// GOOD: Define material once, apply to many
Material.setPbrMaterial(entity1, { texture: Material.Texture.Common({ src: 'images/wall.jpg' }) })
Material.setPbrMaterial(entity2, { texture: Material.Texture.Common({ src: 'images/wall.jpg' }) })
// Same texture URL = shared in memory
System Optimization
Avoid Per-Frame Allocations
// BAD: Creates new Vector3 every frame
engine.addSystem(() => {
const target = Vector3.create(8, 1, 8) // Allocation!
})
// GOOD: Reuse constants
const TARGET = Vector3.create(8, 1, 8)
engine.addSystem(() => {
// Use TARGET
})
Throttle Expensive Operations
let lastCheck = 0
engine.addSystem((dt) => {
lastCheck += dt
if (lastCheck < 0.5) return // Only run every 0.5 seconds
lastCheck = 0
// Expensive operation here
})
Remove Systems When Not Needed
const systemFn = (dt: number) => { /* ... */ }
engine.addSystem(systemFn)
// When no longer needed:
engine.removeSystem(systemFn)
Asset Preloading (AssetLoad Component)
For large assets that would cause visible pop-in, use AssetLoad to pre-download before rendering:
import { engine, AssetLoad, LoadingState, GltfContainer, Transform } from '@dcl/sdk/ecs'
import { Vector3 } from '@dcl/sdk/math'
// Create a preload entity at scene startup
const preloadEntity = engine.addEntity()
AssetLoad.create(preloadEntity, { src: 'models/large-model.glb' })
// System to track loading progress
function assetLoadingSystem(dt: number) {
for (const [entity] of engine.getEntitiesWith(AssetLoad)) {
const state = AssetLoad.get(entity)
if (state.loadingState === LoadingState.FINISHED) {
// Asset is cached — now safe to create the visible entity
GltfContainer.create(entity, { src: 'models/large-model.glb' })
Transform.create(entity, { position: Vector3.create(8, 0, 8) })
AssetLoad.deleteFrom(entity) // Remove preload component
}
}
}
engine.addSystem(assetLoadingSystem)
Use this pattern for any model over ~1 MB or for assets that should be ready before a game phase begins.
Loading Time Optimization
- Lazy-load 3D models (load on demand, not all at scene start)
- Use compressed .glb files (Draco compression)
- Minimize total asset size
- Use CDN URLs for large shared assets when possible
- Preload critical assets with
AssetLoad, defer non-essential ones
Common Performance Pitfalls
- Too many systems: Each system runs every frame. Combine related logic.
- Unnecessary component queries: Cache
engine.getEntitiesWith()results when the set doesn't change. - Large GLTF files: Optimize in Blender before export (decimate, remove hidden faces).
- Uncompressed audio: Use .mp3 instead of .wav for music (10x smaller).
- Continuous raycasting: Set
continuous: falseunless you need per-frame raycasting. - Text rendering:
TextShapeis expensive. UseLabel(UI) for text that doesn't need to be in 3D space.
Cross-References
- add-3d-models — model loading, colliders, and file organization
- game-design — performance budgets, design patterns, and MVP planning
- advanced-rendering — texture modes, material reuse, and LOD with VisibilityComponent
More from dcl-regenesislabs/opendcl
game-design
Plan and design Decentraland games and interactive experiences. Scene limit formulas, performance budgets, texture requirements, asset preloading, state management patterns (module-level, component-based, state machines), object pooling, UX/UI guidelines, input design, and MVP planning. Use when the user wants game design advice, scene architecture, performance planning, or help structuring a game. Do NOT use for specific implementation (see add-interactivity, build-ui, multiplayer-sync).
30audio-video
Add sound effects, music, audio streaming, and video players to Decentraland scenes. Covers AudioSource (local files), AudioStream (streaming URLs), VideoPlayer (video surfaces), video events, and media permissions. Use when the user wants sound, music, audio, video screens, radio, or media playback. Do NOT use for 3D model animations (see animations-tweens).
30add-3d-models
Add 3D models (.glb/.gltf) to a Decentraland scene using GltfContainer. Covers loading, positioning, scaling, colliders, parenting, and browsing 5,700+ free assets from the OpenDCL catalog. Use when the user wants to add models, import GLB files, find free 3D assets, or set up model colliders. Do NOT use for materials/textures (see advanced-rendering) or model animations (see animations-tweens).
28nft-blockchain
NFT display and blockchain interaction in Decentraland. NftShape (framed NFT artwork), wallet checks (getPlayer, isGuest), signedFetch (authenticated requests), smart contract interaction (eth-connect, createEthereumProvider), and RPC calls. Use when the user wants NFTs, blockchain, wallet, smart contracts, Web3, crypto, or token gating. Do NOT use for player avatar data or emotes (see player-avatar).
27build-ui
Build 2D screen-space UI for Decentraland scenes using React-ECS (JSX). Create HUDs, menus, health bars, scoreboards, dialogs, buttons, inputs, and dropdowns. Use when the user wants screen overlays, on-screen UI, HUD elements, menus, or form inputs. Do NOT use for 3D in-world text (see advanced-rendering) or clickable 3D objects (see add-interactivity).
26advanced-input
Advanced input handling in Decentraland. PointerLock (cursor capture state), InputModifier (freeze/restrict player movement), PrimaryPointerInfo (cursor position and world ray), WASD keyboard patterns, and action bar slots. Use when the user wants movement restriction, cursor control, FPS controls, input polling, or cutscene freezing. Do NOT use for basic click/hover events on entities (see add-interactivity).
26