Skip to content

Benchmarks#

leptoskit is benchmarked against itself on every pull request, so a change that slows it down is caught before it merges. The tables on this page are the other half: a comparison, run on demand, of what leptoskit costs next to the alternatives. They compare leptoskit with plain React in the DOM, React Native rendered by react-native-web, Flutter web, and vanilla JS as the floor, plus the build-speed face-off against cargo-leptos.

The methodology lives in benchmarks/README.md: the scenario set follows the js-framework-benchmark keyed table, every scenario verifies the expected DOM state before its duration counts, and the user-facing metrics — time to first render, click throughput, transfer size, heap — are first-class alongside it.

leptoskit, React Native, Lynx, and Flutter#

If you are choosing a way to ship one interface to phones, desktops, and the web, the four serious options make different bets. They differ in where your code runs, what draws the pixels, and what crosses between the two on every update. This section compares leptoskit with the latest releases of React Native, Lynx, and Flutter in two ways:

  • How each one is built. A table of the architecture: how messages cross from your code to the screen, and what each framework supports.
  • What each one measures. The same app is built idiomatically in every framework, run through the same scenarios on every platform, and timed from screen recordings. That way, a framework's own frame counter never decides the result.

All four apps use the same contract. Each one shows a small coloured square whose colour marks when a scenario starts and finishes. The recording shows which frames actually reached the screen, and when. The method, the scenarios, and every app's source are in benchmarks/arena.

How they are built#

leptoskitReact Native 0.88Lynx (ReactLynx 0.129)Flutter 3.47
Your interface codeRust, compiled to WebAssembly and run by an interpreter (wasmi) on deviceJavaScript, compiled to Hermes bytecodeJavaScript in PrimJS. React runs on a background thread; small "main-thread scripts" run on the UI threadDart, compiled ahead of time to machine code
What draws the pixelsThe platform's own widgets: UIKit, AppKit, Android views, GTK 4, Win32. HTML on the webThe platform's own widgets, through the Fabric renderer. HTML on the web through react-native-webLynx's own native elements on iOS and Android, its own renderer on desktop, and HTML on the webIts own renderer (Impeller) on a GPU surface. A canvas on the web
How an update reaches the screenThe Wasm code sends element changes with JSON properties to a Rust host. The host diffs, lays out with yoga, and sends the platform renderer a JSON list of widget changesReact builds immutable shadow nodes in C++ through JSI. A background commit lays them out with Yoga, and the UI thread mounts the differencesThe background thread sends the UI thread a batch of patch operations against precompiled templatesNothing crosses: Dart builds the frame and hands a layer tree to the raster thread in the same process
Where your code runs, relative to the UI threadOn the UI threadOn its own JavaScript threadOn a background thread, apart from main-thread scriptsOn the UI thread, since 3.29 merged the UI and platform threads
Calling native codeNative plugins through a binary request and response bridgeTurboModules over JSI: direct C++ calls, either synchronous or returning a promiseNative modules, called from the background thread with callbacksPlatform channels, which are asynchronous and encode messages in a binary format. dart:ffi for synchronous C calls
3DNone yetThrough libraries: react-native-wgpu (WebGPU, which runs three.js), react-native-filament, expo-glNone: CSS 3D transforms only, and no canvas or WebGL elementflutter_gpu (experimental, opt-in) with flutter_scene (pre-1.0)
Updates without an app store releaseBuilt in: signed WebAssembly code pushThrough third parties, such as Expo UpdatesBundles load from wherever the host app fetches themNot officially supported. Shorebird is a third-party service
PlatformsWeb, iOS, Android, macOS, Windows, Linux, HarmonyOS (experimental)iOS and Android. macOS and Windows through Microsoft's ports, which trail the core release. The web through react-native-webiOS, Android, HarmonyOS, web. macOS and Windows since 3.7iOS, Android, web, macOS, Windows, Linux

What the arena measures#

ScenarioWhat it doesWhat it tells you
Cold openLaunches the app and waits for a ready screen of 20 rowsHow long people wait before they see your app
Big bangMounts 1,000 cards, updates every tenth, then clears them, each timed to the frame that shows itHow fast large changes to the screen land
EchoSends 2,000 round trips each of nothing, an integer, a 1 KiB string, 64 KiB of bytes, and a 100-field object to native codeWhat a call between your code and native code costs, and what a payload adds
Ticker wallRewrites 300 numbers on every frame for five secondsWhether the path from your code to pixels keeps up with the display
AutoscrollScrolls a 2,000-item list from your code, one step per frameSmoothness when your code drives the motion
Busy handsAnimates the list natively while your code is blocked 40 ms out of every 100Whether slow app code makes scrolling stutter
Size tiersBuilds the same app at 1, 12, and 120 screensA framework's fixed cost, and what each extra screen of your app adds

