React Native Performance skill for Codex
Measured fixes for jank and slow starts. Codex profiles a release build on a slow Android phone, names which thread drops frames, and fixes the single largest cost before touching anything else. It moves long lists to FlashList, lets React Compiler or targeted memoisation stop wasted renders, shifts gestures onto Reanimated worklets, and trims cold start, keeping only changes that move a median measurement.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- react-native-performance Installs to ~/.agents/skills/react-native-performance/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install react-native-performance The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project react-native-performance Installs to .agents/skills/react-native-performance/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/react-native-performance.zipunzip -o react-native-performance.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) react-native-performance PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $react-native-performance, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Finds and fixes React Native performance problems with measurements - dropped frames on the JS and UI threads, slow FlatList scrolling fixed with FlashList, wasted re-renders found with the React DevTools Profiler and React Compiler, Reanimated worklets for gestures, image decoding, and cold start time with Hermes, lazy loading and Android vitals thresholds. Use when a list janks, a screen feels slow to open, typing lags, animations stutter, app startup is slow, or before a release on low-end Android devices.
Use it when
- A FlatList or ScrollView drops frames while scrolling
- Typing, taps or navigation feel delayed
- Cold start time is high in Android vitals or user reports
- Gesture or scroll-linked animations stutter
Use something else when
- The slowness is a web React app rather than React Native: use react-performance
- You need build, config or SDK changes unrelated to speed: use react-native-expo
How to check the result
Before and after numbers come from the same release build type on the same device, with the median of at least three scroll runs or five cold starts reported.
The mistake it prevents
Profiling a debug build or an iOS simulator, then shipping memoisation changes that make no difference on real low-end Android hardware.
Done well means: every fix is justified by a before/after measurement on a release build on a real low-to-mid-range Android device, the frame budget (16.7 ms at 60 Hz, 8.3 ms at 120 Hz) holds on the screens users scroll most, and cold start stays well under the Android vitals “excessive” bar of 5 s.
Rules before profiling
- Measure release builds only. Debug builds run unoptimised JS with dev checks and can be 5-10x slower; numbers from them are meaningless.
- Measure on the slowest device you support (a cheap Android phone), not a flagship or the iOS simulator.
- Identify which thread drops frames. The React Native perf monitor (Dev Menu, Perf Monitor) shows JS and UI FPS separately:
| Symptom | Thread | Usual cause |
|---|---|---|
| JS FPS drops, UI fine, taps respond late | JS | Re-renders, heavy work in render, big JSON parse |
| UI FPS drops while JS is fine | UI / render | Overdraw, huge images, shadows, too many views |
| Both drop during scroll | Both | List virtualisation off or item components too heavy |
| Animation stutters only while JS is busy | JS-driven animation | Move to Reanimated worklets or native driver |
Workflow
- [ ] 1. Read AGENTS.md; note RN version, Expo SDK, architecture (New Arch is default since RN 0.76)
- [ ] 2. Reproduce on a release build and record a baseline number
- [ ] 3. Profile (tool table below) and name the single biggest cost
- [ ] 4. Fix that one cost
- [ ] 5. Re-measure under the same conditions; keep the fix only if the number moved
- [ ] 6. Repeat until the target is met; report before/after
| Question | Tool |
|---|---|
| Which components re-render and why | React DevTools Profiler inside React Native DevTools (press j in Metro), “record why each component rendered” |
| Where JS time goes | React Native DevTools Performance panel / Hermes sampling profiler |
| Native rendering, memory, main thread | Android Studio Profiler, Xcode Instruments (Time Profiler, Animation Hitches) |
| Repeatable Android score for CI | Flashlight (flashlight measure) on a device |
| Startup timing in production | Custom marks with performance.mark or a startup-trace library, plus Play Console Android vitals |
Lists
Replace FlatList for long or heterogeneous lists with FlashList (v2 requires the New
Architecture and no longer needs estimatedItemSize):
import { FlashList } from '@shopify/flash-list';
<FlashList
data={messages}
keyExtractor={(m) => m.id}
getItemType={(m) => m.kind} // recycle text rows with text rows
renderItem={({ item }) => <MessageRow message={item} />}
/>
renderItemcomponents must be cheap and memoised; FlashList recycles views, so localuseStatein a row leaks between items unless keyed by item id.- Stable
keyExtractorfrom data IDs, never the index. - Keep images in rows sized explicitly and served at display size.
- Staying on FlatList? Set
getItemLayoutfor fixed heights, tunewindowSizeandmaxToRenderPerBatch, and avoidremoveClippedSubviewson iOS when items have complex layouts (it can blank cells).
Re-renders
- Profile first. Look for components rendering on every keystroke or every scroll event.
- Enable React Compiler (default in new Expo templates since SDK 54; otherwise add
babel-plugin-react-compiler). It auto-memoises components and hooks that follow the Rules of React. Runnpx react-compiler-healthcheckor the ESLint plugin to find components it skips. - Without the compiler, fix in this order:
- Move state down to the component that uses it.
- Split contexts: a context value that changes on every keystroke re-renders every
consumer. Use a store with selectors (Zustand, Jotai, Redux
useSelector). React.memoheavy children and pass them stable props (useCallback,useMemo) only where the profiler shows the waste.
- Never
JSON.stringifyor sort large arrays in render; derive once in the store or withuseMemokeyed on the source data.
Animations and gestures
- Run gesture-driven and layout animations on the UI thread with Reanimated
(
useSharedValue,useAnimatedStyle) andreact-native-gesture-handler. - Do not call
setStatefromonScrollor gesture updates. Use shared values, and only hop to JS (runOnJS/scheduleOnRNin Reanimated 4) for discrete events. - Classic
Animated: alwaysuseNativeDriver: truefor transform and opacity. - Avoid animating
width,heightortopon large trees; animatetransform.
Images
- Use
expo-image(or FastImage on bare RN) with disk caching andrecyclingKeyinside recycled lists. - Request thumbnails from the server at roughly display size times pixel ratio. A 4000 px photo decoded into a 64 pt avatar burns memory and UI-thread time.
- Prefer
contentFitover wrapping images in extra views for cropping.
Startup
Targets: cold start under 2 s on a mid-range Android device is good; Android vitals flags cold starts of 5 s or more, warm 2 s or more, hot 1.5 s or more as excessive.
- Confirm Hermes is on (default). With Hermes the bundle ships as precompiled bytecode.
- Defer work out of module scope: no SDK
init()calls, large JSON imports or storage reads at top level of the entry file unless the first screen needs them. - Lazy-load heavy screens and libraries (
React.lazy, dynamicimport(); Metro inline requires are on by default in recent templates). - Render the first screen from cached data, then refresh. Keep the splash screen only until that first meaningful frame.
- Audit bundle size:
npx expo exportthen inspect with Expo Atlas (EXPO_ATLAS=true npx expo start) orreact-native-bundle-visualizer. Watch for moment-style locale bundles, full lodash, and icon fonts imported whole. - Android: add a Baseline Profile for the native side when startup is dominated by native init.
Gotchas
console.login hot paths is expensive in release builds too unless stripped; usebabel-plugin-transform-remove-consolefor production or a logger that no-ops.useEffectchains that set state after mount cause double renders on every screen open; derive values instead.- Large
TextinsideScrollView(thousands of lines) is not virtualised; use a list. - Shadows on Android without a background colour, and
overflow: hiddenwith border radius on many items, cost UI-thread time; flatten where possible. - Memory growth over navigation usually means listeners or intervals not cleaned up in effect cleanups, or screens kept mounted in a stack. Check with the platform profilers.
Verification
- Same device, same data set, same release build type for before and after.
- For lists: scroll the same distance three times and compare dropped frames or Flashlight score; report the median.
- For startup: cold start (force stop, then launch) at least five times; report median.
adb shell am start -W -n com.acme.app/.MainActivityprintsTotalTimeon Android. - If a fix does not move the number beyond noise, revert it; complexity without gain is a regression.
Report template
**Device / build:** Galaxy A15, Android 14, release AAB, Hermes on
| Scenario | Before | After | Change |
|---|---|---|---|
| Inbox scroll, 500 rows (Flashlight score) | 41 | 78 | FlatList -> FlashList, memo rows |
| Cold start TotalTime (median of 5) | 3.9 s | 2.1 s | deferred analytics init, lazy settings screen |
**Not fixed:** chat screen keyboard lag (JS thread busy in markdown parser), next step: move parsing off render What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "React Native Performance"
short_description: "Measured fixes for jank and slow starts"
default_prompt: "$react-native-performance Scrolling the inbox list stutters on Android, fix it" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.