ReactiveUI

Performance Benchmarks

Reactive V2 against V1 and BSML, in BeatLeader and in stress tests.

We measured BeatLeader's own UI on Reactive V1 and V2, then ran the same stress tests on V1, V2 and BSML. In BeatLeader, UI time stayed the same while V1's memory leak is gone. In the stress tests, V2 mounts about 4× faster than V1 and lays out faster too, and many writes in one frame cost it far less, though a single write takes a few microseconds longer.

Against BSML, Reactive is much faster when one part of a large UI changes, since only that part is laid out again, and it usually mounts faster because it builds from code. Most rows where BSML comes out ahead are ones where it does less of the work, as the notes under them say.

Each bar shows how many times better one side is, on a log scale. No bar means the difference is within noise. A * marks a row where the two sides don't do the same work or weren't measured the same way, with a note under its group.

BeatLeader

V1 → V2
V1 betterV2 better
Main menu, UI time per frame7.38 → 8.04 mssame
Playing a replay, UI time per frame11.1 → 11.9 mssame
Opening the hub, longest frame108 → 87.9 mssame
Opening the leaderboard, longest frame1385 → 1415 mssame
Memory kept by each replay after the first14.9 → ≤3.0 MB>4.9× less

V1 never frees the Yoga nodes of destroyed Images, Labels and list rows, or anything they reference, so its memory grows for as long as the game runs.

V2 vs V1

V1 → V2
V1 betterV2 better
Layout
No layout engine, parent resizes16.9 → 18.8 ms1.1× slower
1000 flex items, one resizes13.3 → 3.67 ms3.6× faster
500 percent-sized items, root resizes18.4 → 11.5 ms1.6× faster
128 nested containers, innermost resizes19.7 → 2.64 ms7.5× faster
Tree of 1365 growing nodes44.8 → 25.0 ms1.8× faster
300 labels, all change text*42.2 → 35.1 ms1.2× faster

* V1 sets each label's text directly. V2 writes 300 bound states, the way BeatLeader's V2 UI does.

State
One write, one callback2.0 → 7.4 µs3.6× slower
One write, 1000 callbacks74.1 → 38.6 µs1.9× faster
Callbacks writing more states, 6 levels304 → 667 µs2.2× slower
1000 writes per frame, 100 callbacks*730 → 29.3 µs25× faster
1000 subscribes and unsubscribes**5.78 ms → 82.2 µs70× faster
18 stacked diamonds*20.0 ms → 26.7 µs750× faster

* Not the same work: V1 calls back on every write, 100000 times per frame in the burst and 262144 times at the end of the diamonds. V2 runs each callback once per flush, with the latest value.

** V1 has no subscription handle, so this uses its C# event, which copies the whole list of callbacks on every add and remove.

Animation
2000 Images, color33.9 → 31.9 ms1.1× faster
64 flex panels, size and color25.4 → 12.6 ms2.0× faster
Lists
Table of 100000, scrolling8.34 → 8.07 mssame
Flat list of 500 rows, all rebind*275 → 107 ms2.6× faster
Flat list growing by 10 rows per frame*91.7 → 14.4 ms6.4× faster

* V1 ran each of these in a fresh game session, because its leak slows down everything after it, while V2 ran the full suite. So this is V1's best case.

Mounting
1000 Images*320 → 71.8 ms4.5× faster
1000 Labels*865 → 224 ms3.9× faster
500 list rows*1104 → 291 ms3.8× faster
Creating and destroying 50 rows per frame*124 → 31.0 ms4.0× faster

* Also from a fresh V1 session each.

Memory
100000 states18.2 → 24.2 MB1.3× more
5000 animated boxes17.0 → 6.79 MB2.5× less
2000 list rows*82.5 → 73.4 MB1.1× less

* Also from a fresh V1 session.

Single writes, chains of maps and states writing each other are slower because V2 compares the value, queues the state and notifies on flush. That's also what makes bursts cheap, and they all stay under a millisecond.

Both on the same Yoga build

V1 ships with an unoptimized yoga.dll, while V2 was measured on a Release build of it, so the comparison above mixes two things: changes in Reactive and a faster layout engine underneath it. To tell them apart, we swapped V1 onto the same Release build that V2 was measured with and ran the layout and mount benchmarks again. Everything else stayed as before, so any difference left between the two comes from Reactive itself:

