Lesson 4 of 15
Performance & smoothness
Users judge a production app on how it feels — and "feel" is performance: does it launch fast, scroll at 60fps, respond instantly to touch, and not stutter or freeze? A functionally-correct app that's janky feels broken. This lesson covers what makes a cross-platform (React Native) app smooth, the common performance pitfalls, and how to keep it fast on real (including low-end) devices. Let's make it feel fast. Let's chase 60fps.
What "smooth" means and how RN renders
THE TARGET: 60fps (a frame every ~16ms). Miss frames -> visible JANK (stutter,
lag, dropped scroll). Also: fast STARTUP + instant response to touch.
REACT NATIVE HAS TWO THREADS THAT MATTER:
- JS THREAD — runs your React code, state updates, logic, most libraries.
- UI THREAD — draws the native UI + handles native animations.
JANK HAPPENS when one thread is BLOCKED:
- Heavy work on the JS THREAD (big loops, huge re-renders, JSON parsing, non-stop
state updates) -> the app can't respond / animations stutter.
- Animations on the JS thread -> stutter under load (use Reanimated / native
driver -> UI thread).
KEY LEVERS:
- Keep the JS thread FREE: avoid heavy work during interactions; do expensive work
lazily / off the main path; debounce rapid updates.
- LISTS: use FlatList (virtualization) — never map thousands of rows.
- Minimise RE-RENDERS: correct keys, memoization, minimal state.
- Animations/gestures on the UI thread (Reanimated + gesture-handler).
"Smooth" has a concrete target: 60fps — a frame every ~16ms — plus fast startup and instant response to touch. Miss frames and users see jank: stutter, lag, dropped scrolling. To fix it you need RN's model: two threads matter — the JS thread (your React code, state, logic, most libraries) and the UI thread (drawing native UI, native animations). Jank happens when a thread is blocked: heavy work on the JS thread (big loops, huge re-renders, JSON parsing, non-stop state updates) makes the app unresponsive and stutters animations; animations on the JS thread stutter under load. So the key levers are: keep the JS thread free (avoid heavy work during interactions, do expensive work lazily or off the main path, debounce rapid updates); use FlatList for lists (virtualization — never map thousands of rows); minimise re-renders (correct keys, memoization, minimal state); and run animations/gestures on the UI thread (Reanimated + gesture-handler). Performance in RN is largely the discipline of not blocking the JS thread and not doing more rendering work than necessary.
Practical performance techniques
A PRACTICAL CHECKLIST:
RENDERING:
- FlatList for long lists (+ keyExtractor, getItemLayout if fixed height,
windowSize tuning); avoid heavy renderItem work.
- Memoize: React.memo for pure components, useMemo/useCallback for expensive
values/callbacks passed to children — to cut needless re-renders.
- Keep component state minimal + local; lift/split state so a change doesn't
re-render the whole screen.
IMAGES + ASSETS:
- Optimise + correctly size images; cache remote images (expo-image); don't load
hundreds at once (paginate/lazy-load). Images are a top memory/perf cost.
STARTUP:
- Reduce bundle/startup work; lazy-load heavy screens; keep the splash until ready.
- (New RN architecture / Hermes engine improve startup + memory — use them.)
WORK OFF THE MAIN PATH:
- Debounce/throttle rapid events (search input, scroll); batch updates.
- Move heavy computation out of render; consider background/native for very heavy
work.
ANIMATIONS: Reanimated + native driver (UI thread) for 60fps under load.
MEASURE, don't guess (next lesson): profile to find the REAL bottleneck before
optimising.
In practice, performance work follows a checklist. Rendering: use FlatList for long lists (with keyExtractor, getItemLayout for fixed heights, and windowSize tuning) and keep renderItem light; memoize (React.memo for pure components, useMemo/useCallback for expensive values/callbacks passed to children) to cut needless re-renders; and keep component state minimal and local (lift/split it so one change doesn't re-render the whole screen). Images + assets: optimise and correctly size images, cache remote ones (expo-image), and paginate/lazy-load rather than loading hundreds at once — images are a top memory/perf cost. Startup: reduce bundle/startup work, lazy-load heavy screens, and keep the splash until ready (the Hermes engine and RN's new architecture improve startup + memory — use them). Work off the main path: debounce/throttle rapid events (search input, scroll), batch updates, and move heavy computation out of render (or to background/native). Animations: Reanimated + native driver for 60fps under load. And above all — measure, don't guess (next lesson): profile to find the real bottleneck before optimising, so your effort goes where it actually matters.
The mistake beginners make
The first mistake is blocking the JS thread — doing heavy work (big loops, huge re-renders, non-stop state updates) during interactions, so the app stutters and lags; keep the JS thread free, debounce, and move heavy work off the main path. The second mistake is rendering long lists with .map — building thousands of rows at once instead of virtualizing; use FlatList. The third mistake is needless re-renders + unoptimised images — re-rendering the whole screen on every small change (no memoization, bloated state) and loading huge, uncached images; memoize, minimise state, and optimise/cache images. And optimising by guessing — rewriting code that isn't the bottleneck while the real one remains; profile first. And only testing on a high-end phone — where jank hides; test on low-end devices too. Keep the JS thread free, virtualize lists, cut re-renders + optimise images, profile before optimising, and test on low-end devices.
Your turn
Your turn
- Fix a list: convert any long .map-rendered list to a FlatList with keyExtractor (and getItemLayout if rows are a fixed height), and lighten the renderItem so scrolling stays smooth.
- Cut re-renders: wrap a pure child in React.memo and use useMemo/useCallback for expensive values/callbacks passed to it, and split/minimise state so a small change doesn't re-render the whole screen.
- Optimise images: ensure images are correctly sized + cached (expo-image) and long image lists are paginated/lazy-loaded rather than all loaded at once.
- Keep the JS thread free: debounce a rapid event (e.g. a search input), move any heavy computation out of render, and confirm interactions/animations stay responsive (use Reanimated for animation).
- Test on a low-end device: run the app on an older/low-end Android phone (not just a flagship) and look for jank on scroll, startup, and animations — fixing what you find.
Key points
- 'Smooth' = 60fps (a frame every ~16ms) + fast startup + instant touch response; missing frames = visible JANK. RN has a JS thread (your code/state/logic) and a UI thread (drawing + native animations) — jank happens when a thread is BLOCKED.
- Keep the JS thread free: avoid heavy work during interactions, do expensive work lazily / off the main path, debounce rapid updates; use FlatList (virtualization) for long lists — never map thousands of rows; run animations/gestures on the UI thread (Reanimated + gesture-handler).
- Cut re-renders: React.memo for pure components, useMemo/useCallback for expensive values/callbacks passed to children, and minimal/split state so one change doesn't re-render the whole screen; optimise + size + cache images (a top perf/memory cost) and paginate/lazy-load.
- Improve startup (lazy-load heavy screens, keep splash until ready; Hermes + the new RN architecture help) and move heavy computation out of render / to background.
- The mistakes: blocking the JS thread (stutter/lag), rendering long lists with .map (use FlatList), needless re-renders + unoptimised images, optimising by guessing (profile FIRST — next lesson), and only testing on a high-end phone (test low-end too, where jank shows).
Q&A · 0
Enrol to ask questions and join the discussion.
No questions yet — be the first to ask.