Frame numbers are presented frames per second and hitch time: milliseconds of lateness per second of motion, as Apple defines it. Under 5 ms/s feels smooth and over 10 ms/s is visible stutter. Simulators, emulators, and headless browsers run on a desktop CPU without the phone's GPU timing. Their tables are marked shape only: they show which framework is ahead, not the numbers a phone would show.

iOS#

Not measured yet.

Android#

leptoskitReact NativeLynxFlutter
Cold open: launch to ready397 ms397 ms439 ms488 ms
Big bang: 1,000 cards to the screen108 ms745 ms443 ms253 ms
Big bang: update every tenth card18.6 ms63.6 ms60.3 ms48.8 ms
Big bang: clear 1,000 cards17.3 ms49.5 ms73.3 ms28.5 ms
Echo, async: integer round trip (p50)59 µs111 µs111 µs71 µs
Echo, async: integer round trip (p99)92 µs301 µs147 µs102 µs
Echo, async: 1 KiB string (p50)78 µs142 µs153 µs86 µs
Echo, async: 64 KiB of bytes (p50)196 µs22.42 ms389 µs197 µs
Echo, async: 100-field object (p50)486 µs1.66 ms338 µs156 µs
Echo, async: integer calls per second16,444/s8,121/s8,855/s13,657/s
Echo, sync: integer round trip (p50)—0.92 µs28 µs0.08 µs
Echo, sync: 64 KiB of bytes (p50)—49 µs224 µs5.54 µs
Ticker wall: presented frames per second72.629.358.125.9
Ticker wall: display mode it ran in60 Hz60 Hz60 Hz60 Hz
Ticker wall: hitch time5.0 ms/s478.0 ms/s129.8 ms/s578.6 ms/s
Ticker wall, tuned: presented frames per second——98.6—
Autoscroll: presented frames per second95.189.7107.4112.2
Autoscroll: display mode it ran in120 Hz120 Hz120 Hz120 Hz
Autoscroll: hitch time206.7 ms/s265.0 ms/s110.8 ms/s66.5 ms/s
Busy hands: presented frames per second120.582.251.478.6
Busy hands: display mode it ran in120 Hz120 Hz120 Hz120 Hz
Busy hands: hitch time0.0 ms/s314.1 ms/s579.0 ms/s341.9 ms/s

Measured 2026-11-01 on Seeker, Android 16 (physical), 5 runs per framework, medians; leptoskit 0.1.0, React Native 0.88.1, Lynx 4.2.0, Flutter 3.47.8.

  1. Big bang, every framework: each app times its own state change to its earliest after-frame hook, and the results file carries the hook it used. The recording cannot time these steps any more: leptoskit's land in one or two display frames, so Android's recorder often encodes neither beacon, and a window it cannot see is not a window it can time. create_to_pixel_ms, update_to_pixel_ms, and clear_to_pixel_ms are still recorded wherever the frames are there.

macOS#

Not measured yet.

Windows#

Not measured yet.

Linux#

Not measured yet.

Web#

Not measured yet.

App size: minimal, medium, and large#

Every framework builds the same three apps from one specification:

  • minimal: one counter screen;
  • medium: 12 screens, made up of a home menu and three each of counter, list, form, and detail screens;
  • large: the same mix at 120 screens.

Every screen has its own labels and data, as in a real app. Navigation is a piece of state, not a routing library, so no framework pays for a dependency the others skip. "Each extra screen" is the difference between large and medium, divided by the 108 screens between them. It separates what a framework costs before you write anything from what your code adds.

