Benchmark Results
Last updated: 2026-03-01 | Deno 2.7.1
⚠️ Note: These benchmarks run in GitHub Actions CI (shared runners). Results show relative performance
but may have high variability (CV% >20%). For research-grade measurements, run on dedicated hardware.
Performance Comparison
Comparing O(n) linear scan vs O(log n) R-tree across:
- Data sizes: Sparse (n < 100) vs Large (n > 1000)
- Overlap patterns: Sequential, Grid, Overlapping, Large ranges
- Workloads: Write-heavy (pure inserts), Read-heavy (many queries), Mixed (80/20)
Key Question: When does O(log n) beat O(n)?
How to read: Lower time is better. "Relative" compares to RStarTree baseline:
- 1.0x = same speed as RStarTree
- >1.0x = slower than RStarTree (e.g., 2.0x = twice as slow)
- <1.0x = faster than RStarTree (e.g., 0.5x = twice as fast)
Quick Comparison
Speed relative to RStarTree (lower is better):
| Scenario |
lazypartitionedindex |
mortonlinearscan |
rstartree |
RStarTree |
| Sparse (n < 100) |
0.2x |
0.0x |
0.1x |
1.0x (baseline) |
| Large overlapping (n ≈ 1000) |
156.1x |
6.4x |
16.0x |
1.0x (baseline) |
| Large sequential (n ≈ 2500) |
219.0x |
21.1x |
10.2x |
1.0x (baseline) |
Key insights:
- Sparse data (n < 100): All implementations competitive, linear scan overhead minimal
- Medium data (n ≈ 1000): R-tree starts to win, but linear scan still reasonable
- Large data (n ≈ 2500): R-tree dramatically faster (20-28x), hierarchical indexing pays off
Write-Heavy Workloads (Pure Inserts)
column-operations (n=20)
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.00ms |
0.00x |
| rstartree |
0.02ms |
0.02x |
| lazypartitionedindex |
0.05ms |
0.05x |
diagonal-selection (n=30)
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.02ms |
0.02x |
| lazypartitionedindex |
0.15ms |
0.15x |
| rstartree |
0.16ms |
0.16x |
large-grid (n=2500)
| Implementation |
Time |
Relative |
| rstartree |
10.70ms |
10.70x |
| mortonlinearscan |
18.50ms |
18.50x |
| lazypartitionedindex |
252.20ms |
252.20x |
large-overlapping (n=1250)
| Implementation |
Time |
Relative |
| mortonlinearscan |
6.40ms |
6.40x |
| rstartree |
16.00ms |
16.00x |
| lazypartitionedindex |
156.10ms |
156.10x |
large-ranges (n=500)
| Implementation |
Time |
Relative |
| mortonlinearscan |
1.60ms |
1.60x |
| rstartree |
5.20ms |
5.20x |
| lazypartitionedindex |
25.90ms |
25.90x |
large-sequential (n=2500)
| Implementation |
Time |
Relative |
| rstartree |
10.20ms |
10.20x |
| mortonlinearscan |
21.10ms |
21.10x |
| lazypartitionedindex |
219.00ms |
219.00x |
merge-like-blocks (n=15)
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.00ms |
0.00x |
| rstartree |
0.01ms |
0.01x |
| lazypartitionedindex |
0.05ms |
0.05x |
row-operations (n=20)
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.00ms |
0.00x |
| rstartree |
0.02ms |
0.02x |
| lazypartitionedindex |
0.05ms |
0.05x |
single-cell-edits (n=50)
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.01ms |
0.01x |
| rstartree |
0.07ms |
0.07x |
| lazypartitionedindex |
0.19ms |
0.19x |
sparse-grid (n=60)
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.02ms |
0.02x |
| rstartree |
0.10ms |
0.10x |
| lazypartitionedindex |
0.23ms |
0.23x |
sparse-large-ranges (n=30)
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.01ms |
0.01x |
| rstartree |
0.04ms |
0.04x |
| lazypartitionedindex |
0.11ms |
0.11x |
sparse-overlapping (n=40)
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.02ms |
0.02x |
| lazypartitionedindex |
0.30ms |
0.30x |
| rstartree |
0.33ms |
0.33x |
sparse-sequential (n=50)
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.02ms |
0.02x |
| rstartree |
0.09ms |
0.09x |
| lazypartitionedindex |
0.16ms |
0.16x |
striping-alternating-rows (n=25)
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.01ms |
0.01x |
| rstartree |
0.03ms |
0.03x |
| lazypartitionedindex |
0.10ms |
0.10x |
Read-Heavy Workloads (Frequent Queries)
column-operations (n=20) + 100 queries
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.01ms |
0.01x |
| rstartree |
0.03ms |
0.03x |
| lazypartitionedindex |
0.06ms |
0.06x |
diagonal-selection (n=30) + 100 queries
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.02ms |
0.02x |
| lazypartitionedindex |
0.16ms |
0.16x |
| rstartree |
0.17ms |
0.17x |
large-grid (n=2500) + 100 queries
| Implementation |
Time |
Relative |
| rstartree |
10.60ms |
10.60x |
| mortonlinearscan |
18.30ms |
18.30x |
| lazypartitionedindex |
247.50ms |
247.50x |
large-overlapping (n=1250) + 100 queries
| Implementation |
Time |
Relative |
| mortonlinearscan |
6.40ms |
6.40x |
| rstartree |
16.00ms |
16.00x |
| lazypartitionedindex |
155.00ms |
155.00x |
large-ranges (n=500) + 100 queries
| Implementation |
Time |
Relative |
| mortonlinearscan |
1.60ms |
1.60x |
| rstartree |
5.20ms |
5.20x |
| lazypartitionedindex |
25.70ms |
25.70x |
large-sequential (n=2500) + 100 queries
| Implementation |
Time |
Relative |
| rstartree |
10.10ms |
10.10x |
| mortonlinearscan |
21.20ms |
21.20x |
| lazypartitionedindex |
244.30ms |
244.30x |
merge-like-blocks (n=15) + 100 queries
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.01ms |
0.01x |
| rstartree |
0.02ms |
0.02x |
| lazypartitionedindex |
0.06ms |
0.06x |
row-operations (n=20) + 100 queries
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.01ms |
0.01x |
| rstartree |
0.03ms |
0.03x |
| lazypartitionedindex |
0.06ms |
0.06x |
single-cell-edits (n=50) + 100 queries
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.02ms |
0.02x |
| rstartree |
0.08ms |
0.08x |
| lazypartitionedindex |
0.19ms |
0.19x |
sparse-grid (n=60) + 100 queries
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.03ms |
0.03x |
| rstartree |
0.11ms |
0.11x |
| lazypartitionedindex |
0.24ms |
0.24x |
sparse-large-ranges (n=30) + 100 queries
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.01ms |
0.01x |
| rstartree |
0.05ms |
0.05x |
| lazypartitionedindex |
0.12ms |
0.12x |
sparse-overlapping (n=40) + 100 queries
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.03ms |
0.03x |
| lazypartitionedindex |
0.32ms |
0.32x |
| rstartree |
0.33ms |
0.33x |
sparse-sequential (n=50) + 100 queries
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.02ms |
0.02x |
| rstartree |
0.10ms |
0.10x |
| lazypartitionedindex |
0.18ms |
0.18x |
striping-alternating-rows (n=25) + 100 queries
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.01ms |
0.01x |
| rstartree |
0.04ms |
0.04x |
| lazypartitionedindex |
0.11ms |
0.11x |
Mixed Workloads (80% Write / 20% Read)
large-overlapping (n=500) 80/20
| Implementation |
Time |
Relative |
| mortonlinearscan |
1.20ms |
1.20x |
| rstartree |
5.50ms |
5.50x |
| lazypartitionedindex |
21.00ms |
21.00x |
large-sequential (n=1000) 80/20
| Implementation |
Time |
Relative |
| rstartree |
3.70ms |
3.70x |
| mortonlinearscan |
3.80ms |
3.80x |
| lazypartitionedindex |
30.70ms |
30.70x |
sparse-overlapping (n=40) 80/20
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.03ms |
0.03x |
| lazypartitionedindex |
0.32ms |
0.32x |
| rstartree |
0.33ms |
0.33x |
sparse-sequential (n=50) 80/20
| Implementation |
Time |
Relative |
| mortonlinearscan |
0.02ms |
0.02x |
| rstartree |
0.10ms |
0.10x |
| lazypartitionedindex |
0.18ms |
0.18x |
Query-Only Benchmarks (Construction Not Measured)
large (n=5000, 10k queries)
| Implementation |
Time |
Relative |
| rstartree |
70.30ms |
70.30x |
| mortonlinearscan |
324.10ms |
324.10x |
overlapping (n=1000, 10k queries)
| Implementation |
Time |
Relative |
| mortonlinearscan |
5.60ms |
5.60x |
| rstartree |
24.50ms |
24.50x |
| lazypartitionedindex |
65.90ms |
65.90x |
sequential (n=1000, 10k queries)
| Implementation |
Time |
Relative |
| mortonlinearscan |
4.00ms |
4.00x |
| rstartree |
4.40ms |
4.40x |
| lazypartitionedindex |
32.20ms |
32.20x |
Summary
What do these numbers actually mean?
These benchmarks compare O(n) linear scan vs O(log n) R-tree for different data sizes and workload patterns.
Sparse data (n < 100): Typical for individual spreadsheet properties (backgrounds, borders, etc.)
Large data (n > 1000): Consolidated or heavy usage scenarios
RStarTree is the baseline (1.0x) - numbers > 1.0x are slower, < 1.0x are faster.
lazypartitionedindex (2.1KB minified):
- Fastest in 0/35 scenarios, slowest in 29/35 scenarios
- Average 1.00x vs RStarTree (same)
mortonlinearscan (2.4KB minified):
- Fastest in 29/35 scenarios, slowest in 1/35 scenarios
- Average 1.00x vs RStarTree (same)
rstartree (6.1KB minified):
- Fastest in 6/35 scenarios, slowest in 5/35 scenarios
- Average 1.00x vs RStarTree (same)
System: linux x86_64
Run: deno task bench:update to regenerate