From 35de1a663a30b273431a54627b08681c78ec81bb Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Sun, 2 Aug 2026 09:08:54 -0600 Subject: [PATCH] docs(mobile/v4): add a practical Performance guide MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The architecture pages document the render/publish/mount pipeline and subtree reuse thoroughly, but there's no developer-facing "why is my tap heavy / what do I do" page. Add digging-deeper/performance.md covering: - what a tap costs (event → render → publish → diff+paint) and that reactivity is per-screen (child components are composition, not update boundaries); - that native diff + paint scale with on-screen node count even when little changed — the practical lever; - levers in order: key lists, flatten the tree, split heavy screens into their own route, use child components for reuse (not isolation), keep polling lean; - how to measure with the built-in BenchmarkComponent (event/diff/paint breakdown), with the real-device caveat. --- .../mobile/4/digging-deeper/performance.md | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 resources/views/docs/mobile/4/digging-deeper/performance.md diff --git a/resources/views/docs/mobile/4/digging-deeper/performance.md b/resources/views/docs/mobile/4/digging-deeper/performance.md new file mode 100644 index 00000000..99c3e77e --- /dev/null +++ b/resources/views/docs/mobile/4/digging-deeper/performance.md @@ -0,0 +1,97 @@ +--- +title: Performance +order: 35 +--- + +## Overview + +SuperNative's model is simple: a screen re-renders itself whenever its state changes, and the +[render → publish → mount](../architecture/render-publish-mount) pipeline keeps that cheap through +[subtree reuse](../architecture/subtree-reuse). For the vast majority of screens you never think about it — taps +feel instant and you move on. + +This page is for the exception: a screen that has grown large — a long form, a dense dashboard, a big table — +where taps start to feel heavy. It covers what a single interaction actually costs, what scales with screen size, +and the levers (in the order worth reaching for them). The golden rule up front: **measure before you optimize.** + +## What a tap costs + +When someone taps a control, the update runs a fixed pipeline: + +1. **Event** — the native press crosses the bridge into the waiting PHP thread (transport + serialization). +2. **Render** — your handler mutates state and the framework re-renders. Reactivity is per **screen**: a state + change re-renders the *whole* component that owns it, top to bottom. (Child components are composition, not + independent reactive boundaries — a parent's state change re-renders them too. See the note below.) +3. **Publish** — the new [frame](../architecture/glossary#frame) is encoded. [Subtree reuse](../architecture/subtree-reuse) + means only the parts that actually changed are re-encoded; everything else travels as a tiny reuse marker. +4. **Diff + paint** — the native side diffs the incoming frame against the current tree and re-renders only the + views whose nodes differ. + +Steps 1–3 are largely bounded by *what changed*, which is why "a one-line text change costs about as much as +changing one line of text." Step 4 is the one to keep in mind on big screens: the native diff and the +SwiftUI/Compose layout pass still operate over the on-screen view hierarchy, so **the diff and paint costs grow +with the number of on-screen nodes**, even when only a small part changed. + +## The lever that matters: node count + +Because encode/serialize is already minimized by reuse, the practical lever on a heavy screen is the **number of +elements on screen**. Roughly in the order worth trying: + +**1. Key your dynamic lists.** Give repeated items a `:native:key` so an insert or reorder is understood as one +changed item instead of every item changing — see [Subtree Reuse](../architecture/subtree-reuse). This is the +single highest-leverage change for list-shaped screens. + +**2. Flatten the tree.** Every wrapper element is a node the diff and layout must consider. A row of a few +elements with alternating background stripes is cheaper than the same row wrapped in nested cards-within-cards. +Prefer the shallowest markup that gets the look you want. + +**3. Split a very large screen into its own route.** The screen is the unit of re-render, so a smaller screen is +a cheaper interaction. If a single screen hosts several heavy sections (say a setup form *and* a long scored +checklist), giving the heavy section its own `Route::native(...)` step means a tap there re-renders only that +section's tree — not the whole page. This also tends to make multi-step flows clearer. + +**4. Reach for child components for *reuse*, not for isolation.** Extracting a section into a child component (or +a Blade partial) is great for organisation and reuse, but it does **not** create an independent update boundary — +the owning screen still re-renders as a whole. Don't split into components expecting per-component reactivity; +split into *screens* if you need to shrink the re-render. + +**5. Keep polling lean.** [Polling](reactivity#polling) drives a full re-render on every tick — keep intervals as +long as the UX allows, and prefer [events](../the-basics/events) for updates that arrive when something actually +happens. + +## Measure, don't guess + +The package ships a benchmark harness so you can see the real numbers instead of guessing. Point a route at it and +launch straight into it: + +```php +use Native\Mobile\Edge\BenchmarkComponent; + +Route::native('/bench', BenchmarkComponent::class); +``` + +@verbatim +```bash +php artisan native:run ios --start-url=/bench +``` +@endverbatim + +Run the **Counter Tap** (minimal screen) and **Large Tree Tap** (~200 nodes) scenarios and compare their pipeline +breakdown — **event**, **post** (diff), and **paint** are reported separately. The diff stage grows markedly with +node count, and on real hardware so does paint. That breakdown tells you which lever to pull: a big *event* number +points at bridge/transport, a big *diff/paint* number points at node count. + + + +