Mobile — blog category by Dharmendra Singh Yadav
Category

Mobile

React Native deep-dives, performance fixes, native module patterns and lessons from shipping iOS and Android apps to real users.

React Native, where the rubber meets the device.

Mobile is where good engineering gets tested. The bridge stutters at 60K users. Native modules break between iOS versions in undocumented ways. Your perfect design dies on a low-end Android phone bought three years ago that's still very much in your user base. This category is about how to ship apps that actually work on the messy real world — not just the demo simulator on a recent MacBook Pro.

I've shipped React Native to production for both consumer apps and B2B tools, across iOS and Android, with all the rough edges that implies: native bridge debugging, app store review rejections, push notification certificate expiries at 2am, and the particular flavour of pain that is upgrading React Native to a new version. The posts here are the lessons that came out of those experiences.

Why this category exists

Most React Native content online is one of two things. The first is "compare React Native to Flutter and native" pieces — useful for someone choosing a platform, useless for someone already in it. The second is library demo posts — "here's how to use this new animation library" with three lines of code that don't survive contact with a real app. There's a real gap in the middle.

This category fills the gap. It assumes you've already chosen React Native (because you have, or your team did, and that decision isn't getting reversed). It focuses on what you do once you're there: how to keep your app performant as it grows, how to ship reliably to both stores, how to integrate with native code without losing your weekends, and how to design for the device diversity that web developers rarely have to think about.

It also assumes you've built apps before. There's no "what is JSX" filler. There's a lot of "here's a specific production problem I had and what I did about it." The audience is the engineer two weeks into their first React Native job and the engineer three years in who still doesn't fully understand the Hermes engine.

What you'll find here

The posts in this category cover five broad areas: React Native performance, native module integration, the release process and store realities, push notifications and deep linking, and architectural decisions specific to mobile.

  • Performance — fixing FlatList performance at scale, dropping frames diagnosis, JS thread profiling, Hermes vs JSC, and the specific patterns that survive 100K users
  • Native modules — when to write your own, how to bridge Kotlin and Swift without losing your sanity, and the testing harness that makes this maintainable
  • Releasing to the stores — App Store and Play Store checklists, common rejection patterns, screenshot pipelines, and the surprisingly painful parts of the release process
  • Push notifications and deep links — patterns that work on both platforms, dynamic links, universal links, app links, and the matrix of states that breaks most implementations
  • Cross-platform vs native — the honest tradeoffs nobody talks about, with project-specific framings of when each makes sense

React Native performance: the part people get wrong

"My React Native app is slow" usually has the same root cause: too many re-renders on the JS thread combined with a list that isn't using FlatList correctly. Fix those two things and you've solved 80% of the performance issues in 80% of React Native apps. The remaining 20% is where it gets interesting.

FlatList performance is a category of its own. The defaults work for short lists. They fall apart at long lists, especially with complex item layouts. The specific levers — `removeClippedSubviews`, `initialNumToRender`, `maxToRenderPerBatch`, `windowSize`, key extraction — each address a specific failure mode. Tuning them without understanding what they do is a recipe for surprise regressions. Posts here walk through each, with the mental model for when to reach for which.

Hermes is the other big performance lever for Android. The JIT-vs-AOT decision was painful for a while but Hermes is now stable enough to be the default. Posts here cover the migration path, the things that break (occasionally surprising), and the actual performance numbers in production. The gains are real but unevenly distributed across app patterns.

The JS thread is a bottleneck that web developers don't think about. Heavy animations, complex reducer logic, and synchronous state updates all share the same thread that also handles touch events and scroll. The pattern that helped me most: move heavy work off the JS thread (Reanimated worklets, native animations, background tasks) and measure aggressively. The Flipper performance plugin and the Hermes profiler are both genuinely useful and underused.

Native modules without losing your weekends

The native module story for React Native got dramatically better with the new architecture (Turbo Modules), but most production codebases are still on the old bridge. Both are workable; both have specific patterns that make them maintainable.

The advice that took me longest to learn: don't write a native module if a JS library would do. The cost of a native module isn't writing it — it's maintaining it across React Native version upgrades, iOS releases, Android API level changes, and the next engineer who joins your team and looks at the Kotlin file. Reach for native code only when there's a genuine reason: a hardware API, a performance-critical path, an existing SDK that has to be wrapped.

