DocumentationPerformance
Documentation

Performance

Measured in desktop Chromium on the town scenes coding agents wrote and on the kit's explorable town. The numbers describe these scenes, not yours: measure your own with town.optimization.stats() and renderer.info.render.

Desktop only

Desktop Chromium only; frame times are reported, not judged (SwiftShader is software rendering on a shared machine: relative cost only). No mobile claim.

Scene optimization (SDK 0.5.1)

library.assembleScene(scene, { optimize: true }) (Three.js) or <OptimizedScene> (React Three Fiber) draws every mesh that at least two instances share with GPU instancing. Only the draw calls change; triangles are counted per instance either way.

Agent-written towns (10)
169–264 → 63–93 — draw calls, −55.6 % to −69.6 %
Explorable town (161 instances)
621 → 148 — draw calls, −76.2 %
Largest pixel difference
0.070 % — of the pixels; pass condition <= 0.1 % pixels > 16/255

The draw counts are identical in every run (Host GPU and Software rendering (SwiftShader)).

Host GPU Passed

ANGLE (Apple, ANGLE Metal Renderer: Apple M2 Pro, Unspecified Version), Apple M2 Pro, 1280x800, 120 frames per measurement, measured 2026-10-26. API: library.assembleScene(scene, { optimize: true }) (runtime-three optimizeScene, preset 'large-scene').

SceneInstancesDraw callsChangePixelsFrame ms
Claude Code · with MCP
run of 2026-10-24 08:00
31212 → 87−59.0 %0.021 %1.6 / 1.5
Claude Code · with MCP
run of 2026-10-24 12:53
34217 → 93−57.1 %0.025 %1.7 / 1.5
Claude Code · without MCP
run of 2026-10-24 08:05
36222 → 91−59.0 %0.031 %1.7 / 1.6
Claude Code · without MCP
run of 2026-10-24 12:57
52264 → 91−65.5 %0.032 %1.8 / 1.7
OpenAI Codex CLI · with MCP
run of 2026-10-24 08:09
25198 → 75−62.1 %0.031 %1.7 / 1.5
OpenAI Codex CLI · with MCP
run of 2026-10-24 08:16
21169 → 63−62.7 %0.027 %1.5 / 1.5
OpenAI Codex CLI · with MCP
run of 2026-10-24 13:03
21175 → 67−61.7 %0.028 %1.5 / 1.4
OpenAI Codex CLI · with MCP
run of 2026-10-24 13:07
28207 → 63−69.6 %0.036 %1.6 / 1.5
OpenAI Codex CLI · without MCP
run of 2026-10-24 08:18
31221 → 88−60.2 %0.030 %1.8 / 1.1
OpenAI Codex CLI · without MCP
run of 2026-10-24 13:09
22180 → 80−55.6 %0.024 %1.6 / 1.5
Explorable town
the kit scene
161621 → 148−76.2 %0.070 %2.2 / 2.1

Software rendering (SwiftShader) Passed

ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (LLVM 10.0.0) (0x0000C0DE)), SwiftShader driver), Apple M2 Pro, 1280x800, 40 frames per measurement, measured 2026-10-26. API: library.assembleScene(scene, { optimize: true }) (runtime-three optimizeScene, preset 'large-scene').