V1 → V2
V1 betterV2 better
Layout
1000 flex items, one resizes3.03 → 2.87 mssame
1000 flex items, root resizes3.28 → 5.23 ms1.6× slower
128 nested containers, innermost resizes1.06 → 1.81 mssame
Tree of 1365 growing nodes24.9 → 26.8 mssame
Mounting
1000 Images*312 → 71.8 ms4.3× faster
1000 Labels*764 → 224 ms3.4× faster
500 list rows*1204 → 291 ms4.1× faster

* V1 ran each of these in a fresh game session, V2 in the full suite.

With the engine the same on both sides, V1 catches up on layout and in some cases even runs up to 1.6× faster, so V2's layout lead over the shipped V1 comes from the Release build. Its mounts don't catch up, though, and V2 still mounts 3.4 to 4.3× faster, so that gain is Reactive's own, from the reworked mounting.

Reactive vs BSML

BSML can't always do the same work, see where the work differs.

BSML → V2
BSML betterV2 better
Layout
1000 items, one resizes*12.5 → 3.67 ms3.4× faster
Tree of 1365 nodes, fixed-size leaves34.9 → 5.81 ms6.0× faster
Tree of 1365 growing nodes35.7 → 25.0 ms1.4× faster
300 labels, all change text**20.1 → 35.1 ms1.8× slower

* BSML uses 40 rows of 25, because its layout groups can't wrap items of different sizes. Reactive wraps all 1000 in one container.

** BSML uses 30 rows of 10 and sets each text directly. Reactive wraps all 300 in one container and writes 300 bound states.

State
500 labels showing one value*19.9 → 54.9 ms2.8× slower

* BSML's labels sit in grid cells of a fixed size, so their text isn't measured for layout, while Reactive's are sized by their text.

Animation
2000 Images, color*30.5 → 31.9 mssame
500 items, position only**≈0 → 3.15 ms>1.6× slower

* BSML has no animations, so a hand-written loop works out one value per frame and sets it on every item. Reactive runs a separate animation per item.

** The same loop, and BSML's items ignore layout, so moving them only writes their transforms. Reactive moves absolute flex items, so its layout runs every frame.

Lists
Table of 100000, jumping far*126 → 7.78 ms16× faster
Flat list of 2000 rows, idle15.2 → 21.3 ms1.4× slower
Flat list of 500 rows, all rebind**89.8 → 107 ms1.2× slower

* BSML's custom-list creates and parses a new cell whenever a row comes into view, instead of reusing one. That's BSML's own behavior.

** BSML has no list to give new items to, so each row's bound object is updated in place. Reactive's list gets a new array of items.

Mounting
1000 Images*556 → 71.8 ms7.8× faster
1000 Labels*623 → 224 ms2.8× faster
500 list rows*1444 → 291 ms5.0× faster
Creating and destroying 50 rows per frame*121 → 31.0 ms3.9× faster

* BSML parses its markup on every mount, where Reactive builds from code. BSML also places the first three in a grid of fixed cells rather than wrapping them, so its 1000 Labels aren't measured for layout.

Memory
5000 animated boxes*21.0 → 6.79 MB3.1× less
2000 list rows120 → 73.4 MB1.6× less

* BSML has no animations, so its 5000 Images hold no animation state, while Reactive's hold one each.

How it was measured

Everything was measured on 2026-11-05. UI time is how long the main thread spends on a frame beyond an idle one, taken as the median (p50), while state changes are timed directly. A mount is the median of every mount after the first, counting its first 6 frames, and memory is the managed heap while the benchmark is mounted.

The machine, the versions and the rules are listed below:

  • UI time window: from the first Update to the end of Canvas.willRenderCanvases.
  • Same: the ranges over all rounds overlap, or the gap is under 5% or under the noise floor of an empty benchmark, which is 0.66 ms on V1 and V2 and 1.95 ms on BSML.
  • PC: Beat Saber 1.44.0, Ryzen 7 5800H, RTX 3070 Laptop, in FPFC on a hidden desktop, which makes absolute numbers higher than usual. V1 is Reactive 859fe5d, V2 is Reactive 56b3941 with a Release yoga.dll, BSML is 1.14.1. 3 rounds of V2 and BSML, 2 of V1. BeatLeader ran 2 rounds per version, and its main menu 5 sessions.
  • Same work: V1 and V2 end with matching layouts, apart from animations, tables and state-subscribe. BSML can't match them, so we compared screenshots of every benchmark instead.
  • Not covered: PC VR, other machines and GPU cost.