platformtierleptoskitReact NativeLynxFlutter
iOSminimal (1 screen)5.01 MB20.27 MB17.00 MB13.64 MB
iOSmedium (12 screens)5.03 MB20.30 MB17.05 MB14.79 MB
iOSlarge (120 screens)5.19 MB20.54 MB17.48 MB15.12 MB
iOSeach extra screen1.5 KB2.3 KB4.1 KB3.1 KB
Androidminimal (1 screen)5.75 MB18.05 MB12.02 MB14.74 MB
Androidmedium (12 screens)5.77 MB18.08 MB12.03 MB15.80 MB
Androidlarge (120 screens)5.92 MB18.29 MB12.11 MB16.05 MB
Androideach extra screen1.4 KB2.0 KB0.8 KB2.4 KB
macOSminimal (1 screen)3.30 MB——36.87 MB
macOSmedium (12 screens)3.42 MB——39.16 MB
macOSlarge (120 screens)4.48 MB——39.81 MB
macOSeach extra screen10.1 KB——6.1 KB
Webminimal (1 screen)393.5 KB307.1 KB726.0 KB7.28 MB
Webmedium (12 screens)515.2 KB347.4 KB757.1 KB7.72 MB
Weblarge (120 screens)1.56 MB563.2 KB1.01 MB7.92 MB
Webeach extra screen10.0 KB2.0 KB2.6 KB1.9 KB

Built 2026-10-28 on darwin aarch64. Android: the arm64-v8a release APK. iOS: the unsigned arm64 device .app. macOS: the .app bundle. Web: bytes transferred on first load, gzip-compressed as served.

Runtime in the browser#

All fixtures implement the same keyed-table app with the same labels. Durations are medians over 7 fresh-page runs in headless Chromium; transfer sizes are gzip-compressed, as served. Flutter web renders to a canvas, so its table scenarios cannot be DOM-verified — it reports startup, transfer, and memory only.

appfirst rendertransfercreate 1kcreate 10kupdate 10thswapremoveselectappend 1kclearclicks/sheap
react24.4 ms63.3 KB16.6 ms258 ms7.4 ms24.0 ms18.2 ms17.5 ms27.3 ms14.9 ms229779/s11.74 MB
react-native-web41.5 ms144.4 KB35.1 ms425 ms17.1 ms28.0 ms16.1 ms17.4 ms42.3 ms10.7 ms239693/s79.23 MB
vanilla-js20.0 ms3.5 KB13.2 ms118 ms6.1 ms16.9 ms17.5 ms16.4 ms27.6 ms16.7 ms816993/s2.73 MB
leptoskit30.0 ms64.2 KB18.4 ms174 ms5.7 ms18.0 ms18.2 ms16.9 ms27.0 ms16.9 ms1184834/s5.29 MB
flutter164 ms646.7 KB——————————

Measured 2026-10-20 on darwin aarch64, 12 cores, headless Chrome/155; 7 runs per app, medians.

Build speed#

The same app sources — byte-identical, enforced at benchmark time — built through each toolchain on the same machine. Warm is a no-change rebuild after a priming build; incremental bumps the client source first (the loop developers live in); cold removes the target directory and output entirely. The leptoskit pipeline runs wasm-bindgen in-process and skips it entirely when the compiled wasm is unchanged, which is where the warm and incremental gaps come from.

toolwarm rebuildincremental rebuildcold build
leptoskit255 ms1072 ms31131 ms
cargo-leptos584 ms1192 ms38238 ms

Measured 2026-10-20 on darwin aarch64, 12 cores; hyperfine wall-clock medians, tools interleaved per round.

The view layer, the wasm runtime, and a JavaScript baseline#

The browser numbers above measure leptos rendering to the DOM. On iOS and Android, leptoskit apps do something else entirely: the interface is a wasm module running in wasmi — an interpreter, because iOS forbids JIT compilation — driving leptoskit's own view layer (leptoskit_native over the leptoskit_yoga layout engine) through a versioned ABI. This table measures those layers directly, with one binary that runs on the host, in the iOS simulator, and on an Android emulator.

Three layers, one workload — a 1,000-row list, the same shape the browser benchmark renders:

  • native and yoga — the view layer on its own: build the element tree, commit it (resolve, create, lay out), relayout after a viewport change, diff a partial update and a removal, resolve Tailwind classes, dispatch events, hit-test, and the layout engine standalone (flat, grid, incremental).
  • wasm — the shipped path: a Rust guest compiled to wasm32-unknown-unknown (benchmarks/native/guest) builds the list, handles presses, and re-renders through the real rendering ABI, interpreted by wasmi, with the host decoding JSON props and committing to the surface.
  • js — the same guest written in JavaScript, call for call, run in QuickJS: a bytecode interpreter, the same execution class as the Hermes and JavaScriptCore engines a React Native app runs on where JIT is unavailable. It drives the same ABI into the same surface, so the only variable is the layer running the app logic. QuickJS is a stand-in for that class, not a Hermes measurement.

