Three.js plant instancing benchmark: 240 desktop samples, CSV/JSON and a standalone harness

I put together a reproducible plant-rendering benchmark for Three.js 0.186.1: 240 desktop attempts across 48 cells, with per-run CSV/JSON, raw frame intervals and a standalone harness.

Data, methods and harness: Three.js Plant Instancing Benchmark: 1k to 100k | Digital Garden by Alicia

The fixture is one owned Houdini grape hyacinth (6,056 triangles), repeated at 1k, 10k and 100k counts. It compares:

  • classic WebGLRenderer;
  • WebGPURenderer on an inspected, actual WebGPU backend;
  • WebGPURenderer with its forced WebGL2 backend.

InstancedMesh and BatchedMesh run at all three counts. Separate Mesh objects run at 1k and 10k only; there is no 100k separate-object result.

A small slice of the results

Static camera, InstancedMesh; each entry is the median of five per-run mean-FPS values:

Plants Classic WebGL Native WebGPU Node WebGL2
1,000 59.99 59.99 59.99
10,000 59.99 59.99 59.99
100,000 20.26 20.15 20.24

The 1k/10k rows hit presentation pacing, so those numbers do not establish equal maximum throughput or a WebGPU victory. The 100k workload deliberately submits about 605.6 million triangles before the node output pass. This is a stress fixture, not a production plant budget.

A more interesting detail is BatchedMesh’s renderer counters. At 1k/static, the classic path reported 1 render call, native WebGPU 1,001, and node WebGL2 2. The node totals include an output pass. These are renderer counters, not direct JavaScript API-call counts, and the result is specific to this pinned renderer revision. A batch class name alone does not guarantee a single underlying draw on every backend.

What I controlled

  • RTX 5070 Ti, Ryzen 9 9950X, Chromium 155 on Windows; 1280×720, DPR 1, antialiasing off.
  • Same seeded layout, double-sided unlit vertex colours and matched simple GLSL/TSL wind; identity rotations avoid a transform-order mismatch found in an earlier rejected comparison.
  • Static and fixed-orbit cameras. All frustum culling is disabled; BatchedMesh culling/sorting is disabled too, to keep the submitted set equal.
  • Fresh visible document per attempt; warm-up of at least 5 seconds and 300 frames. Measurement ends at 30 seconds or 1,800 frame intervals; five repeats, alternating backend order.

All 240 formal attempts passed timing/visibility checks. The archive retains failure fields rather than defining failure as a zero-FPS success. CPU timings wrap renderer.render(); separate GPU diagnostics are not interchangeable with CPU submission time or total VRAM. Other applications remained open, and I have no physical-phone results.

The linked page includes the fixture/harness download and checksummed v1 data. Measurement data is CC BY 4.0; the private garden engine and seasonal production assets are excluded.

I’d be interested in methodological feedback, especially on interpreting BatchedMesh counters across these three paths or on reproducing the fixture on different GPUs.

Disclosure: Codex executed the harness and helped prepare this write-up for Alicia; the fixture and measurement records are from this project.