When you do write one, the testing story matters enormously. Native code that's only tested by "build the app and tap around" rots fast. The pattern I use: integration tests on the native side for the module's own behaviour, JS-level tests for the bridge contract, and explicit version compatibility tests in CI. Posts here cover the specific harness that's held up across several React Native upgrades.

The new architecture (Fabric + Turbo Modules) is worth understanding even if your codebase hasn't migrated yet. It changes the contract between JS and native in ways that affect how you should write modules today, so you're not forced into a rewrite when the migration becomes mandatory.

The release process is harder than people admit

Shipping a React Native build to the App Store and Play Store sounds straightforward in tutorials. In practice, it's a checklist of fifty small details, any one of which can cause a rejection that costs you days. The Apple review process in particular has subtle requirements around screenshots, privacy disclosures, and in-app purchases that aren't documented in obvious places.

The release process I've converged on: Fastlane for the build automation, EAS Build when the team wants managed, hand-rolled GitHub Actions when the team wants control. App Store screenshots generated from Fastlane snapshot to ensure consistency. Crashlytics and Sentry both wired in from the start — the Crashlytics native crash story is still better than Sentry's, but Sentry's JS error reporting is better. Posts here cover the specific pipeline I've used across multiple apps.

The most common rejection patterns I've seen: missing privacy disclosures for third-party SDKs, attempted use of private APIs, account-deletion requirements (Apple now mandates this), and screenshots that don't match the actual app behaviour. Each gets a dedicated checklist post in this category over time.

The over-the-air update story (CodePush, EAS Update) is genuinely useful for shipping JS-only fixes between full app store releases. It's also a footgun if used carelessly — silently shipping changes that bypass the review process violates store policies in subtle ways. Posts here cover the patterns I trust and the boundaries I respect.

Push notifications and deep links: the matrix of pain

Push notifications look simple. You send a payload, the device shows a notification, the user taps it, your app opens to the right screen. In reality, the matrix of states is enormous: app foregrounded vs backgrounded vs killed, iOS vs Android, notification permission granted vs denied vs not-yet-asked, deep link valid vs invalid vs partially valid. Each combination has its own behaviour and its own bugs.

The library landscape doesn't help. `react-native-firebase` is solid but heavy. `expo-notifications` is great if you're on Expo. `onesignal-react-native` is convenient but ties you to their service. Posts here cover the tradeoffs and the specific patterns I've used across different stacks.

Deep linking specifically deserves its own post. Universal Links (iOS) and App Links (Android) are the modern way, replacing the older URL scheme approach for most use cases. Both require server-side verification files that are easy to get wrong. The development workflow for testing deep links is painful — emulators don't behave exactly like devices, and the iOS simulator has its own quirks. Posts here cover the testing harness that finally let me iterate on deep linking without losing hours per attempt.

App size, update size, and the cost of every megabyte

Mobile users don't have the patience web users do. A 60MB app update over cellular is the difference between "user updates" and "user uninstalls and downloads a competitor." Most React Native apps I've audited had app sizes 30–50% larger than they needed to be, and almost none of the teams were tracking it as a metric.

The specific culprits are predictable: unused dependencies hanging around in package.json (and shipped to users), full-resolution images being shipped when WebP at half the size would have been visually identical, fonts loaded eagerly for languages most users don't speak, and the JS bundle itself growing slowly across releases without anyone noticing.

The workflow I've converged on: track app size as a CI artifact across releases, with a hard alert when it grows by more than 5% in a single release. Use `npx react-native-bundle-visualizer` (or the equivalent for your build setup) every few months to spot regressions. Convert images to WebP at the build step rather than checking PNG sources into the repo. Lazy-load translations for languages the user hasn't selected. Strip dev-only code from production bundles explicitly.

For OTA updates (CodePush, EAS Update), the size matters even more because users pay for those bytes on cellular and the silent-install behaviour means a slow OTA is a silently broken update. Posts here cover the specific patterns for keeping OTA payloads small, including the dependency-splitting tricks that let you ship JS-only updates without the asset re-download.

