IFR Benchmarks v0.5
This page collects the measurement campaigns behind Instant First-Frame Rendering and Element Templates. Each campaign answers one question:
A note on configuration names. Campaigns 1 and 2 ran before enableIFR
defaulted to enabling Element Templates. Columns are labeled with today's
product configurations: what the raw reports call ifr is today's IFR
without ET (enableElementTemplates: false opt-out), and ifr+et is
today's default enableIFR: true. Campaign 3b always sets both flags
explicitly. Full methodology and raw data:
strategy benchmark,
all-examples sweep,
real-browser verification,
large-app reevaluation.
1. Strategy ladder
Takeaway: Element Templates are the inflection point of render cost. Plain IFR replay costs as much JS as a full render (~8–11 ms per 1000 elements under an interpreter); ET cuts it about 6–15× across reruns; a Vapor-style design would buy another 2–3× and sit at the PAPI floor. This is why ET is enabled by default with IFR: it attacks the one real cost IFR adds to the main thread — the synchronous first-screen render inside
loadTemplate.
Seven rendering strategies were prototyped and measured against the same logical first screens through the same Element PAPI surface. All variants produce byte-identical rendered documents. Scenes are ~1000–1400 elements: static-heavy (99.7% template-static), content (card feed, 37%), list (v-for, 12%).
Warm render time, --jitless V8 (interpreter ≈ main-thread engine proxy)
Cold first run, --jitless (models a device's one-shot first frame)
Ops payload that would cross the thread boundary
The prototypes are kept for reference: ifr-direct was superseded by ET
(handle-based application is implied by template instantiation), and the
Vapor design remains the tracked endgame — its prototype omits
reactive-effect bookkeeping, so read its numbers as an optimistic bound.
2. All-examples sweep
Takeaway: this campaign measures IFR's costs and its semantic safety — not its benefit. Bundle gzip ×2.26 and a TTI upper bound of ×1.36 are the price; 22/23 examples rendering byte-identical documents across all configurations is the safety result. The per-example FCP columns come out flat (median ×1.04) — which is itself a finding: IFR is a dual-thread architecture optimization, and a single-process harness has no background boot or IPC to remove. Read the flat FCP as proof that the JS work is conserved, then see campaign 3 for the actual win.
Every example was built in three configurations and executed as real bundle
halves in a PAPI-over-jsdom environment (5 runs each, medians, --jitless,
main-thread parse excluded as lepus ships precompiled bytecode).
(FCP/TTI in ms. networking renders 0 nodes at first frame — fetch-driven,
the documented "don't enable IFR" profile. todomvc-codex was
thread-incompatible at measurement time and has since been fixed at the
example level.)
Bundle size (gzip, KiB) — the headline cost
The main-thread bundle section grows from ~17 KiB (worklet registrations
only) to 78–195 KiB (Vue runtime + app copy); ET adds ~1% on top. The sweep
also surfaced and fixed three real bugs (SystemInfo clobbering,
numeric-style baking bypassing auto-px normalization, CSS Modules crashing
the main-thread bundle) — running every real app shape through the
dual-thread pipeline is worth doing for correctness alone.
3. Real threads
Takeaway: with a genuine thread boundary, IFR wins on content-first screens — FCP about −8% to −26% depending on the example set and host, with campaign medians of −19% (ten demos) and −12% (later seven-app mix). The win comes from removing background boot + IPC from the critical path. On small screens the two IFR configurations (with and without ET) are usually within a few percent on web FCP: ET's clearest measured advantages remain render cost and ops payload (campaign 1) and native cold starts.
Setup: Lynx for Web — the background runtime runs in a genuine Web Worker
with real postMessage IPC, in headless Chromium. FCP = <lynx-view>
insertion → first painted content, medians of 7 fresh browser contexts.
Both throttle levels below come from one session measured under the
current flag semantics (enableIFR: true = IFR + ET; explicit opt-out =
IFR without ET). Absolute milliseconds drift with host load between
sessions; within-run ratios are the stable currency.
The ReactLynx control
ReactLynx has no IFR-off switch, so "off" is emulated faithfully (empty main-thread first screen, full background render + hydration). First, in the same single-process harness as campaign 2, ReactLynx is just as flat as Vue Lynx — the reference implementation cannot demonstrate its own headline feature without a thread boundary either:
On real threads both frameworks win, by the same class of margin: the ReactLynx probe (85 nodes) improves 97.7 → 75.2 ms (−23%), squarely inside the range the Vue examples below span.
Full matrix — no CPU throttle
(FCP in ms. Medians: −19% default, −15% without ET — the per-example differences between the two configurations flip between runs and should be read as noise. The Hacker News row here pre-dates shell IFR; see §3b.)
4× CPU throttle
Two readings from this campaign:
- Content-first screens keep a win under throttling on this set (hello-world −16…−18%, todomvc-day1 −10%, swiper −10%), while screens whose FCP is dominated by CSS processing or big bundles compress toward zero — a throttled CPU multiplies the bundle-parse term that the web platform pays on the FCP path.
- The inversion profile reproduces at both throttle levels when the first paint has little sync chrome. The Hacker News row above paid +14% at full speed and +9…11% throttled while the example skipped the IFR mount entirely. After shell IFR (always mount; gate fetches), the same app flipped to about −12% at full speed — see §3b. On native, precompiled lepus bytecode shrinks the parse term inversions are made of.
How far is this from plain single-threaded web?
Vanilla single-threaded baselines (plain Vue via @vue/runtime-dom; plain
Preact via ReactLynx's exact fork pointed back at the real DOM) decompose
the remaining gap. On the ReactLynx probe screen, no throttle:
The thread boundary costs ~23 ms and IFR removes exactly that slice. The remaining gap vs plain web is the web-host emulation layer — a per-host constant that native Lynx, whose platform layer is native code, does not pay.
3b. Large-app reevaluation
Takeaway: keep the campaign-3 story for small content-first screens, and widen the range once product-sized apps join. A second Lynx-for-Web pass (explicit
off/et/ifr/ifr-etflags; hello-world, TodoMVC, gallery, Hacker News, AI Chat, Elk) landed content-first medians around −12% at full speed — inside the same −8%…−26% band as campaign 3's per-example spread, but a lower suite median than −19%. Under 4× throttle, Elk / AI Chat regress about +25% to +44%; the all-seven median flips slightly positive. Bundle gzip stays about ×2.2–2.5 (median ×2.23 here vs ×2.26 in campaign 2).
Focus matrix — no CPU throttle (campaign 3b)
Four explicit configs. FCP ms, medians of 7. ifr = IFR with
enableElementTemplates: false.
† Settled node count after hydration / fetch. Hacker News remounted with
shell IFR in this pass (earlier campaign-3 row skipped app.mount() on the
main thread and inverted). AI Chat / Elk leave IFR off in-tree; numbers are
forced-on for the matrix.
Same set — 4× CPU throttle
Reading across §3 and §3b: treat −12% to −19% as the plausible content-first median band on Lynx for Web, not a single headline −19%. ET-only remains roughly free on size (~+1%) and flat on web FCP; keep defaulting ET with IFR for the strategy-ladder render-cost win (about 6–15× across reruns), not because it moves small-screen web FCP.
Raw JSON and write-up: packages/ifr-bench/reeval/.
4. Native engine observations
The full example suite was validated on a native simulator (LynxExplorer, Lynx SDK 1.4 / PrimJS): 24/25 examples pass with zero hydration mismatches, and No-IFR vs IFR+ET screens are visually identical (SSIM ≥ 0.9977; scoped CSS, baked inline styles, and auto-px semantics all correct on the native engine). Single-sample cold-start recordings show first content ~0.3 s earlier (gallery) up to ~1.2 s earlier (7guis) with IFR — directional confirmation of the mechanism, not a stable benchmark.