The Physics of Memory
I’ve heard that many algorithms, like sorting, are improved quite a bit when the accessed data has high memory-locality (e.g. the CPU can load all of the relevant information in it’s L1/L2 cache for processing, instead of hitting RAM). A common way to work with this is using the Entity Component System (ECS) software architecture. However, it can be difficult to achieve this memory layout for interpreted or OOP languages (Java, Python, Javscript), as they often don’t offer the developer as much control over their object memory location. My question is:
Is it possible to use an ECS-style architecture in Javascript? And for applicable operations, does that actually do better than objects + V8’s garbage collection?
To test this, I created a complex-enough benchmark of… “balls bouncing around in a box”! This follows standard 2d physics engine techniques:
- A “broad-phase” collision step (AKA “do bounding boxes intersect?”)
- A “narrow-phase” collision step (AKA “given these intersecting boxes, resolve collisions!”)
And of course, I got very carried away and ended up creating a ton of benchmark types below. Try it!
Background - What is ECS?
If you’ve spent any time in game development, you’ve probably heard of Entity Component System (ECS). If OOP is a row-based database (where each object stores every attribute in one memory chunk), ECS is a column-based database, storing each attribute (or collection of attributes) in separate column arrays.
Put another way (and ou can probably find much better descriptions of ECS elsewhere)… instead of building complex class hierarchies like a GameEntity that inherits from MovingObject which inherits from PhysicalObject etc, ECS breaks everything down into three distinct concepts:
- Entities: These are nothing more than unique integer IDs. They don’t contain any data or behavior.
- Components: Plain data structures that represent individual facets of state (e.g.
Position,Velocity,Color). They contain no logic. Each entity can have a dynamic set of components, from 0 to all. - Systems: Functions or loops that execute the game logic. They query entities given a specific set of components to then operate on that data.
Benchmark Choices
I went overboard and tested across five dimensions:
- Language: Javascript (testing V8 engine optimizations & TypedArrays) vs. WASM (AssemblyScript for extreme memory control).
- Architecture: Traditional OOP (arrays of object references) vs. ECS (cache-friendly Structure of Arrays).
- Broad-phase: Spatial AVL Tree ( balanced tree) vs. Sweep & Prune (sort on X-axis, sweep for overlaps).
- Sorting Strategy (for S&P): Tested Insertion Sort (the textbook favorite for nearly-sorted data), Hybrid Quicksort, a zero-copy Hybrid Mergesort, and JS Native
Array.sort(). - Motion Coherence: Wandering (smooth movement; array stays nearly sorted), Erratic (random teleporting; extreme cache thrashing), and Static (zero motion but with permanent collisions; isolates pure memory read speed with some collisions).
Anyways, let’s see how it works! This is hosted on GitHub if you want to play around with it.
Macbook M4 Results
Running all these configurations locally on my Apple M4 Pro chip (plugged in, 200 frames) creates a lot of numbers. If you want the full dataset of all 6 runs, check out the Appendix: Full Benchmark Dataset below.
To cut through information overload, let’s focus on 3 main hypotheses:
H1: Spatial Trees vs. Sweep & Prune
Contiguous 1D memory sweeps consistently outperform hierarchical 2D spatial trees, even when both are stored in flat arrays.
At 15,000 wandering entities, the flat array Sweep & Prune (WASM ECS S&P Quick) runs in 1.81 ms (a 9.02x speedup over the baseline), while the flat array spatial tree (ECS Tree) runs in 9.97 ms (only a 1.64x speedup).
| Broad-phase System | Architecture | Avg Frame Time | 99th Percentile | Speedup vs OOP Tree |
|---|---|---|---|---|
| WASM ECS S&P (Quick) | Flat 1D Array | 1.809 ms | 2.031 ms | 9.02x |
| ECS (Custom SoA) S&P (Quick) | Flat 1D Array | 4.668 ms | 6.111 ms | 3.50x |
| ECS Tree | 2D Spatial BVH | 9.970 ms | 43.666 ms | 1.64x |
| OOP Tree | 2D Spatial BVH | 16.321 ms | 40.756 ms | 1.00x |
Why is the tree so much slower, even though it uses flat arrays and avoids JavaScript heap objects?
- Nested Indirection: While Sweep & Prune has a single layer of index indirection (
posX[indices[i]]), the AABB Tree requires nested indirection. During traversal, the CPU must look up a child index (treeLeft[idx]) and then use that to look up the node’s bounds (treeMinX[leftChild]). This non-linear jumping across arrays prevents the CPU’s hardware prefetcher from predicting the next memory address. - Branch Misprediction: The tree traversal loop is filled with conditional branches (e.g., deciding whether to descend left, right, or both based on overlap). At 15,000 moving entities, these branches are highly unpredictable, causing frequent CPU instruction pipeline flushes. Sweep & Prune, by contrast, is a simple, highly predictable linear sweep.
- Temporal Coherence: Because entities only move a tiny amount frame-to-frame (high spatial coherence), the sorted
indicesin Sweep & Prune remain nearly contiguous. This keeps index lookups highly localized in the cache, whereas tree traversal always jumps across distant nodes.
H2: JavaScript vs. WebAssembly
WebAssembly is not required to realize the cache-locality benefits of Data-Oriented Design, but it provides a major secondary boost by enabling unchecked memory access.
Simply migrating from OOP (Arrays of Objects) to ECS (flat TypedArrays) in pure JavaScript yields a 1.58x to 24.9x speedup depending on spatial coherence.
| Runtime & Memory Layout | Contender | Avg Frame Time | Speedup vs OOP |
|---|---|---|---|
| WebAssembly (SoA) | WASM ECS S&P (Quick) | 2.141 ms | 3.91x |
| JavaScript (SoA) | ECS (Custom SoA) S&P (Quick) | 5.318 ms | 1.58x |
| JavaScript (AoS) | OOP S&P (Quick) | 8.380 ms | 1.00x |
Moving to AssemblyScript WASM provides an additional ~2.5x speedup (down to 2.11 ms). The primary driver of this boost is the ability to use the unchecked() operator. This tells the compiler to bypass array boundary checks, allowing it to emit highly optimized vector instructions and maximize CPU instruction throughput.
Interesting Learnings
There were a number of interesting things I discovered while making this!
1. The Spatial Coherence Sorting Trap (Why we need Hybrid Sorts)
Textbook Insertion Sort is a performance trap for real-time physics; a hybrid sort is required to prevent worst-case frame jank.
Under Wandering (high coherence), the array remains nearly sorted, and Insertion Sort is extremely fast (4.21 ms). However, under Erratic (low coherence) workloads (such as explosions or teleporting), the array becomes fully unsorted. Insertion Sort collapses to its quadratic worst-case (), ballooning frame times to 36.39 ms and causing severe jank.
To solve this, we implemented a Hybrid Sort (Quicksort or Mergesort that switches to Insertion Sort when a subarray falls below 12 elements). This eliminates the recursive stack overhead for small partitions while guaranteeing performance under chaotic motion (5.60 ms).
Why switch to insertion sort when the subarray falls below 12 elements? This eliminates the relatively high call-stack and buffer overhead of divide-and-conquer recursion for tiny arrays, where insertion sort’s near-zero setup overhead is very fast. Changing to this improved performance! (I forget how much though)
2. The Library Trade-off (bitECS vs. Custom SoA)
Using an ECS library is slightly slower than a hand-tuned SoA, but remains highly competitive and is the optimal choice for real-world engineering.
At 15,000 erratic entities, bitECS S&P (Quick) runs in 9.56 ms compared to our custom ECS at 5.60 ms. While 2x slower, it still delivers a massive 14x speedup over the OOP baseline (131.6 ms).
Why is bitECS slower?
- No Property Packing: In our ECS (Custom SoA), we pack
y,w, andhinto a singleposYwharray (posYwh[i * 3 + 0]), meaning all three values load into a single CPU cache line. By default,bitECSallocates a separate TypedArray for every property in a component (y,w, andhare separate arrays), forcing the CPU to fetch from three separate memory streams. - Query & ID Mapping Overhead: bitECS must query archetypes and map sparse Entity IDs to internal array slots, introducing minor indirection.
However, for a real game, the usability, API safety, and dynamic entity management of a library like bitECS are far more valuable than saving a few milliseconds of micro-optimization.
Plus, this can all go away by doing one of the performance optimization tips below, and extracting the physics data to a custom structure before operating on it.
3. The JavaScript Float32 Gotcha
Switching to 32-bit floats (Float32Array) in pure JavaScript degrades performance due to V8’s double-precision promotion overhead.
At 15,000 entities, the Float32Array implementation of our ECS (Custom SoA) was ~5% slower than the Float64Array version (5.60 ms vs. 5.32 ms).
Why? JavaScript numbers are always 64-bit doubles. When V8 reads from a Float32Array, it must convert the 32-bit float to a 64-bit double at runtime before performing math. When writing back, it must convert it back. The overhead of these runtime conversions outweighs the L1/L2 cache density gains.
In native environments (like WASM, C++, or Rust), f32 is processed directly on FPU registers without conversion, making it a pure performance win.
4. The Physics of Determinism
Cross-algorithm trajectory determinism is a false economy once contact resolution order diverges.
Because trees and sorted sweeps traverse and resolve contacts in different orders, their rounding errors accumulate differently. This causes the simulation paths to diverge almost immediately (by frame 2). Focus instead on statistical correctness and local stability rather than matching trajectories exactly across different paradigms.
5. DOD and Frame-Time Stability (The 99th Percentile)
Data-Oriented Design provides rock-solid frame rate stability by eliminating garbage collection and cache-miss spikes.
If you look at the 99th percentile numbers in our benchmarks, a striking pattern emerges: in the OOP implementations, the 99th percentile is often 2x to 3x higher than the average frame time. In the ECS (Custom SoA) and WASM implementations, the 99th percentile is almost identical to the average.
In OOP, entities are scattered across the heap. As they move and interact, the JavaScript engine’s garbage collector is constantly triggered, and the CPU frequently stalls waiting for pointer lookups. This causes sporadic frame drops (micro-stutter). Because ECS uses pre-allocated, flat TypedArrays, memory access is 100% predictable and GC overhead is zero, guaranteeing perfectly smooth frame delivery.
🏆 Grand Finale
Question: What seems to be the fastest reasonably architected code that we can come up with?
Verdict: Pitting the top spatial trees (WASM Tree, JS ECS Tree) against the Sweep & Prune champions (WASM Quick S&P, JS Quick S&P) across 15,000 chaotic colliding bodies:
| Contender | Paradigm | Architecture | Runtime (Erratic) | Verdict |
|---|---|---|---|---|
| WASM Quick S&P | ECS | 1D Flat Array | 2.14 ms | 🏆 Winner (~7.9x faster than WASM Tree) |
| JS Quick S&P | ECS | 1D Flat Array | 5.32 ms | Best pure Javascript engine |
| WASM Tree | ECS | 2D Spatial BVH | 16.98 ms | Peak spatial tree performance |
| JS ECS Tree | ECS | 2D Spatial BVH | 16.64 ms | JS spatial tree baseline |
My Takeaways:
- Cache Locality > Algorithmic Complexity: At 15,000 entities, pointer-chasing and unpredictable tree branching cannot compete with the contiguous L1/L2 cache locality of a flat 1D array sort—even though trees have a better theoretical Big-O complexity.
- You Don’t Need WASM for ECS Wins: Simply switching your JavaScript codebase to a flat Structure of Arrays (SoA) layout yields up to a 24x speedup over OOP. WASM is the cherry on top (another 2.5x), not the entry ticket.
- Pragmatism Wins: While a hand-tuned SoA is the absolute fastest, using a production ECS library like
bitECSstill gives you a massive 14x speedup over OOP while providing a clean, scalable API. IMO, for 99% of applications using a library is the correct engineering choice.
Potential Improvements
This section outlines how to optimize the S&P broadphase even further for production-grade game engines.
1. Eliminating Index Indirection via the ETL Physics Buffer Pattern
We can eliminate index indirection during the Sweep phase by extracting and physically permuting a lightweight physics buffer.
In our simple ECS S&P, the sweep phase must read coordinates indirectly: posX[indices[i]]. While we could physically sort all of our component arrays to make this contiguous, doing so in a real game is highly impractical because of archetype fragmentation and the waste of permuting non-physics components (like inventories or AI state).
Instead, we can use an Extract-Transform-Load (ETL) Physics Buffer pattern:
- Extract: At the start of the broadphase, we copy only the required spatial fields (
x, y, w, h, id) from the main ECS arrays into a dedicated, flatPhysicsBuffer. - Transform (Sort & Permute): We sort the
indicesof this buffer, and then physically permute only this lightweightPhysicsBuffer. - Contiguous Sweep: The Sweep phase can now read the sorted
PhysicsBuffersequentially (posX[i]instead ofposX[indices[i]]). - Load (Output): The broadphase outputs the resulting contact pairs (Entity IDs) to the narrowphase, leaving the main ECS database completely untouched.
Why this is highly practical:
- Universal Compatibility: Because the data is extracted into an isolated buffer, this pattern is 100% compatible with any ECS library (like bitECS). You simply query the library, populate the buffer, solve, and write back.
- Incredibly Fast Copies: Straight sequential data copying in JavaScript (using
.set()or simple loops) is extremely fast. Modern CPUs are highly optimized for sequential memory copies, easily saturating memory bandwidth while keeping the prefetcher happy. - Measurable Gains: Implementing this in our benchmark yielded an additional ~6% speedup at 15,000 entities (reducing frame times from 5.32 ms to 5.01 ms under erratic loads).
2. Spatial Hash Grids (Alternative Spatial Partitioning)
For uniform-sized entities, a flat-array Spatial Hash Grid can offer average-case performance with zero sorting or tree-balancing overhead.
While we compared Sweep & Prune against a Dynamic AABB Tree, another highly cache-friendly alternative is the Spatial Hash Grid.
- Unlike the AABB Tree (which requires recursive, non-linear tree traversal), a Spatial Hash Grid maps 2D coordinates into a 1D grid array.
- Entities are inserted into grid cells in a single pass, and queries are performed by checking neighboring cells in time.
- This approach is extremely cache-friendly when implemented using flat arrays, and it completely avoids the sorting step of Sweep & Prune, making it highly optimal at very large scales.
3. Multi-Axis Sweep & Prune
Sorting on the axis of highest spatial variance prevents Sweep & Prune from degrading under alignment pathological cases.
Our simple Sweep & Prune only sorts along the X-axis. If many entities align vertically (e.g., falling down a narrow corridor), they all overlap on the X projection, causing the sweep phase to degrade toward .
- A production-grade Sweep & Prune calculates the variance of entity positions along both the X and Y axes.
- It then dynamically chooses to sort and sweep along the axis with the highest variance, minimizing the number of overlapping intervals.
4. WebAssembly SIMD Vectorization
We can use WASM SIMD (Single Instruction Multiple Data) to check four bounding box intersections simultaneously.
Since our PhysicsBuffer stores coordinates contiguously, we can load 128-bit SIMD registers with the bounds of four entities at once. The CPU can then perform the AABB overlap checks in parallel with a single instruction, potentially quadrupling the throughput of the sweep phase.
Am I missing anything?
What am I missing? What other configurations should we add? Did I mess something up?
FAQs
- Why not use Web Workers? Because in a microbenchmark, putting things in Web Workers can actually obscure the real bottlenecks. If we spin up multiple threads, the browser might optimize the main thread in a vacuum, leading to “microbenchmark syndrome.” Keeping it single-threaded makes sure we are directly comparing how the browser’s engine manages memory access under active simulation load.
- Why not C or C++ instead of AssemblyScript? I attempted to use C++ here, but there were two main hurdles that made it slower than AssemblyScript:
- Pointer Aliasing: AssemblyScript allowed me to easily use
unchecked()to disable pointer aliasing protection, which was a bit harder to do with C++ and clang, but definitely not impossible. - Optimized Math Operations: I could not figure out how to get the math library calls faster in C++ to WASM - somehow AssemblyScript links in really fast math calls for sin/cos/pow/etc, and I failed to get that working with C++.
- Pointer Aliasing: AssemblyScript allowed me to easily use
Appendix
Full Benchmark Dataset
For the statistics nerds and performance purists, here is the full, unadulterated dataset of our run of 200 frames across all 18 registered simulator configurations.
1. Static Behavior (Absolute Coherence - 5,000 Entities)
| System | Avg Frame Time | 99th Percentile | Speedup | |
|---|---|---|---|---|
| WASM ECS S&P (Insertion) | 0.148 ms | 0.178 ms | 4.43x | |
| WASM ECS S&P (Merge) | 0.182 ms | 0.226 ms | 3.60x | |
| WASM ECS S&P (Quick) | 0.202 ms | 0.243 ms | 3.24x | |
| ECS (Custom SoA) S&P (Insertion) | 0.434 ms | 0.484 ms | 1.51x | |
| ECS (Custom SoA) S&P (Quick) | 0.445 ms | 0.525 ms | 1.47x | |
| ECS (Custom SoA) S&P (Merge) | 0.445 ms | 0.507 ms | 1.47x | |
| ECS (Custom SoA) S&P (Native) | 0.601 ms | 0.807 ms | 1.09x | |
| OOP S&P (Native) | 0.604 ms | 0.821 ms | 1.08x | |
| OOP S&P (Insertion) | 0.654 ms | 0.912 ms | 1.00x | |
| OOP S&P (Quick) | 0.706 ms | 1.067 ms | 0.93x | |
| OOP S&P (Merge) | 0.720 ms | 1.044 ms | 0.91x | |
| WASM Tree | 0.742 ms | 0.859 ms | 0.88x | |
| ECS Tree | 0.887 ms | 1.052 ms | 0.74x | |
| bitECS S&P (Insertion) | 0.888 ms | 0.967 ms | 0.74x | |
| bitECS S&P (Quick) | 0.957 ms | 1.067 ms | 0.68x | |
| bitECS S&P (Merge) | 1.066 ms | 2.337 ms | 0.61x | |
| OOP Tree | 1.104 ms | 1.782 ms | 0.59x | |
| bitECS S&P (Native) | 1.351 ms | 3.841 ms | 0.48x |
2. Wandering Behavior (High Coherence - 5,000 Entities)
| System | Avg Frame Time | 99th Percentile | Speedup | |
|---|---|---|---|---|
| WASM ECS S&P (Insertion) | 0.318 ms | 0.355 ms | 3.54x | |
| WASM ECS S&P (Merge) | 0.343 ms | 0.483 ms | 3.28x | |
| WASM ECS S&P (Quick) | 0.374 ms | 0.472 ms | 3.01x | |
| ECS (Custom SoA) S&P (Insertion) | 0.845 ms | 0.963 ms | 1.33x | |
| ECS (Custom SoA) S&P (Merge) | 0.856 ms | 1.090 ms | 1.31x | |
| ECS (Custom SoA) S&P (Quick) | 0.858 ms | 0.994 ms | 1.31x | |
| ECS (Custom SoA) S&P (Native) | 1.059 ms | 1.723 ms | 1.06x | |
| OOP S&P (Insertion) | 1.124 ms | 1.676 ms | 1.00x | |
| OOP S&P (Quick) | 1.201 ms | 1.769 ms | 0.94x | |
| OOP S&P (Merge) | 1.325 ms | 1.885 ms | 0.85x | |
| OOP S&P (Native) | 1.379 ms | 2.104 ms | 0.81x | |
| bitECS S&P (Insertion) | 1.588 ms | 2.603 ms | 0.71x | |
| bitECS S&P (Quick) | 1.662 ms | 2.384 ms | 0.68x | |
| bitECS S&P (Merge) | 1.773 ms | 2.556 ms | 0.63x | |
| bitECS S&P (Native) | 1.829 ms | 2.673 ms | 0.61x | |
| WASM Tree | 2.233 ms | 2.614 ms | 0.50x | |
| ECS Tree | 2.353 ms | 2.941 ms | 0.48x | |
| OOP Tree | 2.938 ms | 11.642 ms | 0.38x |
3. Erratic Behavior (Low Coherence - 5,000 Entities)
| System | Avg Frame Time | 99th Percentile | Speedup | |
|---|---|---|---|---|
| WASM ECS S&P (Quick) | 0.462 ms | 0.719 ms | 28.71x | |
| WASM ECS S&P (Merge) | 0.475 ms | 0.740 ms | 27.91x | |
| ECS (Custom SoA) S&P (Quick) | 1.070 ms | 1.605 ms | 12.39x | |
| ECS (Custom SoA) S&P (Merge) | 1.125 ms | 1.637 ms | 11.79x | |
| ECS (Custom SoA) S&P (Native) | 1.487 ms | 2.149 ms | 8.92x | |
| OOP S&P (Quick) | 1.549 ms | 1.884 ms | 8.56x | |
| OOP S&P (Merge) | 1.649 ms | 2.101 ms | 8.04x | |
| bitECS S&P (Quick) | 1.675 ms | 2.174 ms | 7.92x | |
| bitECS S&P (Merge) | 1.743 ms | 2.373 ms | 7.61x | |
| bitECS S&P (Native) | 1.966 ms | 2.413 ms | 6.74x | |
| OOP S&P (Native) | 2.052 ms | 2.765 ms | 6.46x | |
| WASM ECS S&P (Insertion) | 3.433 ms | 4.153 ms | 3.86x | |
| ECS Tree | 4.422 ms | 5.360 ms | 3.00x | |
| WASM Tree | 4.485 ms | 4.973 ms | 2.96x | |
| ECS (Custom SoA) S&P (Insertion) | 4.511 ms | 6.598 ms | 2.94x | |
| OOP Tree | 5.509 ms | 12.983 ms | 2.41x | |
| bitECS S&P (Insertion) | 11.583 ms | 19.861 ms | 1.14x | |
| OOP S&P (Insertion) | 13.258 ms | 18.408 ms | 1.00x |
4. Static Behavior (Absolute Coherence - 15,000 Entities)
| System | Avg Frame Time | 99th Percentile | Speedup | |
|---|---|---|---|---|
| WASM ECS S&P (Insertion) | 0.959 ms | 1.085 ms | 5.43x | |
| WASM ECS S&P (Merge) | 1.112 ms | 1.349 ms | 4.69x | |
| WASM ECS S&P (Quick) | 1.170 ms | 1.449 ms | 4.45x | |
| ECS (Custom SoA) S&P (Insertion) | 3.162 ms | 3.653 ms | 1.65x | |
| ECS (Custom SoA) S&P (Quick) | 3.293 ms | 3.769 ms | 1.58x | |
| ECS (Custom SoA) S&P (Merge) | 3.395 ms | 3.954 ms | 1.53x | |
| WASM Tree | 3.587 ms | 5.116 ms | 1.45x | |
| ECS (Custom SoA) S&P (Native) | 3.902 ms | 4.741 ms | 1.34x | |
| ECS Tree | 4.320 ms | 5.095 ms | 1.21x | |
| OOP S&P (Insertion) | 5.210 ms | 6.260 ms | 1.00x | |
| OOP S&P (Native) | 5.423 ms | 7.281 ms | 0.96x | |
| OOP S&P (Quick) | 5.614 ms | 6.560 ms | 0.93x | |
| OOP S&P (Merge) | 5.995 ms | 9.221 ms | 0.87x | |
| OOP Tree | 6.746 ms | 9.319 ms | 0.77x | |
| bitECS S&P (Quick) | 7.456 ms | 10.520 ms | 0.70x | |
| bitECS S&P (Merge) | 7.447 ms | 9.227 ms | 0.70x | |
| bitECS S&P (Native) | 7.712 ms | 9.912 ms | 0.68x | |
| bitECS S&P (Insertion) | 8.448 ms | 14.122 ms | 0.62x |
5. Wandering Behavior (High Coherence - 15,000 Entities)
| System | Avg Frame Time | 99th Percentile | Speedup | |
|---|---|---|---|---|
| WASM ECS S&P (Insertion) | 1.557 ms | 1.859 ms | 4.71x | |
| WASM ECS S&P (Merge) | 1.767 ms | 2.186 ms | 4.15x | |
| WASM ECS S&P (Quick) | 1.809 ms | 2.031 ms | 4.05x | |
| ECS (Custom SoA) S&P (Insertion) | 4.489 ms | 6.483 ms | 1.63x | |
| ECS (Custom SoA) S&P (Quick) | 4.668 ms | 6.111 ms | 1.57x | |
| ECS (Custom SoA) S&P (Merge) | 4.781 ms | 5.670 ms | 1.53x | |
| ECS (Custom SoA) S&P (Native) | 5.658 ms | 8.731 ms | 1.30x | |
| OOP S&P (Insertion) | 7.335 ms | 9.044 ms | 1.00x | |
| OOP S&P (Merge) | 7.540 ms | 9.290 ms | 0.97x | |
| OOP S&P (Quick) | 7.645 ms | 10.421 ms | 0.96x | |
| OOP S&P (Native) | 8.036 ms | 11.292 ms | 0.91x | |
| bitECS S&P (Quick) | 9.077 ms | 10.931 ms | 0.81x | |
| WASM Tree | 9.083 ms | 10.938 ms | 0.81x | |
| bitECS S&P (Merge) | 9.215 ms | 11.554 ms | 0.80x | |
| bitECS S&P (Native) | 9.674 ms | 11.345 ms | 0.76x | |
| ECS Tree | 9.970 ms | 43.666 ms | 0.74x | |
| bitECS S&P (Insertion) | 10.730 ms | 37.820 ms | 0.68x | |
| OOP Tree | 16.321 ms | 40.756 ms | 0.45x |
6. Erratic Behavior (Low Coherence - 15,000 Entities)
| System | Avg Frame Time | 99th Percentile | Speedup | |
|---|---|---|---|---|
| WASM ECS S&P (Quick) | 2.141 ms | 2.588 ms | 61.92x | |
| WASM ECS S&P (Merge) | 2.226 ms | 2.548 ms | 59.56x | |
| ECS (Custom SoA) S&P (Quick) | 5.318 ms | 5.765 ms | 24.93x | |
| ECS (Custom SoA) S&P (Merge) | 5.668 ms | 6.334 ms | 23.39x | |
| ECS (Custom SoA) S&P (Native) | 6.991 ms | 8.146 ms | 18.97x | |
| OOP S&P (Quick) | 8.380 ms | 9.820 ms | 15.82x | |
| OOP S&P (Merge) | 8.826 ms | 12.094 ms | 15.02x | |
| bitECS S&P (Quick) | 9.692 ms | 11.258 ms | 13.68x | |
| bitECS S&P (Merge) | 10.037 ms | 12.722 ms | 13.21x | |
| OOP S&P (Native) | 10.645 ms | 14.415 ms | 12.45x | |
| bitECS S&P (Native) | 10.877 ms | 12.864 ms | 12.19x | |
| ECS Tree | 16.639 ms | 19.107 ms | 7.97x | |
| WASM Tree | 16.976 ms | 19.827 ms | 7.81x | |
| OOP Tree | 29.261 ms | 74.175 ms | 4.53x | |
| WASM ECS S&P (Insertion) | 32.518 ms | 35.499 ms | 4.08x | |
| ECS (Custom SoA) S&P (Insertion) | 36.896 ms | 41.801 ms | 3.59x | |
| bitECS S&P (Insertion) | 101.341 ms | 122.182 ms | 1.31x | |
| OOP S&P (Insertion) | 132.579 ms | 140.137 ms | 1.00x |
Published: Wed Jun 24 2026 00:00:00 GMT+0000 (Coordinated Universal Time)