March 18, 2026·10 min read
Splash screens: hide them when you’re actually ready
The worst launch feels like: splash → blank → content. Users read that as “slow app.” Hold the splash until fonts, critical config, or auth bootstrap finishes—then hide once. Define “ready” as what the user needs to tap next—not every SDK you could initialize.
Prevent auto-hide until ready
Call preventAutoHideAsync early, await your critical promises, then hideAsync. Don’t await everything—just what blocks first paint.
Timebox: if auth hangs, fall through to a guest experience rather than infinite splash.
Assets and orientation
Mismatch between storyboard splash and JS theme causes a flash. Match background colors at the native and JS layers.
Test dark mode if you ship it; splashes are easy to forget.
Updates and OTA
After an OTA fetch, you may want a different readiness gate. Document what “ready” means for cold start vs resume.
Don’t block launch on optional analytics init.
What belongs in the critical path
Usually: fonts and design tokens needed for first paint, feature flags that gate navigation, and session restore if your home screen depends on it. Usually not: full analytics graph, A/B SDK init that can lazy-load, or prefetching every catalog page.
Write the list down—when launch regresses, you will know what you allowed to block the user.
User perception and measurable readiness
Users tolerate short splash screens; they do not tolerate blank flashes or frozen spinners. Define readiness as ‘first interactive frame that matches product promises’—fonts, theme, session, critical remote config. Instrument time-to-interactive and segment by device tier. If you hide splash too early, you trade a longer splash for perceived speed incorrectly—measure both.
Coordinating fonts, assets, and theme
Preload fonts used on first paint; defer nice-to-have weights. Match splash background colors between native and JS layers to avoid flashes. For dark mode, align native splash assets or accept a brief mismatch—document the choice. Animated splashes can delight but add complexity; ensure they do not block readiness logic.
Auth and remote config gates
Awaiting auth should have timeouts and guest paths. Remote config should fail open or closed per risk—document which. Avoid serializing every SDK init before splash hides; parallelize independent tasks and timebox slow ones. Feature flags should have defaults that let the app function offline when possible.
Updates, OTA, and splash interactions
If fetching OTA bundles during splash, show determinate progress when feasible. Handle failures with retry and offline modes. Ensure splash logic respects runtime version compatibility—reject bad bundles early with user-readable errors in dev/internal builds.
Testing splash flows
Cold start, warm start, upgrade from older versions, low network, and airplane mode. Test with slow storage devices. Capture video recordings for design review—subjective flicker is easier seen than logged. Compare iOS and Android; parity is ideal but document acceptable differences.
Accessibility and reduced motion
Respect reduced motion settings for animated splashes. Ensure VoiceOver/TalkBack users hear meaningful announcements when transitioning from splash to content—silence can imply a hang. If showing branded animations, provide alt text or skip options where appropriate.
Release checklist
Readiness criteria documented, splash hide timed, failures handled, metrics in place, designers signed off on transitions, QA matrix completed across devices, and rollback plan if a bad SDK init blocks launch. Splash issues are P0—treat them accordingly.
Shipping and reliability habits (1)
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 (2)
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 (3)
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 (4)
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 (5)
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 (6)
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.
Platform differences worth rehearsing (7)
Testing onboarding changes with funnel metrics beats debating opinions. Segment by acquisition channel and platform; back behavior differs. Skip paths must be genuine—dark patterns may win short metrics and destroy brand trust. Localization length tests prevent clipped CTAs in verbose languages.
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.
Security, privacy, and data handling (8)
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.
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)
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.
FlatList performance is configuration as much as code. Stable keys, reasonable `windowSize`, and memoized rows beat switching to a different list primitive blindly. Nested virtualized lists are a last resort—redesign first. Profile with production-like data volumes; dev placeholders lie.
Shipping and reliability habits (11)
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.
Platform differences worth rehearsing (12)
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.
Security, privacy, and data handling (13)
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.
Performance and measurement discipline (14)
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.
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.
Team process and long-term maintenance (15)
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.
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.