March 29, 2026·11 min read
List performance myths in React Native
ScrollView renders all children. That’s fine for twelve items and a trap for twelve hundred. Virtualized lists exist because memory is finite and users scroll faster than you expect. Pick the tool for the data size, then measure—do not cargo-cult FlatList or ScrollView.
Measure first
Use performance monitors or just watch JS thread spikes. If you’re under budget, ship the simpler layout.
Nested virtualized lists need care—sometimes FlashList or a different layout wins.
Myth: “FlatList is always slow”
Misconfigured FlatList is slow. Stable keys, reasonable `windowSize`, and lightweight `renderItem` fix most issues.
Images in rows need the same discipline as anywhere else.
When ScrollView wins
Short static content, screens that must measure full height, or wizard steps with few panels. Know the tradeoff explicitly in code comments.
Don’t mix infinite scroll inside ScrollView without a plan.
Red flags in code review
`ScrollView` mapping hundreds of rows, `FlatList` inside `ScrollView` with unbounded height, or `keyExtractor` returning Math.random. Each is a common source of jank that passes small-data QA.
If you must nest scrollables, document why and test on a low-end Android with production-like data.
Virtualization economics in production feeds
Feeds are memory-bound before CPU-bound on many devices. Virtualization trades CPU for bounded memory by recycling off-screen rows—essential for hundreds of items. The cost is complexity: stable keys, careful `renderItem`, and attention to `extraData`. Myth: virtualization is always slower than ScrollView for ‘medium’ lists—misconfigured FlatList is slow, but a ScrollView rendering hundreds of complex rows is worse because every child stays mounted. Measure with production-like data: images, variable heights, and network-loaded avatars change the story. Nested scrollables remain a footgun—prefer redesigns that separate horizontal carousels from vertical feeds with explicit height contracts. FlashList improves some workloads with better recycling heuristics—benchmark with your actual row component, not a placeholder. Watch out for `removeClippedSubviews` on Android with absolutely positioned children—clipping can break shadows or overlays. Document list performance budgets for your team: max items before pagination, max image decode size in rows, and expectations for skeleton loaders.
Profiling list jank without guessing
Start with the performance overlay: JS vs UI thread. If JS is pegged, optimize `renderItem`, memoization, and data transformations—maybe move sorting and filtering off the render path. If UI is pegged, look at image decoding, shadows, and overdraw. Systrace and Flipper tools vary by RN version—follow current docs. Capture traces on a cold start versus warm scroll—they differ. For variable-height rows, incorrect `getItemLayout` skips cause misalignment—either measure carefully or omit when heights vary wildly. Pagination should debounce ‘load more’ to prevent thrash when users bounce at list ends. Empty and error states should be lightweight components—do not mount heavy charts in placeholders. When using context, remember all subscribed rows rerender—narrow context or use selectors. After optimizations, compare memory with Xcode Instruments or Android Profiler—leaks often hide in image caches or event listeners not cleaned on unmount.
When ScrollView is still the right tool
Short static screens, legal text, onboarding without dynamic lists, and form wizards with a dozen fields often belong in ScrollView. If you must measure content height for animations or printing, virtualization fights you. The key is intentionality: comment why ScrollView is chosen so future devs do not ‘optimize’ it into FlatList blindly. If content can grow—user-generated attachments—revisit the decision and add virtualization when thresholds exceed your performance budget. Mixed patterns exist: ScrollView for header + FlatList for feed with `ListHeaderComponent`—watch nested scroll conflicts. Testing on low-memory Android with developer options showing GPU overdraw highlights expensive stacks. Educate designers that infinite feeds have engineering constraints—sometimes ‘load more’ buttons beat invisible infinite scroll for accessibility and predictability.
Team practices that keep lists fast
Establish shared list primitives: a `StandardRow` with image sizing, text truncation, and memo defaults. Centralize image component choice—expo-image or similar—with caching rules. Add lint rules banning inline arrow functions in hot paths if your team repeatedly regresses memoization. Performance-test major list screens in CI with synthetic data volumes—catch spikes before merge. Document known third-party issues—certain chart libraries jank when recycled. Train new hires on key extractor pitfalls early—duplicated keys are silent killers. When incidents occur, write short postmortems with traces attached—patterns emerge across products. Lists are user-perceived performance: treat regressions as P1 when they affect core feeds.
Shipping and reliability habits (1)
Native modules are product decisions disguised as engineering tasks. You inherit Xcode and Gradle upgrades, store review scrutiny, and security obligations. Prefer maintained Expo modules and config plugins before writing JNI or Swift glue from scratch. When you must go native, budget pairing time with platform specialists and write runbooks for on-call—crashes in native code bypass many JS safeguards.
Deep links are a cross-team system: marketing URLs, hosted association files, entitlements, router params, and analytics query preservation. Debug with structured logging of raw URLs (scrub secrets) and reproduce cold-start races with auth hydration. Staging and production should be obviously separated—accidentally opening prod from a QA link erodes trust and pollutes data.
Platform differences worth rehearsing (2)
OTA updates are powerful and risky: runtime compatibility, rollback plans, and user-visible behavior changes need governance. Channels should map to release maturity—staging versus production—with access controls on publish credentials. Large assets over cellular need care; silent failures erode trust more than a frank ‘update failed, retry’ message.
Keyboard and form UX separate polished apps from ‘works on desktop simulators.’ Platform differences in soft input modes matter; test smallest phones and Android gesture navigation. Primary actions must remain reachable when the keyboard is visible—scroll containers and keyboard controllers exist because this problem is universal.
Security, privacy, and data handling (3)
Expo SDK upgrades are integration projects: `expo doctor`, aligned community packages, regenerated native projects, and device smoke tests for camera, push, and IAP. Freeze unrelated native refactors during the upgrade window and keep rollback paths hot. Document surprises for the next upgrade while memory is fresh.
Hermes versus JSC is not a lifestyle choice—profile your app. Hermes usually wins on startup; some libraries still assume JSC quirks. Engine toggles are not substitutes for fixing quadratic renders in your own code. Upgrade notes matter: Intl support and debugging tooling evolve.
Performance and measurement discipline (4)
Metro cache issues masquerade as logic bugs. Establish a documented reset ladder: dev server restart, cache flags, Watchman, derived data, then dependency reinstalls. Compare platforms when only one breaks—native steps diverge. Keep CI caches deterministic with lockfiles and pinned toolchains.
Environment variables should be classified: public-by-design, sensitive-with-mitigations, or never-on-device. `EXPO_PUBLIC_` values are extractable—treat them that way. Align env handling across EAS profiles and local dev; fail fast when keys are missing instead of shipping undefined behavior.
Team process and long-term maintenance (5)
Internationalization is a product feature, not a string swap. Plural rules, RTL layout, and locale-aware formatting change behavior—not just copy length. Pseudolocale helps find clipping early, but real Arabic and German QA catches nuance. Avoid concatenating translated fragments; context matters. Document glossary terms so translators do not invent inconsistent product names.
Monorepos amplify both leverage and failure modes: duplicate React versions cause mysterious hook errors, and Metro misconfiguration blocks local packages from resolving. Invest in workspace discipline—single React version, documented `watchFolders`, and lint rules preventing packages from importing app navigators accidentally. CI must mirror local installs; ‘works on my laptop’ with different package managers is a time bomb.
Shipping and reliability habits (6)
WebViews are untrusted browsers inside your app. Validate `postMessage` payloads, lock navigation to expected hosts, and prefer system-browser auth flows when OAuth security demands it. Third-party JavaScript can change without your deploy—treat XSS in web as bridge compromise risk. Clear storage on logout and rate-limit message handlers.
E2E tests should protect revenue paths, not every permutation. Stable selectors (`testID`) beat text that marketing rewrites weekly. Flake management is a feature: quarantine, fix root causes, and keep smoke suites green on CI devices. Five reliable tests beat fifty flaky ones that everyone ignores.
Platform differences worth rehearsing (7)
In-app purchases require server validation, restore flows, and support tooling that respects privacy. Sandbox quirks are normal—budget QA time. Subscriptions interact with family sharing, regional pricing, and refunds; engineering must stay aligned with finance and legal narratives users see in receipts.
Privacy nutrition labels and Play Data Safety forms should reflect actual SDK behavior—inventory dependencies each release and remove dead code. Drift between claims and telemetry is legal and store risk, not just embarrassment. Involve legal early when adding analytics or ads.
Security, privacy, and data handling (8)
Design tokens and semantic colors make dark mode and rebrands feasible. Mixing three styling systems doubles migration cost—pick a primary approach and draw boundaries. Runtime CSS-in-JS can cost frame time on hot screens—profile before adopting wholesale.
ScrollView versus FlatList is a data-volume question. Small static content belongs in ScrollView; long feeds belong in virtualized lists. Nested scrollables need explicit height contracts—redesign beats fighting physics. Document intentional choices so future refactors do not ‘optimize’ blindly.
Performance and measurement discipline (9)
Security and privacy expectations move faster than roadmaps. Treat analytics, crash, and attribution SDKs as part of your threat model: initialize them deliberately, document data flows, and verify ‘off’ truly stops network calls. Client-side secrets are public secrets—anything shipped in an APK or IPA should be assumed extractable. Pair mobile changes with backend policies so authorization remains consistent across platforms.
Accessibility is compatibility. Labels, focus order, and dynamic type are not polish—they determine whether users can complete tasks at all. Test with VoiceOver and TalkBack on hardware; simulators miss focus bugs. When designs prioritize minimalism, negotiate text alternatives for icon-only controls. Accessibility regressions often follow navigation redesigns—add checklist items to those PRs specifically.
Team process and long-term maintenance (10)
Error boundaries catch render failures, not native crashes or async mistakes. Pair them with platform crash reporting and structured client logs. Fallback UI should include build identifiers and humane copy—never raw stack traces for end users. Test fallbacks with screen readers; a broken error screen is still broken UX.
Storage is not a database. AsyncStorage and MMKV excel at key-value preferences; SQLite or remote APIs belong elsewhere for relational data. Migrations should be incremental, logged, and non-blocking for UI. Secure tokens need secure storage when your model demands it—speed is not a substitute for correctness on auth material.
Shipping and reliability habits (11)
Storage is not a database. AsyncStorage and MMKV excel at key-value preferences; SQLite or remote APIs belong elsewhere for relational data. Migrations should be incremental, logged, and non-blocking for UI. Secure tokens need secure storage when your model demands it—speed is not a substitute for correctness on auth material.
WebViews are untrusted browsers inside your app. Validate `postMessage` payloads, lock navigation to expected hosts, and prefer system-browser auth flows when OAuth security demands it. Third-party JavaScript can change without your deploy—treat XSS in web as bridge compromise risk. Clear storage on logout and rate-limit message handlers.
Platform differences worth rehearsing (12)
JWT and session refresh flows need single-flight refresh, clear logout semantics, and secure storage for refresh tokens when appropriate. Parallel 401s should not stampede refresh endpoints. Clock skew and biometrics policies belong in explicit product decisions, not accidental implementation details.
In-app purchases require server validation, restore flows, and support tooling that respects privacy. Sandbox quirks are normal—budget QA time. Subscriptions interact with family sharing, regional pricing, and refunds; engineering must stay aligned with finance and legal narratives users see in receipts.
Security, privacy, and data handling (13)
Background execution policies change with OS updates—revalidate after major iOS and Android releases. Misused background modes invite rejection. Persist user work frequently; the OS can kill you anytime after backgrounding. Uploads and timers should tolerate pause and resume without corrupting state.
Design tokens and semantic colors make dark mode and rebrands feasible. Mixing three styling systems doubles migration cost—pick a primary approach and draw boundaries. Runtime CSS-in-JS can cost frame time on hot screens—profile before adopting wholesale.
Performance and measurement discipline (14)
Performance work should start with measurement, not instinct. Watch JS thread versus UI thread separately; they bottleneck differently. Lists, images, and animations dominate most regressions—optimize those before micro-optimizing pure functions. Hermes, JSC, and bridge internals evolve; re-profile after every major upgrade instead of trusting last year’s numbers. Battery and thermal throttling on mid devices reveal issues flagship phones hide.
Security and privacy expectations move faster than roadmaps. Treat analytics, crash, and attribution SDKs as part of your threat model: initialize them deliberately, document data flows, and verify ‘off’ truly stops network calls. Client-side secrets are public secrets—anything shipped in an APK or IPA should be assumed extractable. Pair mobile changes with backend policies so authorization remains consistent across platforms.