The dev workflow piece nobody talks about: a Metro cache that's gotten out of hand can make local builds enormous. Periodic `rm -rf node_modules/.cache && watchman watch-del-all` is the unglamorous maintenance that keeps the dev experience usable. Worth automating as a postinstall step on long-running projects.

Common mistakes I see (and have made)

The patterns repeat across React Native projects:

  • FlatList without optimization — using `ScrollView` for hundreds of items, or `FlatList` without tuning, leading to predictable performance issues
  • Synchronous everything on the JS thread — heavy computation in render functions, blocking touches and scrolls
  • State management overkill — bringing in Redux on a 10-screen app when Context + reducer would have been enough
  • No device matrix testing — only testing on the latest iPhone and a single Pixel, missing entire classes of bugs on older hardware
  • Push notification permissions asked too early — getting denied immediately, then unable to send notifications without prompting the user to go into Settings
  • App Store rejections from missed compliance — privacy nutrition labels, account deletion, third-party SDK disclosures
  • No offline handling — building network-required UIs without considering the mobile reality of intermittent connectivity

What's coming next in this category

The next few posts on the docket: a deep-dive on React Native performance with the actual profiler workflow I use to find regressions, a write-up of the native module testing harness that's held up across three React Native upgrades, a piece on push notification architecture across iOS and Android with the state matrix laid out explicitly, a comparison of EAS Build vs Fastlane in production, and a long-overdue post on offline-first patterns for React Native.

If there's a mobile problem you're stuck on, the contact form on the homepage works. The most useful posts in this category have come from reader questions — the kind of specific production bug that's hard to search for but trivially recognised by anyone who's seen it before. Those are the posts where one paragraph can save another engineer days of head-banging.

/Frequently Asked

Common questions

React Native if your team is JavaScript-heavy and you want a single language across web and mobile. Flutter if you have no existing JS investment, want consistent UI across platforms with less effort, and don't mind Dart. Both are mature. Both can ship to App Store and Play Store. Don't pick on benchmarks — pick on which language and ecosystem your team can move fastest in. Native (Swift/Kotlin) only when you have a specific reason — performance-critical workloads, deep OS integration, or hiring strength in those stacks.
Two things solve 80% of cases: too many re-renders on the JS thread (fix with memoisation, smaller components, state lower in the tree), and FlatList misuse (tune `removeClippedSubviews`, `initialNumToRender`, `windowSize` or switch to FlashList for very long lists). For Android, switch to Hermes if you haven't — the perf gains are real. After that, use the Flipper performance plugin to profile. Don't optimise without profiling — you'll waste time on the wrong thing.
When there's no choice — a hardware API not yet bridged, a vendor SDK that has to be wrapped, or a performance-critical native path. Don't write a native module just to avoid a working JS library; the maintenance cost across React Native upgrades and OS releases is real. When you do write one, build it with explicit version compatibility tests in CI. The new architecture (Turbo Modules) changes the contract — understanding it now is worth it even if you haven't migrated yet.
Expo for almost everyone. The 'eject and use bare RN' advice is outdated — Expo now supports custom native code via config plugins and EAS Build handles the build pipeline beautifully. The cases for bare RN: heavy custom native code, specific libraries that don't work with Expo, or organisational requirements. For most apps, Expo is faster to set up, easier to ship, and easier to onboard new engineers.
Use Firebase Cloud Messaging (FCM) for both platforms — Apple Push Notification Service is wrapped behind FCM cleanly. Either `react-native-firebase` (full-featured, heavy) or `@react-native-firebase/messaging` (just the messaging piece). The hard part isn't sending notifications — it's handling the state matrix: app foregrounded, backgrounded, killed, permission granted, permission denied, deep link valid, deep link invalid. Build the handler around an explicit state diagram and test each branch.
Usually 24–48 hours for routine updates, longer for first submissions or anything that triggers manual review. The patterns that cause delays: privacy nutrition labels that don't match actual SDK usage, account-deletion flow missing (now mandatory), screenshots that don't match the actual app behaviour, in-app purchase configuration issues. Play Store is faster (often hours) but stricter on policy compliance around personal data. Build the App Store checklist into your release runbook — half the rejections are preventable.

No articles in this category yet.

Back to categories