Skip to content
CrossFly is 100% open sourceFollow us on GitHub
CrossFlyCrossFly
Get started
Benchmark

Five frameworks, identical work, one phone

Fourteen phases, reimplemented scene for scene in Flutter, React Native, .NET MAUI and Kotlin Multiplatform, and run end to end without a tap. Every number on this page came off a device; none of it was chosen.

Measured on Redmi Note 10 Prosweet, Adreno 618, 1080×2400, 120 Hz, Android 123 iterations per phase
CrossFly CoreCrossFly Sync

Read this first

The tables below are not readable without these. They come from the benchmark's own documentation, not from us.

One phone, one spec version

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.

Rows at 120 fps are floors, not results

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.

The frame rate is the app's own

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.

PhaseCrossFlyFlutterReact NativeKotlin Multiplatform.NET MAUI
draw_rect
16 000 filled rects in a full-screen grid
104.99.310.357.734.7
draw_text
16 000 short text runs
55.26.713.99.98.0
draw_image
16 000 image quads from one texture
87.97.612.735.227.8
cards
as many realistic cards as fit at a fixed font scale
118.8at the cap49.075.4103.174.0
textfield
2400 real, interactive TextFields
56.412.80.0617.918.7
image_one_texture
a 48×48 quad grid from one atlas page
118.5at the cap53.972.6119.7at the cap120.0at the cap
shapes_lines
a batched line every 1px across the surface
111.429.3120.1at the cap119.8at the cap119.9at the cap
shapes_circles
50 concentric filled circles
99.140.9107.3112.9105.1
shapes_polygon_concave
3 nested 36-point concave stars
118.6at the cap119.2at the cap89.0117.287.4
shapes_polygon_convex
3 nested 36-side convex polygons
119.6at the cap119.3at the cap120.0at the cap120.0at the cap120.0at the cap
shapes_shadow
8 nested rounded rects with a 40px blurred shadow
119.9at the cap110.2119.4at the cap120.0at the cap120.0at the cap
shapes_text
16 offset passes over a screenful of text
80.346.3117.673.644.1
sqlite_validate
that the encryption below is real
11/11 pass7/7 pass3/5 passnot implemented7/7 pass
sqlite_bench
100k rows × {plain, ChaCha20, SQLCipher}
3 ciphers2 ciphers2 ciphersnot implemented2 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.

The row where a port wins

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.

CrossFly118–135 MB · 2400 text fields 132 MB
floorceiling
Kotlin Multiplatform121–227 MB · 2400 text fields 341 MB
floorceiling
React Native126–591 MB · 2400 text fields 601 MB
floorceiling
Flutter152–286 MB · 2400 text fields 556 MB
floorceiling
.NET MAUI174–203 MB · 2400 text fields 321 MB
floorceiling

The floor is the runtime and the GL context — the price of admission before a single pixel.

The one scene where every port pays and the reference does not

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.

Unshrunk — R8 off
CrossFly
5.95 MB
Flutter
22.65 MB
Kotlin Multiplatform
6.17 MB
React Native
29.69 MB
.NET MAUI
37.34 MB
Shrunk — only the code each app reaches
CrossFly
2.66 MB−55%
Flutter
20.74 MB−8%
Kotlin Multiplatform
0.88 MB−86%
React Native
27.59 MB−7%
.NET MAUI
21.13 MB−43%

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.

MeasurementCrossFlyFlutterReact Native.NET MAUI
plain insert, 100 000 rows216 ms251 ms—1248 ms
rows/s463 000398 000—80 100
plain bulk insert154 ms232 ms—1242 ms
ciphers runplain, chacha20, sqlcipherplain, sqlcipherplain, sqlcipherplain, sqlcipher
validation11/117/73/57/7
Why it's this fast

Fewer layers between you and the GPU

Every hop a frame makes costs time. CrossFly draws directly through LibGDX — no JavaScript bridge to cross.

CrossFly
2 hops
Your Java code
Pure Java, no DSL
LibGDX engine
GPU-accelerated draw
GPU
Pixels on screen
Flutter
2 hops
Your Dart code
New language to learn
Skia engine
Renders widgets
GPU
Pixels on screen
React Native
3 hops + bridge
Your JS code
Runs in a JS engine
The bridge
Serialized — the bottleneck
Native UI
Platform views
GPU
Pixels on screen

Everything that qualifies the numbers above

Kept whole rather than summarised. A benchmark without its caveats is an advertisement.

One phone, one spec version

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.

Rows at 120 fps are floors, not results

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.

The frame rate is the app's own

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.

CPU cannot be read across platforms row by row

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.

Nothing under about 3% is a finding

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.

Where a port does not implement a phase, it says so

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.

Run it yourself

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

Numbers are one thing. Run the thing.

Thirty-eight examples run in your browser, each with its complete source beside it.

Open the examplesRead the docs