March 21, 2026·10 min read
Images: caching and placeholders that feel intentional
Unoptimized images are the original mobile performance sin. Modern Expo Image (or similar) gives you disk cache, transitions, and blurhash-style placeholders—use them so lists don’t pop and jank. Users feel “fast” when pixels stop jumping, not when you shaved five milliseconds off unrelated JS.
Decode size vs display size
Downloading a 4000px asset for a 96px avatar wastes memory and battery. Resize on the server when you can; otherwise pick sensible `contentFit` and constraints.
Vector or SVG for icons where possible.
Placeholders and errors
A consistent skeleton or blurhash beats empty boxes. Failed loads should show retry, not infinite spinners.
Prefetch hero images on navigation when bandwidth allows.
Lists of images
Recycle views already help; pair with capped concurrent loads so you don’t open fifty connections at once.
Watch memory on low-RAM Android devices.
Cheap wins you can ship today
Add default placeholders for every remote image component, cap width/height in style so the layout does not thrash, and log 404 or CDN failures in staging. On feeds, avoid loading full-bleed images for rows that show thumbnails—generate a smaller URL at the API layer when you can.
Profile with network link conditioner: slow 3G exposes image issues that Wi-Fi-only dev hides.
Network and decode pipeline
Download size and decode size differ; both affect performance. Prefer CDN URLs with width parameters or responsive image services. Cache aggressively but respect `Cache-Control` headers where appropriate. For animated formats, ensure decode does not block scrolling. Profile memory when loading many images—recycling helps but is not magic.
Placeholders and perceived performance
Skeletons and blur hashes reduce perceived load time. Match placeholder aspect ratios to final assets to avoid layout shift. For failures, show retry affordances and avoid infinite spinners. Prefetch hero images on navigation when bandwidth allows; cancel prefetch on fast back navigation to save data.
Lists, recycling, and concurrency
Cap concurrent downloads per screen to avoid saturating connections and memory. Use appropriate `contentFit` to reduce overdraw. Pair with FlatList/FlashList tuning—images amplify list performance issues. Test on low-RAM Android with developer options showing GPU overdraw if chasing jank.
Security and privacy
Only load images from trusted hosts or sanitize URLs. Avoid mixed content issues in hybrid setups. For authenticated images, prefer short-lived signed URLs and refresh logic. Do not log full URLs if they contain tokens.
Accessibility
Set `accessibilityLabel` on meaningful images; mark decorative images hidden from accessibility trees. Provide text alternatives for charts exported as images. Ensure loading states are announced sensibly—avoid rapid repeated announcements.
Offline and stale caches
Define staleness policies—when to refetch versus show cached. Expose pull-to-refresh where users expect fresh media. Handle offline gracefully with cached thumbnails when possible. Clear caches on logout if images include sensitive content.
Operational checklist
Decode sizes appropriate, placeholders consistent, failures handled, list concurrency bounded, memory profiled on target devices, signed URLs managed, and analytics on image load errors for triage. Images are silent performance killers—treat them as first-class engineering work.
Shipping and reliability habits (1)
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.
Platform differences worth rehearsing (2)
Shipping React Native features is less about any single API and more about the system around it: typed boundaries, predictable navigation, and telemetry that tells you what broke in production. Prefer boring, explicit modules over clever metaprogramming that the next hire cannot grep. When platform vendors change behavior in point releases, your defense is automated smoke tests on real devices and a short internal changelog of native assumptions you rely on.
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, privacy, and data handling (3)
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.
Performance and measurement discipline (4)
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.
Team process and long-term maintenance (5)
Splash screens and launch gates should reflect honest readiness: fonts, theme, session, critical remote config—not every SDK under the sun. Infinite splash is worse than a slightly longer branded hold. Match native and JS background colors to avoid flashes; respect reduced motion for animations.
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.
Shipping and reliability habits (6)
Reanimated and gesture libraries earn their place when profiling proves UI-thread work and your team can maintain native upgrades. Worklets have constraints—read errors carefully. Respect reduced motion and test Android timing differences—identical JS does not guarantee identical feel.
Analytics schema governance prevents warehouse disasters: version events, avoid high-cardinality strings, and align names across iOS, Android, and web. Consent gating must stop network calls, not just UI. Separate dev and prod projects to avoid polluting dashboards.
Platform differences worth rehearsing (7)
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.
Security, privacy, and data handling (8)
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.
Performance and measurement discipline (9)
Push notifications walk a line between helpful and intrusive. Prime users with context, respect notification channels on Android, and measure opt-outs after campaigns—spikes mean copy or frequency problems. Payload design affects background behavior; test killed and locked-device states. Tokens belong server-side with rotation strategies; never treat the client as authoritative for subscription state.
Images and maps look simple in mocks and expensive in production. Decode sizes should match display sizes; static map thumbnails are not interchangeable with interactive MapView gestures. Quotas, API keys, and offline behavior need explicit fallbacks—addresses as text, retry buttons, and calm error copy. Monitor vendor dashboards for spikes that indicate bugs or abuse.
Team process and long-term maintenance (10)
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.
Shipping and reliability habits (11)
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.
Push notifications walk a line between helpful and intrusive. Prime users with context, respect notification channels on Android, and measure opt-outs after campaigns—spikes mean copy or frequency problems. Payload design affects background behavior; test killed and locked-device states. Tokens belong server-side with rotation strategies; never treat the client as authoritative for subscription state.
Platform differences worth rehearsing (12)
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.
Type-safe navigation pays off when routes multiply. Keep param lists near navigators, validate external URLs, and avoid serializing non-JSON-safe values through params. Renaming routes is a cross-cutting change—update analytics, push payloads, and E2E selectors in the same release train.
Security, privacy, and data handling (13)
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.
Reanimated and gesture libraries earn their place when profiling proves UI-thread work and your team can maintain native upgrades. Worklets have constraints—read errors carefully. Respect reduced motion and test Android timing differences—identical JS does not guarantee identical feel.
Performance and measurement discipline (14)
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.
Project structure should make ownership obvious: routes as backbone, feature folders for product areas, thin screens, and shared infrastructure that is deliberately named. Refactor in vertical slices with device-tested releases—big-bang rewrites without tests are how teams lose weeks.
Team process and long-term maintenance (15)
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.
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.