SceneInstancesDraw callsChangePixelsFrame ms
Claude Code · with MCP
run of 2026-10-24 08:00
31212 → 87−59.0 %0.003 %50.5 / 51
Claude Code · with MCP
run of 2026-10-24 12:53
34217 → 93−57.1 %0.009 %46 / 45.6
Claude Code · without MCP
run of 2026-10-24 08:05
36222 → 91−59.0 %0.016 %50.5 / 48.6
Claude Code · without MCP
run of 2026-10-24 12:57
52264 → 91−65.5 %0.006 %54.8 / 55.8
OpenAI Codex CLI · with MCP
run of 2026-10-24 08:09
25198 → 75−62.1 %0.002 %48.1 / 46.9
OpenAI Codex CLI · with MCP
run of 2026-10-24 08:16
21169 → 63−62.7 %0.022 %44.7 / 44.3
OpenAI Codex CLI · with MCP
run of 2026-10-24 13:03
21175 → 67−61.7 %0.009 %42.9 / 42.3
OpenAI Codex CLI · with MCP
run of 2026-10-24 13:07
28207 → 63−69.6 %0.022 %45.8 / 48.2
OpenAI Codex CLI · without MCP
run of 2026-10-24 08:18
31221 → 88−60.2 %0.008 %44.5 / 47.6
OpenAI Codex CLI · without MCP
run of 2026-10-24 13:09
22180 → 80−55.6 %0.011 %41.5 / 77.9
Explorable town
the kit scene
161621 → 148−76.2 %0.028 %187 / 117.2

Draw calls: per frame, plain → optimized. Pixels: share of the frame that differs by more than 16/255 between the plain and the optimized render. Frame ms: median render and GPU sync, plain / optimized — reported, not judged.

What was judged

  • draw calls: agent-written towns: optimized <= 50 % of plain; every other scene: fewer draw calls (the reduction is recorded)
  • pixels: <= 0.1 % pixels > 16/255
  • picking: identical first visible hit
  • semantics: customize after optimize, clip; variants scenes: parts picked, both variants batched
  • frame time: reported, not judged.

Fewer draw calls cut CPU and driver work per frame. Whether that makes frames faster on slower or mobile GPUs was not measured, so it is not claimed.

When not to batch an instance

The optimization keeps the SDK’s behaviour by itself: playClip and town.customize hand an instance back and batch it again, and an instance you move, hide or give another material is drawn by its own objects from that frame on. Leave an instance out of the batches when your code changes it often or decorates it per object:

Your instance …If it is batchedDo this
is animated by your own code (a mixer, a tween — not playClip)handed back on the first changed frame (same image), batched again after it rests; every rest rebuilds all batchesexclude it
gets its own material at run time (hover or selection highlight, a fade)handed back on the frame you swap the material; a rebuild after each restexclude it, or unbatch(id) while highlighted
carries a per-object render effect: onBeforeRender, customDepthMaterial, an outline or selection pass, a picking render that draws meshes one by onenot detected: while batched, its meshes are invisible to the renderer (a batch draws them), so these effects never see themexclude it
is dragged or edited interactively (gizmos)handed back while it moves; a rebuild after each dropexclude it, or unbatch(id) when it is picked up
is re-customized many times in a row (a configurator on one building)every town.customize rebuilds all batchesoccasional edits need nothing; otherwise exclude it, or use strict
Three.js: exclude by scene instance id, or batch only unmodified instances
const town = await library.assembleScene(sceneJson, { optimize: { exclude: ['windmill-1', 'house-3'] } });
// or { optimize: 'strict' }: only unmodified, clip-free instances are batched; customized ones keep their own meshes
React Three Fiber: the instanceIds of the <Asset>s to leave out
<OptimizedScene exclude={['windmill-1']}>
  {instances.map((i) => <Asset key={i.instance_id} instanceId={i.instance_id} assetId={i.asset} customize={Object.keys(i.values).length ? i.values : undefined} position={i.position_m} quaternion={i.quaternion_xyzw} />)}
</OptimizedScene>
// or <OptimizedScene preset="strict">
  • Exclude by instance id. An exclusion names the scene instance, so it still applies after town.customize(id, …) replaces the instance. SDK 0.5.1 also accepts the instance’s LoadedAsset (with SDK 0.5.0 only an id survives town.customize).
  • strict keeps every customized or clip-bearing instance on its own meshes and still batches the repeated tiles and props; it saves fewer draw calls than the default large-scene.
  • Picking needs no exclusion. Raw raycasts return exactly the hits they return without optimization, and town.optimization.pick(raycaster) resolves the first drawn hit to its instance and mounted part.

GLB optimization: nothing shipped

