reis-mobile 0.7.3 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/CHANGELOG.md +39 -0
- package/README.md +100 -26
- package/agents/android-architect.md +3 -3
- package/agents/android-performance-engineer.md +1 -1
- package/agents/flutter-architect.md +3 -3
- package/agents/flutter-performance-engineer.md +1 -1
- package/agents/ios-architect.md +3 -3
- package/agents/ios-performance-engineer.md +1 -1
- package/agents/kmp-architect.md +3 -3
- package/agents/kmp-performance-engineer.md +1 -1
- package/agents/lead-mobile.md +3 -0
- package/agents/mobile-release-engineer.md +3 -3
- package/agents/mobile-staff-engineer.md +1 -1
- package/agents/rn-architect.md +3 -3
- package/agents/rn-performance-engineer.md +1 -1
- package/bin/reis-mobile.mjs +123 -1
- package/commands/consult.md +95 -0
- package/commands/debate.md +15 -11
- package/commands/reis-mobile.md +7 -2
- package/commands/review.md +2 -0
- package/core/config/project.mjs +1 -1
- package/core/diagnostics/doctor.mjs +23 -1
- package/core/providers/catalog.mjs +168 -0
- package/core/providers/consult.mjs +220 -0
- package/core/providers/providers.mjs +156 -0
- package/core/router/intents.mjs +15 -0
- package/package.json +1 -1
- package/skills/mobile-accessibility/SKILL.md +3 -0
- package/skills/mobile-accessibility/references/localization.md +77 -0
- package/skills/mobile-observability/SKILL.md +137 -0
- package/skills/mobile-observability/references/android.md +105 -0
- package/skills/mobile-observability/references/flutter.md +86 -0
- package/skills/mobile-observability/references/ios.md +88 -0
- package/skills/mobile-observability/references/kotlin-multiplatform.md +76 -0
- package/skills/mobile-observability/references/react-native.md +62 -0
- package/skills/mobile-offline-sync/SKILL.md +132 -0
- package/skills/mobile-offline-sync/references/android.md +123 -0
- package/skills/mobile-offline-sync/references/flutter.md +103 -0
- package/skills/mobile-offline-sync/references/ios.md +103 -0
- package/skills/mobile-offline-sync/references/kotlin-multiplatform.md +114 -0
- package/skills/mobile-offline-sync/references/react-native.md +108 -0
- package/skills/mobile-performance/SKILL.md +88 -0
- package/skills/mobile-performance/references/android.md +96 -0
- package/skills/mobile-performance/references/background.md +87 -0
- package/skills/mobile-performance/references/flutter.md +101 -0
- package/skills/mobile-performance/references/ios.md +82 -0
- package/skills/mobile-performance/references/kotlin-multiplatform.md +62 -0
- package/skills/mobile-performance/references/react-native.md +85 -0
- package/skills/mobile-release/SKILL.md +2 -1
- package/skills/mobile-release/references/billing.md +92 -0
- package/skills/mobile-security/SKILL.md +3 -0
- package/skills/mobile-security/references/auth.md +92 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
# Performance on native Android
|
|
2
|
+
|
|
3
|
+
Android gives you more first-party measurement than any other mobile platform: a lab harness (Macrobenchmark), a system trace (Perfetto), and field data from every install (Android vitals). Use them in that order — reproduce in the lab, explain with a trace, confirm in the field.
|
|
4
|
+
|
|
5
|
+
Deep guides already in this plugin: [`mobile-android/references/build-and-performance.md`](../../mobile-android/references/build-and-performance.md), which links the Android Studio profiler and R8 analyzer material from the Android team.
|
|
6
|
+
|
|
7
|
+
## Measure
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
adb shell am start -W -S com.example/.MainActivity # TotalTime / WaitTime per cold start
|
|
11
|
+
adb shell dumpsys gfxinfo com.example framestats # per-frame timings
|
|
12
|
+
adb shell dumpsys meminfo com.example # PSS breakdown
|
|
13
|
+
adb shell dumpsys batterystats --charged com.example # wakeups, wake locks, jobs
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Question | Tool |
|
|
17
|
+
|---|---|
|
|
18
|
+
| Startup, frame time, memory, power in a repeatable benchmark | Macrobenchmark (`androidx.benchmark:benchmark-macro-junit4`) |
|
|
19
|
+
| Cost of one function or algorithm | Microbenchmark (`androidx.benchmark:benchmark-junit4`) |
|
|
20
|
+
| Where the time actually goes inside a slow frame or a slow start | System trace / Perfetto (Android Studio "Capture System Activities", or `perfetto` on device) |
|
|
21
|
+
| Allocation and leak hunting | Android Studio Profiler: Memory, heap dump, and LeakCanary in debug builds |
|
|
22
|
+
| Which composables recompose | Layout Inspector recomposition counts, and composition tracing in a system trace |
|
|
23
|
+
| Field jank, ANRs, wakeups, crash rate | Play Console → Android vitals; `JankStats` for your own aggregation |
|
|
24
|
+
| Download size | Android Studio APK Analyzer; the Play Console size report for the delivered app |
|
|
25
|
+
|
|
26
|
+
## Startup
|
|
27
|
+
|
|
28
|
+
Cold start cost is dominated by four things:
|
|
29
|
+
|
|
30
|
+
1. **`Application.onCreate` and content providers.** Every library that self-initializes through a `ContentProvider` runs before your first activity. Use `androidx.startup` (`App Startup`) to sequence and lazily defer initializers, and audit the merged manifest for providers you did not add yourself.
|
|
31
|
+
2. **Dependency graph construction.** Hilt/Dagger graph building, a Room database opened synchronously, `SharedPreferences` read on the main thread (use `DataStore`, which is async by design).
|
|
32
|
+
3. **Missing Baseline Profiles.** Without them, the hot startup and scroll paths run interpreted until JIT catches up. Generate them with `androidx.benchmark`'s `BaselineProfileRule`, ship them through `androidx.profileinstaller`, and add a Startup Profile so the DEX layout matches the startup path. This is often the single largest startup win and needs no application code change.
|
|
33
|
+
4. **A first frame that is not the usable frame.** Call `reportFullyDrawn()` (or `FullyDrawnReporter` when several components must finish) so the platform, Macrobenchmark and Play vitals measure time to *usable* content, not time to a spinner.
|
|
34
|
+
|
|
35
|
+
```kotlin
|
|
36
|
+
@RunWith(AndroidJUnit4::class)
|
|
37
|
+
class StartupBenchmark {
|
|
38
|
+
@get:Rule val rule = MacrobenchmarkRule()
|
|
39
|
+
|
|
40
|
+
@Test fun coldStart() = rule.measureRepeated(
|
|
41
|
+
packageName = "com.example",
|
|
42
|
+
metrics = listOf(StartupTimingMetric()),
|
|
43
|
+
iterations = 10,
|
|
44
|
+
startupMode = StartupMode.COLD,
|
|
45
|
+
) {
|
|
46
|
+
pressHome()
|
|
47
|
+
startActivityAndWait()
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`StartupTimingMetric` reports `timeToInitialDisplayMs` and, when `reportFullyDrawn()` is called, `timeToFullDisplayMs`.
|
|
53
|
+
|
|
54
|
+
## Rendering and recomposition
|
|
55
|
+
|
|
56
|
+
With `FrameTimingMetric` in Macrobenchmark you get `frameDurationCpuMs` percentiles and `frameOverrunMs` (how far past the deadline a frame went) over a scripted scroll. Anything with a positive P90 `frameOverrunMs` is visible jank.
|
|
57
|
+
|
|
58
|
+
Compose-specific causes, in the order they usually bite:
|
|
59
|
+
|
|
60
|
+
- **Unstable parameters.** A composable whose parameter type the compiler cannot prove stable is never skipped. `List<T>` is unstable; `kotlinx.collections.immutable`'s `ImmutableList`, or a `@Immutable`/`@Stable` annotated holder, is stable. Enable the compiler's stability report to see which composables are skippable.
|
|
61
|
+
- **Reading state too high.** Reading a `MutableState` in a parent recomposes the parent and everything it emits. Pass a lambda (`() -> Int`) instead of a value, or read it in the smallest child — deferred reads are the standard Compose fix.
|
|
62
|
+
- **A derived value recomputed on every frame.** `derivedStateOf` recomputes only when the result changes, which is what you want for `listState.firstVisibleItemIndex > 0`.
|
|
63
|
+
- **Lambdas recreated each composition** captured into `remember`-less callbacks, forcing children to recompose.
|
|
64
|
+
- **Backwards writes** — writing state during composition — which force an immediate extra pass.
|
|
65
|
+
|
|
66
|
+
For Views: a deep hierarchy, `ConstraintLayout` nested inside `ConstraintLayout`, `wrap_content` in a `RecyclerView` row forcing remeasure, `notifyDataSetChanged` where `DiffUtil`/`ListAdapter` would update in place, and overdraw (Developer options → Debug GPU overdraw).
|
|
67
|
+
|
|
68
|
+
## Memory
|
|
69
|
+
|
|
70
|
+
- A heap dump after the journey, repeated three times, diffed. LeakCanary in debug catches the common lifecycle leaks: an `Activity`/`Fragment` captured in a static field, a listener never unregistered, an inner-class `Handler`, a coroutine launched outside a lifecycle scope.
|
|
71
|
+
- Bitmaps dominate mobile heaps. Load at the target size (`BitmapFactory.Options.inSampleSize`, or let Coil/Glide size the request from the view). A full-resolution camera image decoded into a thumbnail is the classic OOM.
|
|
72
|
+
- `onTrimMemory`/`ComponentCallbacks2` is a signal, not a fix, but caches should honour it.
|
|
73
|
+
- `MemoryUsageMetric` in Macrobenchmark gives a comparable number per run for CI.
|
|
74
|
+
|
|
75
|
+
## Battery
|
|
76
|
+
|
|
77
|
+
See [background.md](background.md) for the scheduling rules. On Android specifically, Play Console → Android vitals reports **excessive wakeups** and **stuck partial wake locks** for your installed base; both are actionable without reproducing anything locally. `PowerMetric` in Macrobenchmark measures energy per benchmark on supported devices, and Battery Historian parses a `batterystats` dump into a timeline.
|
|
78
|
+
|
|
79
|
+
## App size
|
|
80
|
+
|
|
81
|
+
- `isMinifyEnabled = true` and `isShrinkResources = true` on release, with R8 full mode, plus per-ABI/density/language splits through the App Bundle — Play generates them automatically from an AAB.
|
|
82
|
+
- APK Analyzer shows the DEX method counts and resource weight; compare two builds directly in it.
|
|
83
|
+
- Check the size delta in CI by building the release AAB and comparing the `.aab` size with the previous baseline. Set the threshold as a percentage, since ordinary feature work moves it slowly.
|
|
84
|
+
|
|
85
|
+
## Regression detection in CI
|
|
86
|
+
|
|
87
|
+
Macrobenchmark emits JSON (`*-benchmarkData.json`) with the metric percentiles. Run it on a fixed emulator image or a device farm profile, store the JSON per build, and compare `timeToInitialDisplayMs` P50/P90 and `frameOverrunMs` P90 against the previous baseline. Microbenchmark output is more sensitive to CPU throttling; pin the device and discard the first iterations.
|
|
88
|
+
|
|
89
|
+
## Official documentation
|
|
90
|
+
|
|
91
|
+
- App startup time: https://developer.android.com/topic/performance/vitals/launch-time
|
|
92
|
+
- Macrobenchmark: https://developer.android.com/topic/performance/benchmarking/macrobenchmark-overview
|
|
93
|
+
- Baseline Profiles: https://developer.android.com/topic/performance/baselineprofiles/overview
|
|
94
|
+
- Compose performance: https://developer.android.com/develop/ui/compose/performance
|
|
95
|
+
- Android vitals: https://developer.android.com/topic/performance/vitals
|
|
96
|
+
- Capture a system trace: https://developer.android.com/topic/performance/tracing
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# Background execution and battery
|
|
2
|
+
|
|
3
|
+
Battery is not something an app spends; it is something the app makes the *device* spend by waking it, holding it awake, using the radio, or keeping a sensor on. Both platforms now assume that background work is a privilege they grant, not a thread you own — code written as if a background task runs whenever it wants will be deferred, throttled or killed, and the resulting bug report reads as "sync does not work", not as "battery".
|
|
4
|
+
|
|
5
|
+
## The rule that covers both platforms
|
|
6
|
+
|
|
7
|
+
**Say what the work needs, let the system choose when.** Declare the constraints (network, charging, idle, deadline), make the work idempotent and resumable, and let the scheduler batch it with other apps' work so the device wakes once instead of five times. Every API below is a variation on that.
|
|
8
|
+
|
|
9
|
+
**Never poll on a timer.** A periodic wake to ask "is there anything new?" is the single most expensive pattern in mobile, and it is almost always replaceable by a push message that tells the app there *is* something new (see [mobile-firebase](../../mobile-firebase/SKILL.md) for FCM and APNs delivery).
|
|
10
|
+
|
|
11
|
+
## Android
|
|
12
|
+
|
|
13
|
+
| Need | API |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Deferrable work that must survive process death and reboot | `WorkManager` — `OneTimeWorkRequest` / `PeriodicWorkRequest` with `Constraints` |
|
|
16
|
+
| Work the user is actively waiting for, right now | `setExpedited(...)` on a `WorkRequest`, or a foreground service with the right type |
|
|
17
|
+
| Work tied to a user-visible ongoing task (playback, navigation, an upload the user started) | A foreground service with a declared `foregroundServiceType` and a notification |
|
|
18
|
+
| An exact time (an alarm clock, a calendar reminder) | `AlarmManager.setExactAndAllowWhileIdle` — requires the exact-alarm permission on recent versions, and Google Play restricts which apps may declare it |
|
|
19
|
+
| A reaction to a server event | FCM data message, high priority only when it is genuinely time-critical |
|
|
20
|
+
|
|
21
|
+
Rules that break naive code:
|
|
22
|
+
|
|
23
|
+
- **Doze and App Standby.** With the screen off and the device still, network access and alarms are deferred to maintenance windows; rarely used apps land in restrictive standby buckets with tighter job and alarm quotas. Periodic `WorkManager` work has a minimum interval of 15 minutes and is never exact.
|
|
24
|
+
- **Background start restrictions.** An app in the background generally cannot start a foreground service; the allowed cases are enumerated per platform version. Code that "starts a service when the push arrives" is the usual casualty — use expedited work instead.
|
|
25
|
+
- **Foreground service types are enforced.** Recent Android versions require a declared `foregroundServiceType`, require a matching permission, and apply timeouts to some types (`dataSync` and `mediaProcessing` among them). Check the behaviour-changes page for the `targetSdk` you compile against before relying on a long-running service.
|
|
26
|
+
- **Wake locks.** A partial wake lock never released is reported by Play Console → Android vitals as a stuck wake lock, against your installed base. `WorkManager` holds the wake lock for you; taking one manually is almost always a bug.
|
|
27
|
+
- **Batch the radio.** Each transmission wakes the radio and keeps it in a high-power state for seconds afterwards. Batch analytics, logs and telemetry into one flush with a deadline rather than sending per event.
|
|
28
|
+
|
|
29
|
+
Diagnose with `adb shell dumpsys batterystats --charged <package>` (Battery Historian renders it), and read the Excessive Wakeups and Stuck Partial Wake Locks metrics in Android vitals — those are field data from real installs, which no local run reproduces.
|
|
30
|
+
|
|
31
|
+
## iOS
|
|
32
|
+
|
|
33
|
+
| Need | API |
|
|
34
|
+
|---|---|
|
|
35
|
+
| Short refresh when the system decides the app is likely to be used | `BGAppRefreshTaskRequest` via `BGTaskScheduler` — seconds of runtime |
|
|
36
|
+
| Longer maintenance (database compaction, ML model update, bulk upload) | `BGProcessingTaskRequest`, typically run while charging and idle |
|
|
37
|
+
| A download or upload that must continue after the app is suspended or terminated | `URLSession` with a **background** configuration, which the system continues out of process |
|
|
38
|
+
| Finish a short piece of work when the user leaves the app | `UIApplication.beginBackgroundTask` — roughly half a minute, not a general mechanism |
|
|
39
|
+
| React to a server event | APNs; a background (`content-available`) push wakes the app opportunistically and is rate-limited, a user-visible notification is not |
|
|
40
|
+
|
|
41
|
+
Rules that break naive code:
|
|
42
|
+
|
|
43
|
+
- Every background identifier must be listed in `Info.plist` under `BGTaskSchedulerPermittedIdentifiers`, registered before the app finishes launching, and matched by the right `UIBackgroundModes` capability. A missing registration is a launch-time exception, not a silent no-op.
|
|
44
|
+
- The system decides *whether and when*. There is no guaranteed schedule, and an app the user rarely opens may get no background time at all. Schedule the next request at the end of each run, and treat every run as "do as much as you can, then checkpoint".
|
|
45
|
+
- Always set the task's `expirationHandler` and call `setTaskCompleted(success:)`. A task that neither finishes nor expires cleanly costs the app future background time.
|
|
46
|
+
- Background `URLSession` delivers completion to the app delegate after relaunch: the download handler must work with no UI and no in-memory state.
|
|
47
|
+
- Xcode Organizer and MetricKit report battery and disk-write metrics per release from real devices; `MXAppExitMetric` shows terminations. These are the iOS equivalent of Android vitals.
|
|
48
|
+
|
|
49
|
+
## Flutter
|
|
50
|
+
|
|
51
|
+
Flutter's own isolate model does not extend the platform's background allowances — the constraints above still apply, and the plugin you choose is a wrapper over `WorkManager` and `BGTaskScheduler`.
|
|
52
|
+
|
|
53
|
+
- Background work runs in a **separate isolate** with its own memory: no access to the UI isolate's state, no plugin registry unless the entry point registers it (`@pragma('vm:entry-point')` on the callback, and a background isolate binary messenger for plugins that need one).
|
|
54
|
+
- Persist everything the background run produces; the UI isolate will not see in-memory results.
|
|
55
|
+
- Verify on both platforms separately. A background plugin that works on Android frequently does nothing on iOS, because Android's 15-minute periodic work has no iOS equivalent.
|
|
56
|
+
- For downloads, prefer a plugin backed by the platform's own background transfer (`URLSession` background configuration / `DownloadManager` or `WorkManager`) over a Dart `HttpClient` call in a background isolate, which stops when the process is suspended.
|
|
57
|
+
|
|
58
|
+
## React Native and Expo
|
|
59
|
+
|
|
60
|
+
- Expo exposes background work through `expo-task-manager` plus a background task/fetch module; the module name and API have changed across SDK versions, so check the docs for the SDK the project pins before writing code against it.
|
|
61
|
+
- Bare React Native uses Headless JS on Android (a JS task run without a UI, started from a native service or a push) and the `BGTaskScheduler`/background-`URLSession` bridges on iOS, usually through a community package.
|
|
62
|
+
- The JavaScript runtime is not guaranteed to exist in the background. Any handler must boot its own dependencies, must not assume the navigation state or a store is hydrated, and must finish quickly.
|
|
63
|
+
- Both platforms' limits apply unchanged: a JS background handler does not get more runtime than the native API it is wrapping.
|
|
64
|
+
|
|
65
|
+
## Kotlin Multiplatform
|
|
66
|
+
|
|
67
|
+
Scheduling is a host responsibility: `WorkManager` in `androidMain`, `BGTaskScheduler` in the iOS host app. Put the *work itself* — the sync algorithm, the queue drain, the conflict resolution — in `commonMain` behind a suspend function, and let each host's scheduler call it. That keeps the retry and idempotency logic in one place and tested once ([mobile-offline-sync](../../mobile-offline-sync/SKILL.md) covers the queue design).
|
|
68
|
+
|
|
69
|
+
## Reviewing background code
|
|
70
|
+
|
|
71
|
+
- [ ] Every periodic job has a justification that a push message cannot satisfy.
|
|
72
|
+
- [ ] Work is idempotent: running it twice, or being killed halfway, leaves correct state.
|
|
73
|
+
- [ ] Constraints are declared (network type, charging, battery not low) instead of checked in code after the wake.
|
|
74
|
+
- [ ] Retries use exponential backoff with a cap and a jitter, not a fixed short interval.
|
|
75
|
+
- [ ] Network calls are batched; analytics and logs are flushed with a deadline, not per event.
|
|
76
|
+
- [ ] Location, Bluetooth scanning and sensors are started for a bounded window and explicitly stopped, at the coarsest accuracy that works.
|
|
77
|
+
- [ ] The app degrades correctly when the system grants no background time at all — the user still gets correct data on next launch.
|
|
78
|
+
|
|
79
|
+
## Official documentation
|
|
80
|
+
|
|
81
|
+
- Android background work: https://developer.android.com/develop/background-work/background-tasks
|
|
82
|
+
- WorkManager: https://developer.android.com/topic/libraries/architecture/workmanager
|
|
83
|
+
- Android foreground service types: https://developer.android.com/develop/background-work/services/fgs/service-types
|
|
84
|
+
- Optimize for Doze and App Standby: https://developer.android.com/training/monitoring-device-state/doze-standby
|
|
85
|
+
- BGTaskScheduler: https://developer.apple.com/documentation/backgroundtasks/bgtaskscheduler
|
|
86
|
+
- Using background tasks to update your app: https://developer.apple.com/documentation/backgroundtasks/using-background-tasks-to-update-your-app
|
|
87
|
+
- Downloading files in the background: https://developer.apple.com/documentation/foundation/url_loading_system/downloading_files_in_the_background
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
# Performance in Flutter
|
|
2
|
+
|
|
3
|
+
Flutter splits every frame across two threads: the **UI thread** runs `build`, layout and paint (Dart), and the **raster thread** turns the resulting layer tree into GPU commands. A dropped frame belongs to one of them, and the fix is different. DevTools tells you which.
|
|
4
|
+
|
|
5
|
+
Deep guides already in this plugin: [`mobile-flutter/references/performance.md`](../../mobile-flutter/references/performance.md), with [`app-size.md`](../../mobile-flutter/references/performance/app-size.md) and [`concurrency.md`](../../mobile-flutter/references/performance/concurrency.md).
|
|
6
|
+
|
|
7
|
+
## Measure
|
|
8
|
+
|
|
9
|
+
**Always in profile mode.** Debug builds run the Dart VM in JIT with assertions and unoptimized widget code; numbers from `flutter run` in debug are meaningless.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
flutter run --profile # on a physical device, never an emulator, for frame data
|
|
13
|
+
flutter build apk --analyze-size # or appbundle / ipa / macos
|
|
14
|
+
flutter build apk --split-debug-info=build/symbols --obfuscate
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
| Question | Tool |
|
|
18
|
+
|---|---|
|
|
19
|
+
| Which thread drops frames | DevTools → Performance → Frames chart (blue = UI, teal = raster) |
|
|
20
|
+
| What runs inside a slow frame | DevTools → Performance → Timeline events, then the CPU profiler for the Dart stack |
|
|
21
|
+
| Which widgets rebuild, and how often | DevTools → Performance → Track widget builds; `debugProfileBuildsEnabled`, `debugPrintRebuildDirtyWidgets` |
|
|
22
|
+
| What repaints | `debugRepaintRainbowEnabled`, and DevTools' "Highlight repaints" |
|
|
23
|
+
| Memory growth and leaks | DevTools → Memory: take a snapshot after the journey, repeat it, diff the retained set |
|
|
24
|
+
| Startup cost | `flutter run --profile --trace-startup`, which writes `start_up_info.json` with `timeToFirstFrameMicros` and `timeToFrameworkInitMicros` |
|
|
25
|
+
| Size | `--analyze-size` output opened in DevTools → App size |
|
|
26
|
+
|
|
27
|
+
Mark your own expensive spans so they appear on the timeline:
|
|
28
|
+
|
|
29
|
+
```dart
|
|
30
|
+
import 'dart:developer' as developer;
|
|
31
|
+
|
|
32
|
+
final task = developer.TimelineTask()..start('parseOrders');
|
|
33
|
+
final orders = parseOrders(payload);
|
|
34
|
+
task.finish();
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Rebuilds: the usual causes
|
|
38
|
+
|
|
39
|
+
- **A `setState`, `notifyListeners` or stream emission high in the tree.** Move the state down to the smallest widget that needs it, or split the widget so only the changing part rebuilds.
|
|
40
|
+
- **`const` missing.** A `const` constructor lets the framework skip the subtree entirely. Enable `prefer_const_constructors` in `analysis_options.yaml`.
|
|
41
|
+
- **Rebuilding a constant child inside an animation.** Pass it through the `child:` parameter of `AnimatedBuilder`, `ValueListenableBuilder` or `StreamBuilder` so it is built once and reused.
|
|
42
|
+
- **A selector that returns a new object every time.** `context.watch<Cart>()` rebuilds on every cart change; `context.select((Cart c) => c.itemCount)` rebuilds only when the count changes. The same applies to `BlocSelector`, Riverpod's `select`, and any `buildWhen`/`listenWhen`.
|
|
43
|
+
- **Equality that never holds.** A state class without `==`/`hashCode` (or a freezed/equatable equivalent) makes every emission look new. A `List` field compares by identity: copy it or use an immutable collection.
|
|
44
|
+
- **Work in `build`.** `build` can be called many times per second. No parsing, no sorting, no `Future` creation, no controller construction inside it.
|
|
45
|
+
|
|
46
|
+
## Lists
|
|
47
|
+
|
|
48
|
+
- `ListView.builder` / `GridView.builder` / `SliverList` with a builder — never a `ListView(children: [...])` over a long collection, which builds every child.
|
|
49
|
+
- Give `itemExtent` (or `prototypeItem`) when rows have a fixed height: it lets the framework skip measuring and makes scrolling to an offset O(1).
|
|
50
|
+
- Stable `ValueKey`s on items so reorders and inserts do not rebuild everything.
|
|
51
|
+
- Wrap an item in `RepaintBoundary` only when it repaints independently of its neighbours (an animating row in a static list). Applied indiscriminately it costs memory and a layer per row.
|
|
52
|
+
- `AutomaticKeepAlive` keeps offscreen items alive: useful for a video or a form, expensive for a plain row.
|
|
53
|
+
|
|
54
|
+
## Painting
|
|
55
|
+
|
|
56
|
+
- `Opacity` over a large subtree allocates an offscreen layer. Prefer `AnimatedOpacity` on a leaf, `Color.withValues(alpha: ...)`, or `FadeTransition`.
|
|
57
|
+
- `ClipRRect` with antialiasing and `Clip.antiAliasWithSaveLayer` is the expensive variant; `Clip.hardEdge` is cheap. A rounded container is cheaper as a `BoxDecoration` with `borderRadius` than as a clip.
|
|
58
|
+
- Shadows and blurs (`BackdropFilter`, `ImageFiltered`) scale with the area covered. Constrain them.
|
|
59
|
+
- Decode images at the size they are displayed: `cacheWidth`/`cacheHeight` on `Image.asset`/`Image.network`, or `ResizeImage`. A 4000 px photo in a 100 px avatar costs ~64 MB of decoded pixels.
|
|
60
|
+
- `SizedBox` instead of `Container` when you only need space; `Container` with no decoration still builds a `LimitedBox`/`ConstrainedBox` chain.
|
|
61
|
+
|
|
62
|
+
## Off the UI thread
|
|
63
|
+
|
|
64
|
+
Anything that takes more than a frame belongs in an isolate. `compute()` covers one-shot work (JSON parsing, image manipulation, crypto); `Isolate.run` is the modern equivalent for a single call; a long-lived worker isolate with `ReceivePort` covers repeated work. Remember that only transferable values cross the boundary, and that plugins that need the platform thread cannot be called from a background isolate unless they use a background isolate binary messenger. See [`concurrency.md`](../../mobile-flutter/references/performance/concurrency.md).
|
|
65
|
+
|
|
66
|
+
## Startup
|
|
67
|
+
|
|
68
|
+
- Nothing blocking in `main()` before `runApp`. `WidgetsFlutterBinding.ensureInitialized()` plus a single fast await is the ceiling; move plugin initialization, remote config fetches and database opens behind the first frame or into a splash state.
|
|
69
|
+
- Deferred loading (`deferred as`) splits rarely used code on web; on mobile it does not remove the code from the binary, so it is not a size fix.
|
|
70
|
+
- Confirm which renderer the project actually uses (`flutter --version` and the release notes for the pinned channel). Impeller and Skia have different shader-compilation behaviour, and shader jank fixes that apply to one do not apply to the other.
|
|
71
|
+
|
|
72
|
+
## Regression detection in CI
|
|
73
|
+
|
|
74
|
+
`integration_test` produces a machine-readable timeline summary:
|
|
75
|
+
|
|
76
|
+
```dart
|
|
77
|
+
// integration_test/scroll_perf_test.dart
|
|
78
|
+
final binding = IntegrationTestWidgetsFlutterBinding.ensureInitialized();
|
|
79
|
+
|
|
80
|
+
testWidgets('feed scroll stays within budget', (tester) async {
|
|
81
|
+
app.main();
|
|
82
|
+
await tester.pumpAndSettle();
|
|
83
|
+
|
|
84
|
+
await binding.watchPerformance(() async {
|
|
85
|
+
await tester.fling(find.byType(ListView), const Offset(0, -500), 3000);
|
|
86
|
+
await tester.pumpAndSettle();
|
|
87
|
+
}, reportKey: 'feed_scroll');
|
|
88
|
+
});
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Run it with `flutter drive` (or `flutter test integration_test` with a driver that writes the report) and assert on the emitted summary: `average_frame_build_time_millis`, `worst_frame_build_time_millis`, `missed_frame_build_budget_count` and the raster equivalents. Store the numbers per build and compare against the previous baseline; fail the job when the worst frame or the missed-budget count crosses the threshold you set in §5 of the skill.
|
|
92
|
+
|
|
93
|
+
For size, run `flutter build --analyze-size` in CI, extract the total from the JSON it writes, and compare with the baseline.
|
|
94
|
+
|
|
95
|
+
## Official documentation
|
|
96
|
+
|
|
97
|
+
- Flutter performance best practices: https://docs.flutter.dev/perf/best-practices
|
|
98
|
+
- Flutter performance profiling: https://docs.flutter.dev/perf/ui-performance
|
|
99
|
+
- DevTools performance view: https://docs.flutter.dev/tools/devtools/performance
|
|
100
|
+
- Measuring app size: https://docs.flutter.dev/perf/app-size
|
|
101
|
+
- Integration test performance profiling: https://docs.flutter.dev/cookbook/testing/integration/profiling
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# Performance on native iOS
|
|
2
|
+
|
|
3
|
+
iOS performance work is mostly main-thread work. The main thread renders, handles touch and runs SwiftUI/UIKit updates; anything that blocks it becomes a hitch, a hang or a watchdog termination. Instruments explains a single run, MetricKit and Xcode Organizer explain the installed base.
|
|
4
|
+
|
|
5
|
+
Related material already in this plugin: [`mobile-ios/references/performance-release.md`](../../mobile-ios/references/performance-release.md).
|
|
6
|
+
|
|
7
|
+
## Measure
|
|
8
|
+
|
|
9
|
+
**Release configuration, physical device.** The Simulator has a different GPU path and no thermal or memory pressure; debug builds skip optimization and enable extra checks.
|
|
10
|
+
|
|
11
|
+
| Question | Tool |
|
|
12
|
+
|---|---|
|
|
13
|
+
| Where CPU time goes | Instruments → Time Profiler, with "Record Waiting Threads" to catch blocking |
|
|
14
|
+
| Why a scroll drops frames | Instruments → Animation Hitches (hitch time per frame, scroll and commit phases) |
|
|
15
|
+
| What SwiftUI is doing and why a view updated | Instruments → SwiftUI template; `Self._printChanges()` inside `body` in debug |
|
|
16
|
+
| Main thread blocked | Instruments → Hangs; the Thread Performance Checker in a debug run |
|
|
17
|
+
| Allocations, growth and leaks | Instruments → Allocations (mark generations around the journey) and Leaks; the Memory Graph Debugger for retain cycles |
|
|
18
|
+
| Launch cost | Instruments → App Launch; `DYLD_PRINT_STATISTICS=1` for pre-main dynamic linking time |
|
|
19
|
+
| Field data from real users | MetricKit (`MXMetricManager`), and Xcode Organizer → Launch Time, Hangs, Memory, Disk Writes, Battery |
|
|
20
|
+
| Download size | App Store Connect → App Store → the build's app size report, per device variant |
|
|
21
|
+
|
|
22
|
+
Apple treats the main thread being blocked for more than about 250 ms as a hang; Organizer reports it as a hang rate per hour of use. A scroll hitch is measured as *hitch time ratio* — milliseconds of hitch per second of scrolling.
|
|
23
|
+
|
|
24
|
+
## Launch
|
|
25
|
+
|
|
26
|
+
Launch splits into pre-main (dynamic linker, runtime setup) and post-main (your `AppDelegate`/`App` initialization up to the first usable frame). Apple's guidance is to finish launch in roughly 400 ms, before the system's launch animation ends.
|
|
27
|
+
|
|
28
|
+
- **Pre-main:** the number of dynamically linked frameworks dominates. Merge small dynamic frameworks into static libraries or use mergeable libraries; remove `+load` methods and heavy static initializers (`__attribute__((constructor))`, non-lazy Swift globals with side effects).
|
|
29
|
+
- **Post-main:** every SDK initialized synchronously in `application(_:didFinishLaunchingWithOptions:)` is launch cost. Defer analytics, remote config, crash-symbol upload and network prefetch to after the first frame. Read persisted state asynchronously; `UserDefaults` is cheap but a Core Data or SwiftData stack opened on the main queue is not.
|
|
30
|
+
- **Measure the right end point.** `os_signpost` a span from `didFinishLaunching` to the moment content is on screen, and query it in a UI test with `XCTOSSignpostMetric`.
|
|
31
|
+
|
|
32
|
+
```swift
|
|
33
|
+
import XCTest
|
|
34
|
+
|
|
35
|
+
final class LaunchTests: XCTestCase {
|
|
36
|
+
func testLaunchPerformance() {
|
|
37
|
+
measure(metrics: [XCTApplicationLaunchMetric()]) {
|
|
38
|
+
XCUIApplication().launch()
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
`measure(metrics:)` records a baseline in the test bundle and fails the test when a later run regresses past the configured deviation — this is the cheapest regression gate on iOS.
|
|
45
|
+
|
|
46
|
+
## SwiftUI updates
|
|
47
|
+
|
|
48
|
+
- **Identity drives everything.** A view whose identity changes is torn down and rebuilt; a `ForEach` over indices instead of stable `id`s rebuilds every row on any change. Use `Identifiable` with an id that survives a refresh.
|
|
49
|
+
- **Narrow what `body` observes.** With `@Observable`, SwiftUI tracks the exact properties read inside `body`; reading a whole model object where one property is needed widens the dependency. With `ObservableObject`, every `@Published` change invalidates every observer — split the object instead.
|
|
50
|
+
- `Self._printChanges()` at the top of `body` prints which dependency caused the update. It is the fastest way to answer "why did this redraw".
|
|
51
|
+
- **Lazy containers for long content:** `LazyVStack`/`LazyHStack` inside a `ScrollView`, or `List`. A plain `VStack` in a `ScrollView` builds every row.
|
|
52
|
+
- **`AnyView` and heavy type erasure** defeat structural identity and the diffing fast path; prefer `@ViewBuilder` and generics.
|
|
53
|
+
- **Expensive work in `body`.** `body` may run many times per frame. Format dates, sort and filter outside it, in the model.
|
|
54
|
+
|
|
55
|
+
For UIKit: cell reuse (`dequeueReusableCell`), self-sizing cells with cached heights, `UICollectionViewDiffableDataSource` over `reloadData`, image decoding off the main thread (`UIImage.prepareForDisplay`/`preparingForDisplay`), and avoiding `layoutIfNeeded` inside `layoutSubviews`.
|
|
56
|
+
|
|
57
|
+
## Memory
|
|
58
|
+
|
|
59
|
+
- Retain cycles are the dominant leak: a closure capturing `self` strongly in a long-lived object, a delegate declared `strong`, a `Timer` or `NotificationCenter` observer never invalidated, a `Task` that outlives its owner. `[weak self]` in escaping closures and the Memory Graph Debugger's cycle detection cover most of it.
|
|
60
|
+
- Images: decode at display size (`UIGraphicsImageRenderer`, `prepareThumbnail(of:)`, or `ImageIO` with `kCGImageSourceThumbnailMaxPixelSize`). Full-resolution decoding into a small view is the standard memory spike.
|
|
61
|
+
- `MXAppExitMetric` from MetricKit tells you how often users' apps were terminated for memory pressure in the field — the only reliable signal that the footprint is too large on the devices you actually ship to.
|
|
62
|
+
|
|
63
|
+
## Concurrency and the main thread
|
|
64
|
+
|
|
65
|
+
- `@MainActor` isolates UI state; work that is not UI should not be on it. An `actor` or a detached `Task` with an explicit priority keeps parsing and I/O off the main thread.
|
|
66
|
+
- A synchronous call into a `DispatchQueue.sync`, a `NSLock`, or an `await` on an actor already busy on the main thread all produce a hang. The Hangs instrument names the blocking frame.
|
|
67
|
+
- Background `URLSession` configurations continue transfers after suspension; ordinary sessions do not. See [background.md](background.md).
|
|
68
|
+
|
|
69
|
+
## Regression detection in CI
|
|
70
|
+
|
|
71
|
+
- `XCTMetric` based tests (`XCTApplicationLaunchMetric`, `XCTClockMetric`, `XCTMemoryMetric`, `XCTOSSignpostMetric`) with committed baselines fail the build on regression. Run them on a pinned simulator or device model; baselines are per-device.
|
|
72
|
+
- Track binary size per build from the archive and compare against the previous baseline.
|
|
73
|
+
- Feed MetricKit payloads into your backend so launch, hang and memory metrics have a per-release series. See [mobile-observability](../../mobile-observability/SKILL.md).
|
|
74
|
+
|
|
75
|
+
## Official documentation
|
|
76
|
+
|
|
77
|
+
- Improving your app's performance: https://developer.apple.com/documentation/xcode/improving-your-app-s-performance
|
|
78
|
+
- Reducing your app's launch time: https://developer.apple.com/documentation/xcode/reducing-your-app-s-launch-time
|
|
79
|
+
- Understanding hangs in your app: https://developer.apple.com/documentation/xcode/understanding-hangs-in-your-app
|
|
80
|
+
- Analyzing responsiveness issues: https://developer.apple.com/documentation/xcode/analyzing-responsiveness-issues-in-your-shipping-app
|
|
81
|
+
- MetricKit: https://developer.apple.com/documentation/metrickit
|
|
82
|
+
- Performance tests with XCTest: https://developer.apple.com/documentation/xctest/performance-tests
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Performance in Kotlin Multiplatform
|
|
2
|
+
|
|
3
|
+
A KMP performance problem is either **shared Kotlin cost**, which shows up on both targets, or a **target-specific consequence** — the Kotlin/Native binary on iOS, the interop boundary, or the host UI. Deciding which comes first is the whole job: optimizing shared code that is not the bottleneck is wasted work, and so is profiling the Android host for a cost that lives in `commonMain`.
|
|
4
|
+
|
|
5
|
+
Related material already in this plugin: [`mobile-kmp/references/build-performance.md`](../../mobile-kmp/references/build-performance.md) and [`source-sets.md`](../../mobile-kmp/references/source-sets.md).
|
|
6
|
+
|
|
7
|
+
## 1. Split shared cost from host cost
|
|
8
|
+
|
|
9
|
+
Run the same journey on both hosts and compare:
|
|
10
|
+
|
|
11
|
+
| Observation | Where the cost lives |
|
|
12
|
+
|---|---|
|
|
13
|
+
| Slow on Android and iOS, similar magnitude | Shared Kotlin: algorithm, serialization, database access in `commonMain` |
|
|
14
|
+
| Slow only on iOS | Kotlin/Native: memory management, the Swift/Kotlin bridge, a suspend function crossing into Swift, framework initialization |
|
|
15
|
+
| Slow only on Android | The Android host: Compose recomposition, main-thread work in the ViewModel, `ContentProvider` initializers |
|
|
16
|
+
| Both slow, different symptoms | Two problems. Fix them separately |
|
|
17
|
+
|
|
18
|
+
Then profile with the host's own tools — there is no KMP profiler. Android Studio Profiler and Macrobenchmark for the Android host ([android.md](android.md)); Instruments for the iOS host ([ios.md](ios.md)), where Kotlin frames appear once the framework's dSYM is available to the trace.
|
|
19
|
+
|
|
20
|
+
Benchmark the shared code directly where you can: `kotlinx-benchmark` runs the same benchmark on JVM and Native targets, which is the only way to compare the two compilers' output for your algorithm rather than guessing.
|
|
21
|
+
|
|
22
|
+
## 2. The interop boundary
|
|
23
|
+
|
|
24
|
+
Every call from Swift into the generated framework crosses a bridge. One call per frame is free; one call per list row per frame is not.
|
|
25
|
+
|
|
26
|
+
- **Batch the crossing.** Return a whole page of prepared view state in one call instead of exposing a getter per field. The shared layer should expose data already shaped for the screen.
|
|
27
|
+
- **Suspend functions and `Flow` become callbacks in Swift.** Each emission crosses the boundary and hops a dispatcher. A `Flow` that emits per keystroke from shared code into SwiftUI is a per-keystroke bridge crossing plus a SwiftUI update — debounce or conflate in shared code, not in Swift.
|
|
28
|
+
- **Collections cross by conversion.** A Kotlin `List` handed to Swift is bridged; a large list converted on every access is a hidden O(n) per call. Hold the reference, do not re-fetch.
|
|
29
|
+
- **Objective-C interop erases generics and sealed hierarchies**, which pushes Swift callers into exhaustive `switch`es over erased types and sometimes into defensive copies. Design a flat, explicitly-typed API surface at the boundary.
|
|
30
|
+
|
|
31
|
+
## 3. Kotlin/Native specifics
|
|
32
|
+
|
|
33
|
+
- The current Kotlin/Native memory manager removes the old freezing rules, so shared mutable state across threads is allowed — but objects still cross into Objective-C autorelease pools, and retain cycles between a Kotlin object and a Swift closure leak exactly like they do in pure Swift. Break them with `[weak self]` on the Swift side and explicit cancellation on the Kotlin side.
|
|
34
|
+
- **Framework initialization is launch cost on iOS.** Anything in a Kotlin top-level property with an initializer, or in a Koin/DI module built at framework load, runs before your first screen. Keep it lazy.
|
|
35
|
+
- **Framework size** counts against the app's download size. Check `isStatic`, dead-code elimination and whether you export more modules than the host actually calls (`export(...)` in the framework block pulls whole modules into the public API and prevents their elimination).
|
|
36
|
+
- Release builds only. Kotlin/Native debug binaries are dramatically slower than release ones, and a "KMP is slow" report measured on a debug framework is a measurement bug.
|
|
37
|
+
|
|
38
|
+
## 4. Shared code that is usually the bottleneck
|
|
39
|
+
|
|
40
|
+
- **Serialization.** `kotlinx.serialization` over a large payload on the main dispatcher blocks both hosts. Parse on `Dispatchers.Default`, and parse once — not per screen.
|
|
41
|
+
- **Database access.** SQLDelight queries on the calling thread; map rows lazily and do not materialize a whole table to display twenty rows.
|
|
42
|
+
- **Flow operators.** An unconfined chain of `map`/`combine` re-running on every upstream emission multiplies work across the boundary. `distinctUntilChanged`, `conflate` and `stateIn` with a sharing policy bound to the host lifecycle keep it bounded.
|
|
43
|
+
- **`expect`/`actual` asymmetry.** A cheap JVM implementation and an expensive Native one for the same declaration produces the "slow only on iOS" case above. Measure both actuals.
|
|
44
|
+
|
|
45
|
+
## 5. Compose Multiplatform UI
|
|
46
|
+
|
|
47
|
+
When the UI is shared, the Compose performance rules in [android.md](android.md) apply unchanged — stability of parameters, deferred state reads, lazy layouts, `derivedStateOf`. What changes is measurement: the Android host gives you Layout Inspector recomposition counts and composition tracing; on iOS you have Instruments' time profile with Kotlin frames and no recomposition counter, so validate recomposition behaviour on Android and confirm the frame result on iOS.
|
|
48
|
+
|
|
49
|
+
## 6. Regression detection in CI
|
|
50
|
+
|
|
51
|
+
- Macrobenchmark on the Android host guards startup and frame time for the shared stack ([android.md](android.md)).
|
|
52
|
+
- `XCTApplicationLaunchMetric` on the iOS host guards framework initialization cost ([ios.md](ios.md)).
|
|
53
|
+
- `kotlinx-benchmark` on JVM and Native guards the shared algorithms themselves, and is the only gate that fails for a shared-code regression before either host notices.
|
|
54
|
+
- Track the produced framework size and the Android AAB size per build; an `export(...)` added to the framework block can move both without any UI change.
|
|
55
|
+
|
|
56
|
+
## Official documentation
|
|
57
|
+
|
|
58
|
+
- Kotlin Multiplatform documentation: https://kotlinlang.org/docs/multiplatform.html
|
|
59
|
+
- Kotlin/Native memory management: https://kotlinlang.org/docs/native-memory-manager.html
|
|
60
|
+
- Swift/Objective-C interop: https://kotlinlang.org/docs/native-objc-interop.html
|
|
61
|
+
- kotlinx-benchmark: https://github.com/Kotlin/kotlinx-benchmark
|
|
62
|
+
- Compose Multiplatform: https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-multiplatform-and-jetpack-compose.html
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Performance in React Native and Expo
|
|
2
|
+
|
|
3
|
+
A React Native frame crosses three execution contexts: the **JavaScript thread** (your React code), the **native UI thread**, and — with the New Architecture — the shadow tree and Fabric's rendering pipeline. A slow app is slow in one of them, and the fix differs. Identify the thread first.
|
|
4
|
+
|
|
5
|
+
Related material already in this plugin: [`mobile-rn/references/performance-release.md`](../../mobile-rn/references/performance-release.md).
|
|
6
|
+
|
|
7
|
+
## Measure
|
|
8
|
+
|
|
9
|
+
**Release build, physical device, Hermes enabled.** A Metro dev build carries the dev bundle, React strict-mode double renders, and the bridge dev listeners; its numbers are not the shipped app's.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npx react-native run-android --mode=release
|
|
13
|
+
npx expo run:ios --configuration Release
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
| Question | Tool |
|
|
17
|
+
|---|---|
|
|
18
|
+
| Which React components render, and why | React DevTools Profiler (React Native DevTools), "Highlight updates" |
|
|
19
|
+
| JavaScript CPU time | Hermes sampling profiler via React Native DevTools → Performance, or `.cpuprofile` capture |
|
|
20
|
+
| Frame drops and native thread cost | The platform tools: Perfetto/system trace on Android, Instruments on iOS (see [android.md](android.md) and [ios.md](ios.md)) |
|
|
21
|
+
| Startup cost breakdown | `performance.now()` marks around module require and first render; on Android, `adb shell am start -W` for the whole process |
|
|
22
|
+
| Bundle size and what is in it | `npx react-native bundle --platform android --dev false --sourcemap-output bundle.map` then a source-map explorer; `npx expo export` for Expo |
|
|
23
|
+
|
|
24
|
+
Instrument your own spans, and keep them in release behind a flag, so the same marks exist in the field:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
const start = performance.now();
|
|
28
|
+
const parsed = parseOrders(payload);
|
|
29
|
+
track('parse_orders_ms', performance.now() - start);
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## The JavaScript thread
|
|
33
|
+
|
|
34
|
+
- **Re-renders are the default problem.** A context value recreated each render, an inline object/array/function prop, or a parent that re-renders on every keystroke will cascade. `React.memo` on the child plus `useCallback`/`useMemo` on the props, or moving the state down so the parent is not involved at all.
|
|
35
|
+
- **Selectors, not whole stores.** Subscribing to an entire Zustand/Redux store re-renders on every change; subscribe to the slice. With React Query/TanStack Query, `select` narrows what a component observes.
|
|
36
|
+
- **Never block JS.** JSON parsing of a large payload, sorting thousands of rows, crypto, image manipulation — these freeze every touch handler. Move them to the native side, a worklet, or chunk them across frames with `InteractionManager.runAfterInteractions` and batched state updates.
|
|
37
|
+
- **Text input and animation state** should not live in React state when they change per frame. Use `Animated` with `useNativeDriver: true`, or Reanimated shared values, so the animation runs on the UI thread and never touches JS.
|
|
38
|
+
|
|
39
|
+
## Lists
|
|
40
|
+
|
|
41
|
+
`FlatList`/`SectionList` are virtualized but still render through React. The props that matter:
|
|
42
|
+
|
|
43
|
+
| Prop | Why |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `keyExtractor` | Stable keys; without them React remounts rows on any change |
|
|
46
|
+
| `getItemLayout` | Skips measurement, enables instant `scrollToIndex` — only valid for fixed-height rows |
|
|
47
|
+
| `initialNumToRender` | Keep it to what a screen actually shows; the default renders more |
|
|
48
|
+
| `maxToRenderPerBatch` / `updateCellsBatchingPeriod` | Trade blank space during fast scrolls against JS work per batch |
|
|
49
|
+
| `windowSize` | How many screens are kept mounted around the viewport |
|
|
50
|
+
| `removeClippedSubviews` | Detaches offscreen native views; helps on Android, test it — it has edge cases with absolute positioning |
|
|
51
|
+
|
|
52
|
+
Memoize the row component (`React.memo`) and make `renderItem` stable. If the list is long and rows are heavy, `@shopify/flash-list` recycles native views instead of mounting new ones and is usually a larger win than tuning the props above.
|
|
53
|
+
|
|
54
|
+
## Images
|
|
55
|
+
|
|
56
|
+
- Size the remote image on the server or CDN; downloading a 3000 px JPEG for a 120 px avatar costs bandwidth, decode time and memory on every row.
|
|
57
|
+
- `expo-image` (or `react-native-fast-image` on bare projects) adds disk/memory caching and progressive loading that the core `Image` does not.
|
|
58
|
+
- Set explicit `width`/`height` so layout does not reflow when the image resolves.
|
|
59
|
+
|
|
60
|
+
## Startup
|
|
61
|
+
|
|
62
|
+
- **Enable Hermes** (the default in current React Native). Hermes precompiles to bytecode, which removes JS parse time at launch.
|
|
63
|
+
- **Enable the New Architecture** where the project's dependencies support it: Fabric and TurboModules remove the asynchronous bridge serialization, and TurboModules load lazily instead of all at startup.
|
|
64
|
+
- **Audit top-level requires.** Every module imported at the entry point is evaluated at launch. Lazy-import screens through the navigator's lazy options, and defer SDK initialization until after the first screen.
|
|
65
|
+
- **Native side still counts.** Time to first JS frame includes the Android `Application.onCreate` and the iOS `didFinishLaunching` path; see the native references.
|
|
66
|
+
|
|
67
|
+
## Bundle and app size
|
|
68
|
+
|
|
69
|
+
- Inspect the release source map with a source-map explorer to find the heavy dependency. Moment/lodash-style full imports, duplicated polyfills and an icon font shipped whole are the usual entries.
|
|
70
|
+
- On Android, enable Proguard/R8 for the release variant and ship an App Bundle. On iOS, check the App Store Connect size report per device variant.
|
|
71
|
+
- Expo: `expo-updates` OTA payloads are downloaded by every user — size matters there too, and an update that grows the bundle grows every launch that has to apply it.
|
|
72
|
+
|
|
73
|
+
## Regression detection in CI
|
|
74
|
+
|
|
75
|
+
- Wrap a scripted journey in Detox or Maestro on a pinned device/emulator, capture the platform's frame data (see [android.md](android.md) and [ios.md](ios.md)), and compare percentiles against a baseline.
|
|
76
|
+
- Assert bundle size in CI: build the release bundle, read its byte size, fail past a percentage delta.
|
|
77
|
+
- `react-native-performance` exposes native startup marks (`nativeLaunchStart`, `runJsBundleStart/End`) as `PerformanceEntry`s, which gives a startup number you can assert on and also report from production.
|
|
78
|
+
|
|
79
|
+
## Official documentation
|
|
80
|
+
|
|
81
|
+
- React Native performance overview: https://reactnative.dev/docs/performance
|
|
82
|
+
- Optimizing FlatList configuration: https://reactnative.dev/docs/optimizing-flatlist-configuration
|
|
83
|
+
- Profiling with Hermes and React Native DevTools: https://reactnative.dev/docs/react-native-devtools
|
|
84
|
+
- The New Architecture: https://reactnative.dev/architecture/landing-page
|
|
85
|
+
- Expo performance guidance: https://docs.expo.dev/guides/analyzing-bundles/
|
|
@@ -19,10 +19,11 @@ One release process for every mobile stack. This file holds the process and what
|
|
|
19
19
|
| Native iOS, or the iOS build of any app | [references/ios.md](references/ios.md) |
|
|
20
20
|
| React Native or Expo app | [references/react-native.md](references/react-native.md) |
|
|
21
21
|
| Kotlin Multiplatform shared module and both host apps | [references/kotlin-multiplatform.md](references/kotlin-multiplatform.md) |
|
|
22
|
+
| In-app purchases and subscription lifecycle | [references/billing.md](references/billing.md) |
|
|
22
23
|
| Release notes and store listing text | [references/store-copy.md](references/store-copy.md) |
|
|
23
24
|
| A CI/CD pipeline that builds, signs and uploads | [references/ci-cd.md](references/ci-cd.md) |
|
|
24
25
|
|
|
25
|
-
Flutter and React Native apps also need the Android and iOS references for signing and store details. Deeper platform guides live in the platform skills: Google Play policy and Play Billing in `mobile-android`, TestFlight and App Store Connect in `mobile-ios`, EAS in `mobile-rn`. Security hardening before release is in `mobile-security
|
|
26
|
+
Flutter and React Native apps also need the Android and iOS references for signing and store details. Deeper platform guides live in the platform skills: Google Play policy and Play Billing in `mobile-android`, TestFlight and App Store Connect in `mobile-ios`, EAS in `mobile-rn`. Security hardening before release is in `mobile-security`, and production crash, error and release-health signals are in [mobile-observability](../mobile-observability/SKILL.md).
|
|
26
27
|
|
|
27
28
|
## 2. Readiness checklist
|
|
28
29
|
|