Where the work differs

Each target is written the way its library is normally used: V2 the way BeatLeader's UI is, V1 with its own API, and BSML with markup and ~value bindings. Where that changes the work, or a target was measured differently, the chart row has a * and a note under its group.

Every difference

V1 and V2

  • Fresh sessions for V1: V1's leak slows down everything after it, so its mounts, flat lists, life-churn and ram-rows each ran in a game session of their own, while V2 ran the full suite. That's V1's best case. V1's mounts on V2's Yoga also come from one session each, and its layout there from 2 rounds.
  • Callbacks: V1 calls back on every write. That's 100000 calls per frame in state-burst and 262144 at the end of state-diamond-deep, where V2 runs each callback once per flush. So these rows show V2 doing less work by design.
  • Subscribing: V1 has no subscription handle, so state-subscribe uses its C# event, which copies the whole list of callbacks on every add and remove.
  • Text: V1 sets label text directly, and V2 writes bound states, so V2's text benchmarks include its state work.

BSML

  • No wrapping: UGUI's layout groups can't wrap items of different sizes, so where one item changes size, BSML uses rows of a fixed length with the same number of items. Elsewhere a grid of fixed cells stands in for wrapping, so BSML's labels in life-label and state-labels aren't measured for layout, while Reactive's are, and BSML does less work there.
  • No animations: BSML has no animation system, so a hand-written loop works out one value per frame and sets it on every item, while Reactive runs one animation per item. In anim-position, BSML's items also ignore layout, so only their transforms move, while Reactive's absolute flex items run layout every frame. In ram-animated, BSML's Images hold no animation state. In all three, BSML does less work.
  • Markup: BSML parses markup on every mount, including all 50 rows per frame in life-churn. That's how BSML builds UI, so it's counted.
  • Tables: CustomCellListTableData.CellForIdx creates a GameObject and parses the cell again whenever a row comes into view, instead of reusing one. That's BSML's own behavior.
  • No list component: in list-flat-shuffle, each row's bound object is updated in place, while Reactive's list gets a new array of items.
  • No state system: state-labels is the only state benchmark with a BSML version, and ram-state is left out.

All numbers

Show all numbers

UI time, p50. Milliseconds per frame over idle. State benchmarks are timed directly.

