Read this first
The tables below are not readable without these. They come from the benchmark's own documentation, not from us.
Everything here was measured on a Redmi Note 10 Pro (sweet, Adreno 618, 1080×2400, 120 Hz, Android 12), release builds, 3 iterations per phase and one app launch per iteration. It is a comparison between five frameworks on one device, not a claim about any other device.
The display runs at 120 Hz, and several scenes sit on that cap on more than one framework. A capped row says "this framework was not the limit here" and nothing more; separating them needs a heavier workload, which is what the benchmark's CFBENCH_LOAD knob is for.
Flashlight counts the app's window being presented, which on four of the five platforms is decoupled from what the scene costs — it read exactly 60.00 for a React Native phase that draws nothing at all, and never exceeded 60.00 in 3388 samples on a 120 Hz display. So the frame rate is what each app measured about itself; what Flashlight contributes is CPU and RAM, which no app can measure about itself.
Frame rate, phase by phase
Each app's own measurement of itself, because on four of the five platforms an outside profiler is measuring the window rather than the scene. Higher is better.
| Phase | CrossFly | Flutter | React Native | Kotlin Multiplatform | .NET MAUI |
|---|---|---|---|---|---|
draw_rect 16 000 filled rects in a full-screen grid | 104.9 | 9.3 | 10.3 | 57.7 | 34.7 |
draw_text 16 000 short text runs | 55.2 | 6.7 | 13.9 | 9.9 | 8.0 |
draw_image 16 000 image quads from one texture | 87.9 | 7.6 | 12.7 | 35.2 | 27.8 |
cards as many realistic cards as fit at a fixed font scale | 118.8at the cap | 49.0 | 75.4 | 103.1 | 74.0 |
textfield 2400 real, interactive TextFields | 56.4 | 12.8 | 0.06 | 17.9 | 18.7 |
image_one_texture a 48×48 quad grid from one atlas page | 118.5at the cap | 53.9 | 72.6 | 119.7at the cap | 120.0at the cap |
shapes_lines a batched line every 1px across the surface | 111.4 | 29.3 | 120.1at the cap | 119.8at the cap | 119.9at the cap |
shapes_circles 50 concentric filled circles | 99.1 | 40.9 | 107.3 | 112.9 | 105.1 |
shapes_polygon_concave 3 nested 36-point concave stars | 118.6at the cap | 119.2at the cap | 89.0 | 117.2 | 87.4 |
shapes_polygon_convex 3 nested 36-side convex polygons | 119.6at the cap | 119.3at the cap | 120.0at the cap | 120.0at the cap | 120.0at the cap |
shapes_shadow 8 nested rounded rects with a 40px blurred shadow | 119.9at the cap | 110.2 | 119.4at the cap | 120.0at the cap | 120.0at the cap |
shapes_text 16 offset passes over a screenful of text | 80.3 | 46.3 | 117.6 | 73.6 | 44.1 |
sqlite_validate that the encryption below is real | 11/11 pass | 7/7 pass | 3/5 pass | not implemented | 7/7 pass |
sqlite_bench 100k rows × {plain, ChaCha20, SQLCipher} | 3 ciphers | 2 ciphers | 2 ciphers | not implemented | 2 ciphers |
Shaded cells are on the display's 120 Hz cap. They mean "this framework was not the limit here" and nothing more — they are floors, not results.
shapes_circles — 50 concentric filled circles, about 35 megapixels of opaque overdraw — runs faster on three of the four ports. It is the same work on both sides: they are all Skia on this GPU's OpenGL ES driver, their frame time is linear in painted area exactly as the reference's is, and what separates them is the price of a pixel. The reference is CPU-bound in its own tessellator there.
Memory is where the frameworks separate
Peak PSS across all fourteen phases. The floor is what a framework charges before drawing anything; the ceiling is what its worst scene costs.
The floor is the runtime and the GL context — the price of admission before a single pixel.
2400 native text fields — EditText, TextInput, Entry, BasicTextField — cost between 321 and 601 MB. CrossFly draws its 2400 itself and pays 132 MB, 14 MB above its own floor. The cost is not drawing text fields; it is having them as platform objects.
What ships
Release, signed, arm64-v8a only — anything else compares packaging conventions rather than frameworks.
The shrunk builds were not the ones measured, except MAUI's, which cannot be built any other way. And the shrunk table cannot be ranked as it stands: Kotlin Multiplatform does not implement the SQLite phases, so it ships no engine, while the reference's is 1.82 MB of its 2.66 MB. Subtract it and the two are within 0.04 MB.
SQLite: the spread is the binding layer
100 000 rows, the same SQL, the same journal mode and synchronous setting on every platform. React Native's timings are left out because its own validation phase fails two of five checks — which is what that phase is for.
| Measurement | CrossFly | Flutter | React Native | .NET MAUI |
|---|---|---|---|---|
| plain insert, 100 000 rows | 216 ms | 251 ms | — | 1248 ms |
| rows/s | 463 000 | 398 000 | — | 80 100 |
| plain bulk insert | 154 ms | 232 ms | — | 1242 ms |
| ciphers run | plain, chacha20, sqlcipher | plain, sqlcipher | plain, sqlcipher | plain, sqlcipher |
| validation | 11/11 | 7/7 | 3/5 | 7/7 |
Fewer layers between you and the GPU
Every hop a frame makes costs time. CrossFly draws directly through LibGDX — no JavaScript bridge to cross.
Everything that qualifies the numbers above
Kept whole rather than summarised. A benchmark without its caveats is an advertisement.
Everything here was measured on a Redmi Note 10 Pro (sweet, Adreno 618, 1080×2400, 120 Hz, Android 12), release builds, 3 iterations per phase and one app launch per iteration. It is a comparison between five frameworks on one device, not a claim about any other device.
The display runs at 120 Hz, and several scenes sit on that cap on more than one framework. A capped row says "this framework was not the limit here" and nothing more; separating them needs a heavier workload, which is what the benchmark's CFBENCH_LOAD knob is for.
Flashlight counts the app's window being presented, which on four of the five platforms is decoupled from what the scene costs — it read exactly 60.00 for a React Native phase that draws nothing at all, and never exceeded 60.00 in 3388 samples on a 120 Hz display. So the frame rate is what each app measured about itself; what Flashlight contributes is CPU and RAM, which no app can measure about itself.
A scene running at 120 fps submits eight times the frames of one running at 14, so its CPU is higher for a reason that has nothing to do with efficiency. Divide by the frame count before comparing two cells.
The draw_*, textfield and shapes_text rows drift by that much between sweeps on an unchanged build, so a difference smaller than that is noise.
Kotlin Multiplatform has no SQLite phases and reports the gap rather than skipping it. React Native's SQLite validation fails two of five checks, which is the phase saying the timings beside it cannot be trusted — so they are left out rather than published.
The benchmark app, the four ports and the measurement scripts are one repository. About twelve minutes per platform.
flashlight/run-all.sh flashlight/results/crossfly dev.crossfly.benchmark python flashlight/summarize.py flashlight/results/crossfly