Cold start & Time-to-Interactive (TTI)
InteractionManager.runAfterInteractions(). Measure TTI before and after — don't guess. “I measure a TTI baseline before optimizing — anything else is guessing.”System design
A senior-level tour of mobile system design, mapped onto React Native and the New Architecture. This is the “how do you think about building a mobile app at scale” material.
System design is decision-making before code. On mobile specifically, interviewers want to see that you design for the worst phone, the worst network, and the worst moment — not lab conditions. This five-step framework keeps you structured under pressure:
| Step | What you do | Time |
|---|---|---|
| C — Clarify | Functional vs non-functional requirements, scale (DAU/MAU), platforms, online/offline expectations. Spend 4–6 min here — most candidates rush it and design the wrong system. | ~15% |
| R — Rough HLD | Draw the high-level diagram: the four layers and the major components. A “city map, not a street map.” | ~20% |
| D — Deep dive | Zoom into one component (repository, chat service, image loader) — interfaces, methods, patterns, edge cases. | ~35% |
| D — Discuss trade-offs | Name the alternatives and why you chose one. This is the senior signal. | ~20% |
| S — Summarize | Recap the design, call out risks, handle follow-ups. | ~10% |
Almost every mobile app shares the same skeleton. Memorize it — it's your HLD template:
| Layer | Responsibility | React Native equivalent |
|---|---|---|
| UI / Presentation | Screens, components, gestures, animations. “Dumb” — renders state, emits events. | Function components, hooks, navigation, Reanimated |
| Business / Domain | Use-cases, validation, app rules. Framework-agnostic, the most testable layer. | Plain TS modules, custom hooks, state stores (Zustand/MobX) |
| Data / Repository | Combines remote + local, caching, mapping DTOs → domain models. The app talks to repositories, never to the API directly. | Repository modules wrapping React Query + local DB |
| Network | HTTP engine, serialization, interceptors, retries. | fetch/axios + Apollo/urql, interceptors, MMKV/SQLite |
Pattern evolution: MVC → MVP → MVVM (view binds to a view-model exposing observable state) → MVI (one-way data flow, immutable state, reducers) → Clean Architecture (strict layer boundaries, dependencies point inward). In React Native, the idiomatic shape is essentially MVVM/MVI: components observe a store, dispatch actions, and a unidirectional data flow updates the UI.
A production API client has six separated pieces: (1) the HTTP engine, (2) the serializer, (3) a typed API service interface, (4) interceptors for cross-cutting concerns (auth token, logging, retries), (5) DTOs that match the server JSON, and (6) the repository that wraps it all and adds caching. Never let DTOs leak into the UI — map them to domain models first.
Wrap calls in a result type (success / failure), set sane timeouts, and retry only idempotent requests with exponential backoff + jitter. Distinguish retryable (network, 5xx) from non-retryable (4xx) failures.
| Strategy | How | Use when |
|---|---|---|
| Offset / limit | ?page=3&limit=20 | Small, stable lists; jump-to-page |
| Cursor | Opaque token to the next slice | Feeds & infinite scroll — stable under inserts (the usual right answer) |
| Keyset | WHERE id < lastId | Large datasets, performance-critical |
Two layers, different speeds and lifetimes: memory cache (RAM — fastest, lost on kill) and disk cache (SQLite / Realm / MMKV / files — survives restarts). Caching buys you speed, battery, data savings, and offline support; the cost is complexity — you must decide what to cache, where, when to evict, and when it's stale.
Offline-first flips the model: the local database is the source of truth, and the network just keeps it in sync. The UI always reads from local storage, so the app keeps working with no signal. Three building blocks:
| Technique | How it works | Best for |
|---|---|---|
| Polling | Client asks every N seconds | Simple, low-frequency data; wastes battery if abused |
| Long polling | Server holds the request until data is ready | Near-real-time without WebSocket infra |
| WebSocket | Full-duplex persistent connection | Chat, presence, live trading — true two-way real-time |
| SSE | One-way server → client stream | Feeds, notifications, live scores (server push only) |
| Push (FCM/APNs) | OS-level delivery when app is backgrounded/killed | Re-engagement, messages while app is closed |
Decision guide: two-way + low latency → WebSocket. One-way stream → SSE. App closed → push notification. “Two-way and low-latency, I reach for WebSocket; one-way stream, SSE; and if the app can be closed, that's push, not sockets.”
renderItem cheap, stabilize keys, avoid anonymous inline functions/objects in props.React.memo, useMemo/useCallback, store selectors so a component re-renders only on the slice it uses. Profile first; don't sprinkle memo blindly.“I profile before I optimize — memoization is a targeted fix for a proven bottleneck, not a default.”
React.memo/useMemo everywhere without profiling first — it adds overhead and complexity while often fixing nothing real.“Sensitive tokens go in the platform keystore, never in AsyncStorage — that's plaintext on disk.”
This is the highest-leverage topic for a senior RN interview in 2026. Know the before/after cold:
| Old (legacy bridge) | New Architecture |
|---|---|
| Async Bridge serializing JSON between JS & native — a batching bottleneck | JSI (JavaScript Interface): JS holds C++ references and calls native synchronously, no JSON bridge |
| Paper renderer (async UI) | Fabric: new concurrent renderer with a shared C++ shadow tree; enables synchronous layout & better React 18 concurrent features |
| NativeModules loaded eagerly at startup | TurboModules: lazy, on-demand native modules → faster startup |
| Manual native interfaces | Codegen: type-safe native interfaces generated from TS specs |
Status to quote: the New Architecture has been the default since React Native 0.76 (late 2024); Hermes is the default JS engine; and the legacy bridge is being fully removed (disabled by default around 0.82). Expo apps get it out of the box on recent SDKs. Hermes matters because it precompiles to bytecode for faster startup, lower memory, and smaller bundles.
Classic React Native runs on three threads: the JS thread (your React code & business logic), the native/UI (main) thread (rendering, gestures), and a shadow thread (Yoga layout). Jank happens when you block the JS thread (heavy synchronous work) or thrash the bridge. The New Architecture + JSI reduce cross-thread cost, and tools like Reanimated run animations on the UI thread so they stay smooth even if JS is busy.
The four constraints that shape every decision: limited resources (battery, memory, CPU, storage), unreliable networks (2G→5G→offline), the app lifecycle (foreground / background / killed), and the fact that you ship to millions of different devices you don't control. Design for the worst case and the happy path takes care of itself.
“I check which thread is actually blocked before I fix anything — JS-thread work and UI-thread rendering fail differently.”
useNativeDriver) keep animating smoothly on the UI thread.The senior playbook from two advanced guides, in a concept → example → problem → solution shape so each idea sticks as a real engineering decision, not a definition.
InteractionManager.runAfterInteractions(). Measure TTI before and after — don't guess. “I measure a TTI baseline before optimizing — anything else is guessing.”!!global.HermesInternal.React.memo only helps if the props are referentially stable.useMemo/useCallback, split broad contexts, and move hot state into a store with selectors so only the slice's consumers re-render. “Context is for low-frequency values like theme or locale; high-frequency state goes in a store with selectors.”renderItem and a stable key.keyExtractor, memoize renderItem, and provide getItemLayout for fixed-height rows. FlashList v2 auto-measures under the New Architecture (no estimatedItemSize). “I always key list items by a stable unique id, never the array index — index keys break on reorder or delete.”useEffect; clear every timer; cancel every subscription; avoid capturing large objects in callbacks that live a long time. Confirm with the memory profiler that the curve flattens. “Every subscription and timer gets a cleanup in useEffect — if I add a listener, the same effect removes it.”"sideEffects": false where true, visualize the bundle to find bloat, and enable minify + shrink (R8) on Android to strip dead native/JS code. “I import specific paths, not whole libraries — barrel imports defeat tree-shaking even if I only use one function.”useNativeDriver: true) while a list does JS work.Animated without the native driver, or heavy layout/compute on the JS thread, drops frames during interaction.useNativeDriver: true, and push heavy compute off the critical path (defer with InteractionManager or a worklet). “Animations that must stay smooth run on the UI thread — useNativeDriver: true or a Reanimated worklet — so a busy JS thread can't drop their frames.”memo/useMemo everywhere adds overhead and complexity while often fixing nothing real..pte binary, delegating to a hardware backend (CPU/XNNPACK by default).useLLM), streaming tokens into the UI.expo-dev-client, then npx expo prebuild + npx expo run:ios -d (real device for iOS release), or build a dev client on EAS..pte into assetExts in metro.config.js if bundling models. “Any native code means a dev build, not Expo Go — I add expo-dev-client from day one.”require() (< 512 MB), remote URL (downloaded to the documents dir with progress), or a local file path the user provides.downloadProgress (0→1)..pte into the binary bloats the app and hits the bundling size limit.isReady, downloadProgress, isGenerating, a streaming response, and interrupt(). Only one LLM instance can be active at a time.interrupt(); token emission is batched (~10 tokens / 80ms) so very fast generation doesn't trigger a re-render storm.interrupt() and wait for isGenerating === false before unmounting; bound memory with a sliding-window context strategy and cap generation length (~256 tokens for short answers). “I never unmount while the model is generating — I call interrupt() and wait for isGenerating to go false first.”ImageSegmentation → SemanticSegmentation) and made init mandatory.