BenchmarkCaseV1V2BSML
layout-rect
No layout engine: 1000 anchored Images, parent resized every frame
best16.918.817.5
layout-idle
1000 flex items mounted, nothing changes
best1.861.69≈0
layout-panels
40 fixed-size panels of 25 items: one item changes size inside one panel every frame
best4.582.8115.1
layout-leaf
Fixed-size flex: one of 1000 items changes size every frame
average13.33.6712.5
layout-root
Fixed-size flex: 1000 items, the root resized every frame
worst13.43.783.99
layout-tree-fixed
The same tree with fixed-size leaves, so nothing is measured, the root resized every frame
worst20.35.8134.9
layout-percent
Percent-sized and growing flex: 500 items, the root resized every frame
average18.411.513.7
layout-tree
Tree of nested growing flex containers (1365 nodes), the root resized every frame
worst44.825.035.7
layout-text-one
Text-measured flex: one of 300 labels changes its text every frame
average3.231.474.47
layout-rows
Column of 200 list rows (800 nodes), the root resized every frame
average29.520.015.3
layout-text-all
Text-measured flex: all 300 labels change their text every frame
worst42.235.120.1
layout-tree-text
Tree of content-sized containers with text leaves (364 nodes): text and root change every frame
worst57.019.215.4
layout-deep
128 nested flex containers, the innermost item changes size every frame
worst19.72.645.93
layout-absolute
500 absolutely positioned flex items all move every frame
average5.223.46≈0
layout-toggle
Half of 200 list rows hide and show every frame
worst17.714.719.1
state-labels
One write per frame updates the text of 500 bound labels through a map
worst70.654.919.9
state-single
One state, one callback, one write per frame
best2.0 µs7.4 µs–
state-same-value
1000 writes of the same value per frame, one callback
best11.9 µs8.8 µs–
state-fanout
One write per frame reaches 1000 callbacks
average74.1 µs38.6 µs–
state-chain
Callbacks in callbacks: a chain of 100 maps
average10.7 µs30.1 µs–
state-burst
1000 writes per frame with 100 callbacks
worst730 µs29.3 µs–
state-reentrant
Callbacks in callbacks: two states write each other 200 times per frame
worst17.6 µs53.2 µs–
state-cascade
Callbacks in callbacks: each callback writes four more states, six levels deep (5461 writes)
worst304 µs667 µs–
state-diamond
Callbacks in callbacks: 14 stacked diamonds, where one write reaches the end by 16384 paths
worst1.25 ms22.8 µs–
state-diamond-deep
Callbacks in callbacks: 18 stacked diamonds, where one write reaches the end by 262144 paths
worst20.0 ms26.7 µs–
state-subscribe
1000 subscribes and unsubscribes per frame on a state with 1000 callbacks
worst5.78 ms82.2 µs–
anim-color-small
100 Images animating their color
best0.691.19≈0
anim-position
500 absolute items animating their position
average5.283.15≈0
anim-color-large
2000 Images animating their color, so no layout
average33.931.930.5
anim-size
200 flex items animating their size, so layout every frame
worst5.484.305.19
anim-retarget
1000 color animations restarted every frame
worst17.716.315.8
anim-storm
Nested flex dashboard: 64 panels of 4 boxes animating size and color, plus a label changing text every frame
worst25.412.613.8
list-table-idle
Virtualized table with 100000 items, nothing changes
best0.98≈0≈0
list-table-scroll
Virtualized table with 100000 items, scrolled by a row every frame
average8.348.076.63
list-table-jump
Virtualized table with 100000 items, jumping far every frame so every cell rebinds
worst7.747.78126
list-table-refresh
Virtualized table with 10000 items, replaced by a filtered list every frame
worst9.748.44119
list-flat-idle
Flat list of 2000 rows (8000 nodes), nothing changes
average27.321.315.2
list-flat-shuffle
Flat list of 500 rows, all shifted by one every frame so every row rebinds
worst27510789.8
list-flat-grow
Flat list growing by 10 rows every frame
worst91.714.437.3
life-layout-noflex
Mount and destroy 1000 empty Layouts in a parent without a layout controller
best1.27≈0≈0
life-layout
Mount and destroy 1000 empty Layouts
best1.79≈0≈0
life-image
Mount and destroy 1000 Images
average5.081.21≈0
life-label
Mount and destroy 1000 Labels
average1.801.97≈0
life-row
Mount and destroy 500 list rows (background, icon, two labels)
worst4.335.132.16
life-append
Mount and destroy 4000 empty Layouts in one container, to show how appending scales against life-layout
worst4.563.166.28
life-churn
Create and destroy 50 rows every frame
worst12431.0121
ram-state
100000 states with one callback each
best2.28-1.07–
ram-animated
5000 boxes with a color animation each
average11.09.813.55
ram-rows
2000 list rows (8000 nodes)
worst24.219.611.4

UI time, p99.

BenchmarkV1V2BSML
layout-rect29.630.029.7
layout-idle7.186.777.20
layout-panels14.810.229.3
layout-leaf21.48.8636.7
layout-root24.711.414.1
layout-tree-fixed33.813.657.7
layout-percent29.419.826.7
layout-tree63.542.758.6
layout-text-one10.36.6012.6
layout-rows45.433.428.4
layout-text-all60.554.233.5
layout-tree-text85.333.426.9
layout-deep29.97.5311.6
layout-absolute12.410.85.77
layout-toggle46.133.542.7
state-labels10188.434.0
state-single3.9 µs13.8 µs–
state-same-value20.0 µs15.1 µs–
state-fanout180 µs71.0 µs–
state-chain19.0 µs70.3 µs–
state-burst1.12 ms44.3 µs–
state-reentrant42.9 µs102 µs–
state-cascade606 µs1.21 ms–
state-diamond1.60 ms38.0 µs–
state-diamond-deep25.0 ms42.7 µs–
state-subscribe268 ms134 µs–
anim-color-small5.276.406.01
anim-position12.610.65.36
anim-color-large52.052.048.0
anim-size13.112.012.2
anim-retarget28.928.528.1
anim-storm37.924.824.8
list-table-idle5.654.435.19
list-table-scroll16.517.916.8
list-table-jump15.715.9392
list-table-refresh15.217.9411
list-flat-idle17634.427.1
list-flat-shuffle468149143
list-flat-grow21364.874.3
life-layout-noflex3.583.252.13
life-layout2.942.612.49
life-image6.813.354.00
life-label5.195.913.61
life-row8.629.516.97
life-append9.976.4411.1
life-churn385151299
ram-state3.920.66–
ram-animated12.314.86.40
ram-rows45.528.418.3