Every scenario verifies the surface afterwards (row counts, label text), and the host side is identical for the wasm and JavaScript columns, so differences between them are guest-layer differences. A commit() rebuilds only the nodes the guest changed since its last commit, so a round trip that updates one label costs little more than the guest's own work.

metrichostiosandroid
build a 1,000-row element tree0.226 ms0.788 ms1.2 ms
first commit of 1,000 rows2.8 ms5.1 ms10.4 ms
dispatch 1,000 press events0.008 ms0.012 ms0.016 ms
press events dispatched per second through a 3-level bubble128336756/s81083273/s64279745/s
1,000 hit tests across the viewport0.935 ms18.1 ms12.9 ms
relayout 1,000 rows after a viewport change2.0 ms5.3 ms4.3 ms
remove the middle row of 1,0001.7 ms5.7 ms4.2 ms
resolve Tailwind classes on 1,000 rows0.223 ms1.1 ms0.933 ms
diff and patch every tenth label of 1,000 rows1.6 ms4.1 ms4.1 ms
lay out a 1,000-row column0.346 ms0.625 ms0.578 ms
lay out 1,000 cells in a 20-column grid0.594 ms1.2 ms0.873 ms
relayout 1,000 rows after one row changed height0.249 ms0.301 ms0.397 ms
wasm: 100 press round trips, one label updated each205 ms319 ms746 ms
wasm: press → guest update → commit round trips per second487/s313/s134/s
wasm: compile and instantiate the guest module0.171 ms0.453 ms0.599 ms
wasm: guest rewrites all 1,000 labels and the host diffs them4.9 ms6.4 ms16.4 ms
wasm: guest builds 1,000 rows through the ABI and the host commits them6.8 ms20.9 ms22.4 ms
js: 100 press round trips, one label updated each146 ms227 ms431 ms
js: press → guest update → commit round trips per second683/s441/s232/s
js: create a QuickJS runtime and load the guest script0.124 ms0.153 ms0.314 ms
js: guest rewrites all 1,000 labels and the host diffs them4.1 ms5.0 ms11.9 ms
js: guest builds 1,000 rows through the ABI and the host commits them5.5 ms7.7 ms18.8 ms

Measured per platform — host: 2026-10-21 on macos aarch64, 12 cores, 15 runs; ios: 2026-10-20 on ios aarch64, 12 cores, 15 runs; android: 2026-10-20 on android x86_64, 2 cores, 15 runs; medians.

The iOS and Android columns come from the iOS simulator and an Android emulator. Simulators and emulators execute on the host CPU: they verify the pipeline and expose platform behaviour, but physical-device numbers differ.

App size#

What a minimal app weighs on every platform leptoskit publishes to. Each row is the artifact a fresh leptoskit new scaffold produces in a release build with the default plugin set, measured by walking what actually ships: the browser's initial load (brotli, as served), the macOS and iOS application bundles, and the Android APK. The platform build tools print the same report at the end of every leptoskit native build --release.

artifacthost codewasm guestcode push baselinetotal
web, initial load (brotli)5.7 KB116.8 KB—122.5 KB
macOS App.app2.99 MB116.1 KB—3.11 MB
iOS App.app (simulator)4.83 MB—52.9 KB4.88 MB
Android APK (arm64)5.50 MB—53.1 KB5.64 MB

Measured 2026-10-22 on darwin aarch64; the scaffolded app, release builds, default plugins.

The host code column is the linked binary (the Rust runtime: wasmi, the view layer, yoga, the plugins, and code push's TLS) on Apple platforms and the uncompressed lib*.so inside the APK on Android; the wasm guest is the app's compiled interface, carried raw on macOS and gzipped inside the app.lkb code push baseline on iOS and Android. Numbers are from one machine and one Xcode/NDK pair — treat them as the shape of a leptoskit app, not a promise; the committed JSON carries the exact inputs.

Formatting#

leptoskit format has its own benchmarks, compared against leptosfmt in the formatting guide: a formatted 4,178-line file in 84 ms, where rustfmt alone takes 67 ms, and an unchanged workspace of 335 files checked in 30 ms.

esc