
A sales inbox might look simple from the outside. In practice, it is one of the busiest parts of a mobile app. Agents move between customer conversations, open attachments, check contact context, reply, return to the inbox, and repeat that flow throughout the day. At respond.io, the platform is B2B SaaS, while the mobile workflow involves high-volume B2C customer conversations. Sales and support teams use the app to manage active chats with customers across several channels, often on older phones and inconsistent networks. That puts steady pressure on inbox loading, navigation, rendering, memory, notifications, and media handling. We built the app with React Native because the core workflow is shared across iOS and Android. It gave us one main application layer for the inbox, chat, uploads, notifications, and contact views, while keeping native Swift and Kotlin available for specific performance-sensitive work. As the app grew, we started seeing slower screen loads, laggy navigation, and more crashes on older Android devices. In this article, I explain how we measured those issues, why the existing implementation reached a limit, and what we changed to make the mobile sales workflow more responsive under heavier use. Start With the Screens Where Sales Work Happens We did not instrument every screen in the app. We focused first on the Conversation List, which works as the agent inbox, and the Chat Screen, where agents read and reply to customers. These were the two screens with the highest number of user transactions in our visibility tools. They also represented the most repetitive workflow in the app. App startup was the third area we tracked. A slow start affects every other interaction, particularly when agents open the app from a notification or return from the background. The telemetry integration took seven weeks from planning through to production rollout. That included wiring up tracing, defining what screen-ready meant for each core flow, validating the data, and resolving integration issues. We used Sentry for production telemetry and Flashlight for controlled device benchmarks. Sentry gave us data on screen readiness, app starts, navigation spans, profiling, and crashes. Flashlight helped us run repeatable tests for CPU usage, RAM usage, and frame stability. We collected data for 14 days on the latest app version and compared it with an equivalent 14-day period on the previous version. That was long enough to reduce daily variation while keeping the investigation focused. The Metrics That Showed the Problem In our instrumentation, Time To Initial Display, or TTID, marked the point where a screen had enough content and functionality for the user to begin the core task. For the Conversation List, p75 TTID was 3.43 seconds on iOS and 5.35 seconds on Android. For an agent opening an inbox, five seconds is long enough to interrupt the normal open, scan, reply rhythm. When the same action happens repeatedly across a shift, that delay becomes impossible to ignore. The Chat Screen result was even more serious. P75 TTID was 1.12 seconds on iOS and 29.87 seconds on Android. A customer thread taking around 30 seconds to become usable creates a clear problem in a sales workflow. It also matched user feedback that opening conversations sometimes took too long, as well as crashes clustered on older Android devices. We also measured navigation process time. This captures the JavaScript work needed to process a navigation transition before the destination screen is mounted and displayed. The Chat Screen had a p75 navigation process time of 126ms. The Conversation List reached 378ms. The Conversation List result suggested that the mount path was doing too much work. The likely contributors included large state updates, expensive useEffect logic, list rendering pressure, and avoidable parsing during screen entry. The production data lined up with what users experienced. The inbox took longer to become interactive after launch or background resume. Navigation into the inbox showed visible jitter on older Android devices. Opening a customer thread could slow down when message updates, rendering, and image decoding happened at the same time. Flashlight helped confirm that this was not only network variability. We ran a simple flow on a control device: open the inbox, open a thread, scroll, return, and repeat. CPU and RAM peaks appeared in the same paths that were slow in production. Why React Native Still Fit the Product The data did not suggest that React Native was the wrong choice for the app. It showed that our implementation had reached a limit for a high-frequency conversation workflow. For this product, shared implementation has practical value. The inbox, chat, media uploads, notifications, contact views, and supporting business logic need to behave consistently across iOS and Android. Maintaining separate feature implementations for every workflow change would increase engineering overhead quickly. React Native also gives teams a route to native code when it is genuinely needed. A specific module or screen can use Swift or Kotlin where JavaScript is no longer the right fit, while the rest of the app remains shared. For sales workflows, that is crucial because performance issues are rarely spread evenly through the app. Most of the pressure occurs in a small number of flows. Things like app start, inbox load, thread open, media handling, notifications, and rapid navigation. The platform still needs careful maintenance. A React Native app with frequent rendering, real-time updates, image-heavy screens, and rapid navigation can become slow for the same reasons that a native app can become slow i.e, too much work happening at once. Where the Existing Architecture Started to Struggle React Native’s older architecture used an asynchronous bridge for communication between JavaScript and native code. In our workload, that bridge began behaving like a queueing system. A user might open a conversation while the inbox was updating. Images might decode while state updates arrived. JavaScript might be handling rendering and navigation at the same time. On newer devices, this was often manageable. On older devices, it showed up as delayed interactions, dropped frames, and higher memory pressure. We had already applied common performance fixes: Reducing unnecessary re-renders Tightening list behavior Splitting larger components Deferring some heavy work Removing obvious waste from the app These changes improved individual hotspots, but the slowest user sessions remained slow. At that stage, the team needed to reduce recurring JS-native overhead and clean up the application work that remained on the critical path. The New Architecture Was Part of the Fix React Native’s New Architecture introduced JSI, TurboModules, and Fabric. JSI reduces the cost of JavaScript-native interop by moving away from the old serialized bridge model. TurboModules provide a more structured approach to native modules. Fabric updates the rendering pipeline. For our app, the goal was to make frequent UI updates and repeated JS-native interactions less expensive during busy sales flows. The migration gave the app more room during heavier interaction bursts. It did not make every screen faster automatically. If a screen still performs expensive parsing, mounts a large list, triggers heavy effects, or loads too many images at once, the user still waits. That became useful during the next phase of work. Once there was less runtime overhead, the remaining application-level delays were easier to identify and fix. Most of the Migration Work Was Dependencies and Regression Control The difficult part was less about enabling a setting and more about getting the dependency graph ready. We audited libraries that touched native modules, rendering, gestures, navigation, safe areas, animation, media, and layout. Libraries such as react-native-ui-lib, react-native-safe-area-view, react-navigation, react-native-screens, and react-native-reanimated needed close attention. The main gaps appeared in packages relying on legacy NativeModule APIs, older UIManager or Paper rendering assumptions, or native code that had not been updated for Fabric and TurboModules. We used three paths for dependencies: upgrade, patch, or replace. Simple utility packages usually took a few hours or one to two days to address. Core UI, animation, media, and infrastructure libraries often took several days to a week because they touched critical user flows and required broader regression testing. Where upgrades were stable, we used them. Where they were unavailable or unreliable, we replaced or patched the dependency. Concrete replacements included moving away from react-native-create-thumbnail and migrating audio from react-native-sound to expo-av . To limit regressions, we established a baseline for crash rate, startup timing, inbox load time, and chat rendering before making changes. We wrapped replacement libraries behind adapters so the rest of the app could keep using the same internal API. Targeted QA covered login, inbox loading, chat, media uploads, and push notifications. We staged rollout, monitored Sentry for crash spikes and metric regressions, and kept changes small enough to roll back per pull request or release flight. What Actually Improved the User Experience The work after the migration had to make the Conversation List and Chat Screen respond more consistently for agents handling customer conversations at volume. We focused on lower p75 screen-load times, lower JavaScript-side navigation processing time, and fewer stalls on older devices. First, we made the initial render lighter. Heavy UI and non-essential computations were deferred until after the screen became usable. Large state updates, avoidable parsing, and costly effects were moved out of the navigation path where possible. This improved time to interactivity and reduced blocked-feeling transitions. Second, we tightened caching rules. The app showed cached data quickly where it was safe, then refreshed data while the screen was visible. This reduced blank-screen moments and prevented repeated fetches and parsing during rapid inbox-to-thread navigation. Third, we addressed image memory. Images are a consistent source of pressure in chat-heavy apps. We resized assets closer to their displayed dimensions and favoured disk caching to avoid holding unnecessary image data in memory during scrolling. We also updated packages and removed unnecessary bundle weight. That reduced startup work and lowered the amount of code running during initialisation. Finally, we improved notification-state visibility. Some reports of missed messages came from notifications being disabled rather than rendering failures. Clearer in-app prompts and better visibility into notification state helped support distinguish those cases from actual app-performance issues. Results We validated the changes using production telemetry and repeatable Flashlight runs, focusing on distributions rather than a single average. In our internal comparisons, chat and conversation screens loaded 40% to 60% faster. Navigation improved by around 50%. Cold and warm starts improved by up to 53%. RAM usage dropped by 41%, and CPU peaks were close to half their previous levels. The strongest results appeared in higher-latency sessions, such as p75 and p90 screen loads and navigation timings. These sessions were more common on older devices and during heavier interaction bursts. The practical outcome was more consistency when agents opened an inbox or moved between customer threads. That is the part that was critical for a high-volume sales workflow. These figures reflect our application, devices, and flows. They are not a generic React Native benchmark. The useful lesson is more specific. Once a conversation-heavy app reaches a certain level of traffic and complexity, platform work and application-level performance work need to happen together. What I Would Do Earlier Next Time I would introduce screen-level monitoring earlier and test the main sales workflow on lower-end Android hardware before the feature set becomes large. I would keep dependency upgrades regular instead of allowing several major versions to accumulate. I would also define performance budgets for the inbox, thread open, and app-start flows from the beginning. These are the screens where high-volume B2C sales work happens. They deserve the same attention as message delivery, notifications, and backend reliability. Closing React Native gave us a practical shared foundation for iOS and Android sales workflows. The New Architecture reduced recurring overhead, while lighter first renders, better caching, lower image-memory usage, and dependency cleanup produced the larger user-facing improvements. The main lessons are: To measure the flows where users do real work. Test older devices. Watch the slowest sessions instead of relying on averages. Keep an eye on RAM before it decides that 900MB is an entirely reasonable lifestyle choice.
View original source — Hacker Noon ↗