Quantized and meshopt-compressed variants of the kit's 30 reference GLBs were built and measured. Decision: no variant is shipped — not every condition holds for any variant (see conditions); the reference GLB stays the only published file. Loading a variant (loadAsset(ref, { variant: 'optimized' })) is refused with VARIANT_NOT_AVAILABLE.

Host GPU

ANGLE (Apple, ANGLE Metal Renderer: Apple M2 Pro, Unspecified Version). References: decode 24.6 ms for all 30, town frame 2.2 ms; variants: decode 25.9 / 26.6 / 26.8 / 26.8 ms, frame 2.2 / 2.2 / 2.2 / 2.3 ms (in the table's order). Measured 2026-10-26.

VariantRawgzipbrotliPixelsVertex shift
Reference GLBs
30, published
3.76 MB744 KB513 KB——
quantize2.72 MB
−27.6 %
832 KB
+11.9 %
549 KB
+6.9 %
0.186 %0.62 mm
meshopt-lossless2.46 MB
−34.4 %
1.02 MB
+39.7 %
889 KB
+73.2 %
0.000 %0 mm
meshopt1.27 MB
−66.3 %
584 KB
−21.5 %
514 KB
+0.1 %
0.186 %0.62 mm
meshopt-high1.07 MB
−71.6 %
483 KB
−35.0 %
434 KB
−15.4 %
0.186 %0.62 mm

Conditions each variant does not meet

  • quantize: KHR_mesh_quantization: positions 14 bit (per mesh volume), normals 10 bit; no decoder
    • bytes-smaller: raw 2853108 vs 3939716 B (-27.6 %), gzip 852295 vs 761807 (11.9 %), brotli 561970 vs 525815 (6.9 %)
    • image-equivalent: 0.1860 % of pixels differ by more than 16/255 from the reference render (max delta 157)
    • geometry-within-slot-tolerance:
      largest vertex displacement 0.62 mm (normals 0.087 deg); the published slot-consistency gate allows 0.1 mm between a host's baked part and the part… Show alllargest vertex displacement 0.62 mm (normals 0.087 deg); the published slot-consistency gate allows 0.1 mm between a host's baked part and the part asset, i.e. 0.05 mm per side
  • meshopt-lossless: EXT_meshopt_compression codec only (no quantization, no reorder): bit-exact data; MeshoptDecoder required
    • bytes-smaller: raw 2584328 vs 3939716 B (-34.4 %), gzip 1064515 vs 761807 (39.7 %), brotli 910468 vs 525815 (73.2 %)
    • no-decoder-needed:
      needs MeshoptDecoder (three/examples/jsm/libs/meshopt_decoder.module.js, 29 KB, self-hostable); the SDK loads baseline GLBs only (DECODER_REQUIRED /… Show allneeds MeshoptDecoder (three/examples/jsm/libs/meshopt_decoder.module.js, 29 KB, self-hostable); the SDK loads baseline GLBs only (DECODER_REQUIRED / VARIANT_NOT_AVAILABLE) and would need a decoder path plus tests
  • meshopt: gltf-transform meshopt level 'medium': reorder + quantize (14/10 bit) + codec; MeshoptDecoder required
    • bytes-smaller: raw 1328268 vs 3939716 B (-66.3 %), gzip 598141 vs 761807 (-21.5 %), brotli 526596 vs 525815 (0.1 %)
    • image-equivalent: 0.1860 % of pixels differ by more than 16/255 from the reference render (max delta 157)
    • geometry-within-slot-tolerance:
      largest vertex displacement 0.62 mm (normals 0.087 deg); the published slot-consistency gate allows 0.1 mm between a host's baked part and the part… Show alllargest vertex displacement 0.62 mm (normals 0.087 deg); the published slot-consistency gate allows 0.1 mm between a host's baked part and the part asset, i.e. 0.05 mm per side
    • no-decoder-needed:
      needs MeshoptDecoder (three/examples/jsm/libs/meshopt_decoder.module.js, 29 KB, self-hostable); the SDK loads baseline GLBs only (DECODER_REQUIRED /… Show allneeds MeshoptDecoder (three/examples/jsm/libs/meshopt_decoder.module.js, 29 KB, self-hostable); the SDK loads baseline GLBs only (DECODER_REQUIRED / VARIANT_NOT_AVAILABLE) and would need a decoder path plus tests
  • meshopt-high: gltf-transform meshopt level 'high': as medium + 8-bit octahedral normals, filtered animation data; MeshoptDecoder required
    • image-equivalent: 0.1860 % of pixels differ by more than 16/255 from the reference render (max delta 157)
    • animations:
      clips unchanged by name and target; largest keyframe rotation change 0.0036 deg (limit 0.01), largest translation/scale/weight change 0.000453 (limit… Show allclips unchanged by name and target; largest keyframe rotation change 0.0036 deg (limit 0.01), largest translation/scale/weight change 0.000453 (limit 1e-6)
    • geometry-within-slot-tolerance:
      largest vertex displacement 0.62 mm (normals 1.025 deg); the published slot-consistency gate allows 0.1 mm between a host's baked part and the part… Show alllargest vertex displacement 0.62 mm (normals 1.025 deg); the published slot-consistency gate allows 0.1 mm between a host's baked part and the part asset, i.e. 0.05 mm per side
    • no-decoder-needed:
      needs MeshoptDecoder (three/examples/jsm/libs/meshopt_decoder.module.js, 29 KB, self-hostable); the SDK loads baseline GLBs only (DECODER_REQUIRED /… Show allneeds MeshoptDecoder (three/examples/jsm/libs/meshopt_decoder.module.js, 29 KB, self-hostable); the SDK loads baseline GLBs only (DECODER_REQUIRED / VARIANT_NOT_AVAILABLE) and would need a decoder path plus tests

Publishing conditions

  • Passed reference-unchanged: all 30 reference GLBs re-hashed after the run: equal to their manifest pins (they were only read)
  • Failed own-sha256-and-manifest-entry: Publishing a variant adds an entry to the asset manifest, which changes the bytes a published version pins. A variant therefore ships as a new asset version, never as an edit of an existing one.
  • Failed variant-passes-its-own-qa: The quality gates validate the reference GLB only. No gate validates an optimized variant yet (decoder setup, slot consistency on quantized geometry, customization on the variant).
Software rendering (SwiftShader) run (same decision: nothing shipped)

ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (LLVM 10.0.0) (0x0000C0DE)), SwiftShader driver), measured 2026-10-26. not every condition holds for any variant (see conditions); the reference GLB stays the only published file.

VariantRawgzipbrotliPixelsVertex shift
Reference GLBs
30, published
3.76 MB744 KB513 KB——
quantize2.72 MB
−27.6 %
832 KB
+11.9 %
549 KB
+6.9 %
0.130 %0.62 mm
meshopt-lossless2.46 MB
−34.4 %
1.02 MB
+39.7 %
889 KB
+73.2 %
0.000 %0 mm
meshopt1.27 MB
−66.3 %
584 KB
−21.5 %
514 KB
+0.1 %
0.130 %0.62 mm
meshopt-high1.07 MB
−71.6 %
483 KB
−35.0 %
434 KB
−15.4 %
0.130 %0.62 mm

Conditions each variant does not meet

  • quantize: KHR_mesh_quantization: positions 14 bit (per mesh volume), normals 10 bit; no decoder
    • bytes-smaller: raw 2853108 vs 3939716 B (-27.6 %), gzip 852295 vs 761807 (11.9 %), brotli 561970 vs 525815 (6.9 %)
    • image-equivalent: 0.1299 % of pixels differ by more than 16/255 from the reference render (max delta 157)
    • geometry-within-slot-tolerance:
      largest vertex displacement 0.62 mm (normals 0.087 deg); the published slot-consistency gate allows 0.1 mm between a host's baked part and the part… Show alllargest vertex displacement 0.62 mm (normals 0.087 deg); the published slot-consistency gate allows 0.1 mm between a host's baked part and the part asset, i.e. 0.05 mm per side
  • meshopt-lossless: EXT_meshopt_compression codec only (no quantization, no reorder): bit-exact data; MeshoptDecoder required
    • bytes-smaller: raw 2584328 vs 3939716 B (-34.4 %), gzip 1064515 vs 761807 (39.7 %), brotli 910468 vs 525815 (73.2 %)
    • no-decoder-needed:
      needs MeshoptDecoder (three/examples/jsm/libs/meshopt_decoder.module.js, 29 KB, self-hostable); the SDK loads baseline GLBs only (DECODER_REQUIRED /… Show allneeds MeshoptDecoder (three/examples/jsm/libs/meshopt_decoder.module.js, 29 KB, self-hostable); the SDK loads baseline GLBs only (DECODER_REQUIRED / VARIANT_NOT_AVAILABLE) and would need a decoder path plus tests
  • meshopt: gltf-transform meshopt level 'medium': reorder + quantize (14/10 bit) + codec; MeshoptDecoder required
    • bytes-smaller: raw 1328268 vs 3939716 B (-66.3 %), gzip 598141 vs 761807 (-21.5 %), brotli 526596 vs 525815 (0.1 %)
    • image-equivalent: 0.1299 % of pixels differ by more than 16/255 from the reference render (max delta 157)
    • geometry-within-slot-tolerance:
      largest vertex displacement 0.62 mm (normals 0.087 deg); the published slot-consistency gate allows 0.1 mm between a host's baked part and the part… Show alllargest vertex displacement 0.62 mm (normals 0.087 deg); the published slot-consistency gate allows 0.1 mm between a host's baked part and the part asset, i.e. 0.05 mm per side
    • no-decoder-needed:
      needs MeshoptDecoder (three/examples/jsm/libs/meshopt_decoder.module.js, 29 KB, self-hostable); the SDK loads baseline GLBs only (DECODER_REQUIRED /… Show allneeds MeshoptDecoder (three/examples/jsm/libs/meshopt_decoder.module.js, 29 KB, self-hostable); the SDK loads baseline GLBs only (DECODER_REQUIRED / VARIANT_NOT_AVAILABLE) and would need a decoder path plus tests
  • meshopt-high: gltf-transform meshopt level 'high': as medium + 8-bit octahedral normals, filtered animation data; MeshoptDecoder required
    • image-equivalent: 0.1297 % of pixels differ by more than 16/255 from the reference render (max delta 157)
    • animations:
      clips unchanged by name and target; largest keyframe rotation change 0.0036 deg (limit 0.01), largest translation/scale/weight change 0.000453 (limit… Show allclips unchanged by name and target; largest keyframe rotation change 0.0036 deg (limit 0.01), largest translation/scale/weight change 0.000453 (limit 1e-6)
    • geometry-within-slot-tolerance:
      largest vertex displacement 0.62 mm (normals 1.025 deg); the published slot-consistency gate allows 0.1 mm between a host's baked part and the part… Show alllargest vertex displacement 0.62 mm (normals 1.025 deg); the published slot-consistency gate allows 0.1 mm between a host's baked part and the part asset, i.e. 0.05 mm per side
    • no-decoder-needed:
      needs MeshoptDecoder (three/examples/jsm/libs/meshopt_decoder.module.js, 29 KB, self-hostable); the SDK loads baseline GLBs only (DECODER_REQUIRED /… Show allneeds MeshoptDecoder (three/examples/jsm/libs/meshopt_decoder.module.js, 29 KB, self-hostable); the SDK loads baseline GLBs only (DECODER_REQUIRED / VARIANT_NOT_AVAILABLE) and would need a decoder path plus tests

Demo budgets of the explorable town

Measured on the vanilla demo and the R3F example (161 instances) with the explicit recipe below. The budgets describe the shipped scene, not yours: your customizations change the totals. No mobile performance is claimed: real devices were not measured.

Host GPU Passed

ANGLE (Apple, ANGLE Metal Renderer: Apple M2 Pro, Unspecified Version), Apple M2 Pro, 1280x800, measured 2026-10-26.

BudgetMeasuredLimitStatus
Triangles drawn (highest)
vanilla demo
138,034150,000Passed
Draw calls (highest along the camera path)
vanilla demo
148200Passed
Transferred until ready
vanilla demo
5.30 MB12.0 MBPassed
Triangles drawn (highest)
R3F example
138,034150,000Passed
Draw calls (highest along the camera path)
R3F example
148200Passed
Transferred until ready
R3F example
5.45 MB12.0 MBPassed

Draw calls by setup (max over the camera path)

Vanilla demo, no instancing621
Vanilla demo, instanceStatic with SDK defaults260
Vanilla demo, instanceStatic with the documented opt-ins148
R3F example, no instancing, live shadow map1,241
R3F example, instanceStatic + static shadow map148

Software rendering (SwiftShader) Passed

ANGLE (Google, Vulkan 1.3.0 (SwiftShader Device (LLVM 10.0.0) (0x0000C0DE)), SwiftShader driver), Apple M2 Pro, 1280x800, measured 2026-10-26.

BudgetMeasuredLimitStatus
Triangles drawn (highest)
vanilla demo
138,034150,000Passed
Draw calls (highest along the camera path)
vanilla demo
148200Passed
Transferred until ready
vanilla demo
5.31 MB12.0 MBPassed
Triangles drawn (highest)
R3F example
138,034150,000Passed
Draw calls (highest along the camera path)
R3F example
148200Passed
Transferred until ready
R3F example
5.45 MB12.0 MBPassed

Draw calls by setup (max over the camera path)

Vanilla demo, no instancing621
Vanilla demo, instanceStatic with SDK defaults260
Vanilla demo, instanceStatic with the documented opt-ins148
R3F example, no instancing, live shadow map1,241
R3F example, instanceStatic + static shadow map148

The explicit recipe

{ optimize: true } is this recipe in one option, plus automatic hand-back and re-batching. The explicit form still works and keeps its behaviour: batched meshes snapshot transforms, so call unbatch(id) before you change an instance yourself. An instance is batched by at most one helper at a time.

Batch repeated meshes (opt-in), keep a static shadow map
import { instanceStatic } from '@agent-ready-assets/runtime-three';

const town = await library.assembleScene(sceneJson);
const batches = instanceStatic(town, { includeCustomized: true, includeAnimatable: true });
scene.add(town.group);
renderer.shadowMap.autoUpdate = false; // set renderer.shadowMap.needsUpdate = true when something changes
batches.stats(); // { drawsBefore, drawsAfter, triangles, ... }
// before moving or hiding one instance yourself: batches.unbatch(id); on teardown: batches.dispose(); town.release();

Instancing reduces draw calls only; triangles are counted per instance either way. Measure your own scene with renderer.info.render and stats().

What a choice costs

Asset · choiceTrianglesDraw callsInstanced
stylized-town/bench-01@0.4.0 defaults1,01266
armrests: false836 (-176)44
stylized-town/cafe-01@0.1.0 defaults6,0201414
front-door: door-015,888 (-132)1414
awning: false5,648 (-372)1212
terrace: false3,924 (-2,096)1111
stylized-town/house-01@0.2.0 defaults6,1441313
front-door: door-026,276 (+132)1515
window-style: window-0224,624 (+18,480)5614
roof: roof-hip-017,320 (+1,176)1313
chimney: false6,056 (-88)1212
stylized-town/house-02@0.1.0 defaults14,6081414
front-door: door-0114,476 (-132)1414
window-style: window-015,984 (-8,624)2513
roof: roof-gable-0113,432 (-1,176)1414
stylized-town/shop-01@0.2.0 defaults3,2641111
front-door: door-013,132 (-132)1111
awning: false2,892 (-372)99
stylized-town/street-lamp-01@0.3.0 defaults1,15855
light: false1,114 (-44)44
stylized-town/townhall-01@0.1.0 defaults10,02499
flag: false9,888 (-136)77