Mount, ms.

BenchmarkV1V2BSML
layout-rect79.566.2582
layout-idle76570.0551
layout-panels145392.2765
layout-leaf98798.7600
layout-root71874.2705
layout-tree-fixed1414138745
layout-percent47743.3231
layout-tree1158120813
layout-text-one53095.4170
layout-rows820130663
layout-text-all70583.4181
layout-tree-text45762.1210
layout-deep35.314.8432
layout-absolute112636.8241
layout-toggle1790115562
state-labels1303118237
state-single11.82.06–
state-same-value3.283.72–
state-fanout3.5716.7–
state-chain11.41.99–
state-burst3.362.99–
state-reentrant6.674.32–
state-cascade8.005.92–
state-diamond13.85.59–
state-diamond-deep5.5010.8–
state-subscribe7.782.10–
anim-color-small35715.639.1
anim-position68145.4316
anim-color-large28821671094
anim-size27217.695.1
anim-retarget132388.4554
anim-storm56862.3259
list-table-idle27535.8147
list-table-scroll25830.4130
list-table-jump20626.2120
list-table-refresh22933.8110
list-flat-idle464018236153
list-flat-shuffle9524071563
list-flat-grow17.90.174.39
life-layout-noflex28.726.0527
life-layout40.925.8651
life-image32071.8556
life-label865224623
life-row11042911444
life-append2541022238
life-churn7.935.809.01
ram-state18023.5–
ram-animated272664412672
ram-rows339714125865

Memory while mounted, MB.

BenchmarkV1V2BSML
layout-rect0.820.213.70
layout-idle1.930.483.79
layout-panels1.760.204.07
layout-leaf1.950.463.79
layout-root1.900.413.70
layout-tree-fixed2.570.544.19
layout-percent0.50≈00.72
layout-tree2.720.554.23
layout-text-one7.296.148.54
layout-rows8.024.9512.7
layout-text-all7.416.068.54
layout-tree-text3.381.854.38
layout-deep≈0≈0≈0
layout-absolute0.16≈00.52
layout-toggle7.904.9712.8
state-labels8.0110.510.2
state-single≈0≈0–
state-same-value≈0≈0–
state-fanout≈0≈0–
state-chain≈0≈0–
state-burst≈0≈0–
state-reentrant≈0≈0–
state-cascade≈0≈0–
state-diamond≈0≈0–
state-diamond-deep≈0≈0–
state-subscribe≈0≈0–
anim-color-small≈0≈0≈0
anim-position0.10≈00.55
anim-color-large5.891.454.45
anim-size≈0≈0≈0
anim-retarget1.870.103.54
anim-storm0.63≈01.62
list-table-idle1.030.554.46
list-table-scroll1.170.474.44
list-table-jump1.130.454.43
list-table-refresh0.330.121.45
list-flat-idle125117166
list-flat-shuffle27.325.156.0
list-flat-grow≈0≈0≈0
life-layout-noflex0.520.431.45
life-layout0.700.486.18
life-image1.990.914.02
life-label22.019.121.5
life-row20.615.227.2
life-append5.593.4211.1
life-churn≈0≈0≈0
ram-state18.224.2–
ram-animated17.06.7921.0
ram-rows82.573.4120

Memory left after destroying, MB.

BenchmarkManaged left: V1V2BSMLYoga nodes left: V1V2
life-image
Mount and destroy 1000 Images
1.940010010
life-label
Mount and destroy 1000 Labels
21.900.0710010
life-row
Mount and destroy 500 list rows (background, icon, two labels)
20.500.0720010
life-churn
Create and destroy 50 rows every frame
22800214010
list-flat-idle
Flat list of 2000 rows (8000 nodes), nothing changes
1250080020
list-flat-shuffle
Flat list of 500 rows, all shifted by one every frame so every row rebinds
27.90020020
list-flat-grow
Flat list growing by 10 rows every frame
23.30016820
ram-rows
2000 list rows (8000 nodes)
82.30080010

Memory per component.

ComponentV1V2BSML
Image1.1 KB942 B4.1 KB
Label20.2 KB19.4 KB21.9 KB
Empty container750 B557 B6.3 KB
List row (background, icon, two labels)35.1 KB31.1 KB55.1 KB

Allocated per frame. Benchmarks at 10 KB or more on any target. Only accurate to a heap block.

BenchmarkV1V2BSML
state-subscribe24227 KB0.0 KB–
life-churn2257 KB1645 KB10702 KB
list-table-refresh0.5 KB0.0 KB5756 KB
list-table-jump0.8 KB0.1 KB4796 KB
list-flat-grow844 KB606 KB2936 KB
list-flat-shuffle411 KB18 KB16 KB
list-table-scroll3.5 KB2.5 KB286 KB
layout-absolute0.7 KB146 KB0.1 KB
state-labels47 KB72 KB45 KB
layout-tree-text51 KB24 KB20 KB
layout-rows28 KB19 KB0.3 KB
layout-rect17 KB2.1 KB1.9 KB
layout-text-one4.8 KB4.6 KB12 KB
layout-toggle11 KB1.6 KB0.0 KB

BeatLeader, time.

WhereMetricV1, msV2, msResult
Main menu, idleUI CPU, p507.388.04same
Main menu, idleUI CPU, mean7.878.36same
Main menu, idleUI CPU, p9912.414.1same
Main menu, idleWhole frame, p509.099.65same
Playing a replayUI CPU, p5011.111.9same
Playing a replayUI CPU, mean11.912.4same
Playing a replayUI CPU, p9923.820.0same
Playing a replayWhole frame, p5011.712.5same

BeatLeader, opening screens. Longest frame in milliseconds.

ScreenFirst open: V1V2Later opens: V1V2Result, first open
Hub10887.932.623.6same
Settings41.341.638.425.9same
Replay manager51552231.552.1same
Leaderboard13851415107109same

BeatLeader, memory.

WhereMetricV1V2
Main menuManaged heap134 MB136 MB
Main menuUnity allocator1447 MB1448 MB
Main menuProcess private bytes3038 MB3043 MB
Main menuLive Yoga nodes432423
HubManaged kept by the first open0.39 MB0.17 MB
SettingsManaged kept by the first open0.45 MB0.55 MB
Replay managerManaged kept by the first open2.15 MB2.07 MB
LeaderboardManaged kept by the first open58.1 MB70.6 MB
ReplayManaged kept by the first replay37.3 to 37.8 MB36.2 to 42.3 MB
ReplayManaged kept by each later replay14.9 to 15.6 MBat most 3.0 MB
ReplayYoga nodes kept by each replay3370

V1 on V2's Yoga, layout. Milliseconds per frame.

BenchmarkV1V1 on V2's YogaV2V1 on V2's Yoga vs V1V2 vs V1 on V2's Yoga
layout-leaf14.53.032.874.8× fastersame
layout-root14.23.285.234.3× faster1.6× slower
layout-percent18.110.314.91.8× faster1.4× slower
layout-deep21.31.061.8120× fastersame
layout-tree-fixed23.77.188.693.3× faster1.2× slower
layout-tree47.024.926.81.9× fastersame
layout-rows35.819.421.01.8× faster1.1× slower

V1 on V2's Yoga, mounts. Milliseconds.

BenchmarkV1V1 on V2's YogaV2
life-layout
Mount and destroy 1000 empty Layouts
40.943.725.8
life-image
Mount and destroy 1000 Images
32031271.8
life-label
Mount and destroy 1000 Labels
865764224
life-row
Mount and destroy 500 list rows (background, icon, two labels)
11041204291
life-append
Mount and destroy 4000 empty Layouts in one container, to show how appending scales against life-layout
254148102
ram-rows
2000 list rows (8000 nodes)
339747121412

On this page