How to modernize mobile retail apps with Expo

UsersDevelopmentReact Native21 minutes read

Jonathan Bones

Jonathan Bones

Guest Author

Learn how bitglow modernized DEPOT's React Native app with Expo Prebuild, cutting upgrade time by 80% and boosting performance scores from 36 to 90.

How to modernize mobile retail apps with Expo

This is a guest post from Jonathan Bones - a Senior Frontend Dev at bitglow who loves the details: upgrades, optimization and debugging. You can find him on GitHub & Bluesky.

Expo and React Native empower developers to build fast, beautiful apps. But without careful attention to detail, especially on low-end devices, that promise can quickly fade, leading to developer frustration and customer dissatisfaction. Sound familiar? That was exactly the challenge faced by DEPOT, a prominent home décor retailer in Germany.

Equipped with the latest features from Expo and Expo’s Cloud Services along with the deep React Native expertise of our team at bitglow, we helped DEPOT to completely overhaul their outdated React Native setup, stripping away years of technical debt to build a future-proof codebase.

Why should this matter to you?

Because when technical debt slows down your dev team, even the best ideas won’t reach your customers fast enough. Don’t take my word for it - here’s what users were saying before we started:

★☆☆☆☆ “The app could be so great if it didn’t keep crashing. You constantly have to wait because everything takes forever to load or doesn’t load at all.”

Over the past year, as we’ve gradually revamped the app, feedback has started to shift. While some user concerns now focus on particular features or bugs, the conversation has noticeably moved away from performance frustrations:

★★★★★ “Excellent app. It would be even better if users could bookmark a nearby store and the app would then show whether the item is available at that branch or not.”

This transformation didn't happen overnight. It started with an audit of the existing codebase to understand the pain points and establish a baseline. Gaining a clear picture of where we stood was the first step in defining a path forward.

Auditing the codebase

When we joined the project, our audit revealed the full scope of what we were working with:

  • Ejected Expo app - React Native 0.63
  • React Navigation v5 with heavy use of the deprecated v4 compatibility layer
  • Redux for state management + Sagas for side effects such as API calls
  • TypeScript v4.4

It wasn’t the most modern setup, but it was a solid enough foundation to build upon. Leveraging the power of TypeScript we could be confident in refactoring large sections of the codebase whilst keeping the rate of new issues near zero.

Our analysis showed that there were four key areas to address:

  • Reducing React Native maintenance overhead by adopting Prebuild
  • Optimizing app performance
  • Automating builds and submissions with Expo’s Services
  • Enabling rapid updates with OTA deployments

From weeks to days: Accelerating upgrade cycles with Expo Prebuild

The main challenge was going to be maintaining and updating the outdated and unmaintained versions of React Native. The app was running on React Native 0.63, several major releases behind the latest stable version. With Google Play and Apple App Store enforcing target SDK updates more frequently, keeping up with policy deadlines was going to be an uphill challenge.

Additionally, reliance on React Navigation v5 with the compatibility layer - which was only ever meant as a temporary solution - increased the potential of subtle bugs creeping in during upgrades to core dependencies.

The first couple of React Native upgrades were performed manually and despite the React Native developer community highlighting this as a pain point in previous editions of the State of React Native survey; since the advent of the Upgrade Helper this process has become a lot more manageable over the past few years.

Note: Expo recently introduced a new Claude Code skill for upgrading your app. We have not tried it yet but we hear that it’s very helpful.

However, even with these improvements, it still took our team a couple of weeks to perform the technical changes, QA the builds, patch any wily bugs and prepare a release. This is valuable development time that was essentially being spent on managing the native code required by the framework.

It was also often viewed as downtime in the sprints as we stopped working on features to concentrate our efforts on upgrades, which often required extra co-ordination and risk management to avoid negatively impacting business targets and deadlines. Could there be a way to speed up the process without cutting any corners?

Adopting Prebuild for faster version upgrades

Expo’s keynote on Continuous Native Generation at app.js conf 2022 convinced us to seriously consider expo prebuild in our clients’ projects. Auto‑generating native framework code rather than manually managing the ios/ and android/ native projects promised shorter, more predictable upgrades and less “feature downtime”.

To validate the recommendation for DEPOT, we drew on our experience from previous projects and extrapolated based on factors such as time spent on manual updates, the number of native and non-native dependencies, and the overall size of the codebase. This allowed us to come up with two estimates: one for the migration effort itself and one for the time saved during future upgrades.

Our conclusion was that the migration would require roughly the same amount of time as a single React Native upgrade. In return, future upgrades could be completed in as little as 20% of the time previously required, reducing upgrade cycles from several weeks to just a few days.

Over time, we've completed multiple migrations from React Native CLI to Expo and have become very familiar with the details required to make that process a success. Combined with the continuous improvements from the Expo team and community, this has made migrations increasingly more predictable and efficient. In practice, for many projects the upfront effort is genuinely comparable to a single React Native upgrade, and the return on investment begins with every upgrade thereafter.

This becomes even more compelling when a migration includes multiple React Native version upgrades in parallel, as it did for DEPOT. By spending roughly the same amount of time it would traditionally take to upgrade a single React Native version, we were able to complete the full migration while upgrading three versions at the same time. The result was an immediate time saving, followed by consistently faster and less painful upgrades going forward.

Preparing the migration

We began with a dependency audit. Every entry in the package.json was classified as native or pure JS and we ran unimported to identify unused libraries - this trimmed @react-native-voice/voice, isomorphic-fetch, and traverse on the first pass. We also paid long‑standing technical debt by finally removing the @react-navigation/compat layer and refactoring references to StackActions and NavigationActions with the useNavigation hook instead, clearing the way for upgrades to newer React Navigation releases.

We also scanned for low‑usage dependencies (≤3 references) and replaced a few with tiny custom implementations; for example, react-native-multi-tap-component became a vanilla RN component. Pruning the dependency footprint like this significantly reduced native integration risk and was the single most impactful step in preparing the DEPOT app for adopting Prebuild.

Build configuration setup

With our trimmed package.json in hand, we checked for config-plugin support in the remaining native packages using the Expo config‑plugins repository as a guide. Most native dependencies already had first-party or community plugins available - a great sign. For the ones that didn't, we needed to implement a custom config plugin. However, this turned out to be remarkably straightforward.

The Expo docs were very clear and using existing config plugins as a reference, we had support for the remaining native SDKs within a few hours. What could have been a major blocker proved to be a breeze, giving us the confidence to proceed.

The migration moved in deliberate stages. We added the android/ and ios/ folders to the .gitignore and simplified the app entry point to a minimal render so that we could focus on restoring native integrations before tackling TypeScript and business logic issues:

Code
export default function App() {
return (
<View style={styles.container}>
<Text>If you can see this the app builds and starts</Text>
</View>
);
}

Next we installed the latest version of Expo SDK and added expo-dev-client, running npx expo install expo --fix to update native libraries and align configuration with the installed SDK version. The app.json was renamed to app.config.ts in order to enable build flavors keyed off an APP_VARIANT environment variable:

Code
import { ExpoConfig } from "expo/config";
const name = {
development: "DEPOT (dev)",
preview: "DEPOT (preview)",
production: "DEPOT",
}[process.env.APP_VARIANT ?? "development"];
export default (): ExpoConfig => ({
name: name!,
version: "6.1.0",
// ...other properties
});

An initial iOS build succeeded. 🎉 The Android build threw a few Gradle errors our way. Digging into the stacktrace, we quickly identified the culprit: an outdated Emarsys SDK version that was incompatible with the generated native project. The solution was straightforward—upgrade the SDK, clean the build artifacts and caches, and rebuild.

Restoring app functionality

With the native integration setup, the next step was to add back the initial app code, where we discovered a blocking issue: navigating to the profile tab produced a visibly janky transition and then a crash.

The stack traces and reproduced behavior pointed to the navigation layer rather than application logic, so we were faced with a choice: invest time patching the problem in the unmaintained v5, or tackle the debt and upgrade to the latest supported version (v7). Given the long‑term maintenance cost of patching deprecated internals, we chose the upgrade path, which is worthy of a blog post in itself.

The ROI of the app upgrade

By pruning unused and fragile dependencies, upgrading navigation and SDK versions, and adopting Prebuild to auto‑generate native project files, we converted a high‑risk, time‑consuming workflow into a repeatable, automated process.

The benefits are already visible: faster Time‑To‑Upgrade (TTU) and noticeably higher developer velocity. Expo and React Native upgrades now only require 20% of the effort they used to - freeing up the team to focus on product tasks instead of firefighting native issues. Operationally, Target API level requirement emails no longer trigger the same level of concern they once used to.

Tackling performance bottlenecks

Now that the native dependency integrations had been cleared out of the way, there were a number of JS-domain performance issues to tackle. Before diving in, we needed a solid benchmark to measure progress against. We created a series of typical user flows using Maestro and leveraged Flashlight to obtain a Lighthouse-based performance score covering both JS and native thread usage. Here's a snippet of the Maestro script we created that navigates to a category details page, paginating through the results:

Code
- launchApp
- assertVisible: "Entdecke dein DEPOT"
- tapOn: "Deko & Wohnen"
- tapOn: "Kerzen & Lichtobjekte"
- tapOn: "Kerzen"
- tapOn: "Stumpenkerzen"
- waitForAnimationToEnd
# Title of first product in "Stumpenkerzen" PLP
- assertVisible: ${PRODUCT_TITLE}
# Imitates a fast scroll to bottom of listing page
- swipe:
start: 50%, 75%
end: 50%, 25%
duration: 40
- assertVisible: "Mehr Produkte sehen"
- tapOn:
text: "Mehr Produkte sehen"
index: 1
- waitForAnimationToEnd
- scroll
- scroll
- scroll

The benchmarks were collected on a representative budget device (Xiaomi Redmi 9, Android 12) and we obtained an initial score of 36. Clearly there was room for improvement, but importantly this confirmed our observations and the customer feedback related to scrolling issues on the product listing pages.

The next step was deciding on a target score to measure progress against. We chose a pragmatic score of 85, since reaching higher scores demands exponentially more effort for minimal user-facing returns. With our baseline established, we systematically identified and tackled performance bottlenecks across several key areas:

Image optimization

This proved to be one of the highest-impact opportunities. The product listing pages were rendering oversized images, which not only increased download times but also created memory pressure during decoding and resizing on the main thread. This directly impacted CPU load and degraded Time-To-Interactive (TTI) on the product listing pages.

To address this, we leveraged image resize parameters on media asset URLs, ensuring that devices download appropriately sized images. By shifting the workload upstream, we significantly reduced UI-thread decoding overhead, reducing CPU load.

Before and after optimization (JS & UI thread FPS).

Network efficiency

The app was fetching product details for every item rendered in the list and then re-fetching the same data when navigating to the product detail page (PDP). This stemmed from three issues:

  1. No request batching → a burst of N individual calls was performed on PLP mount rather than a single aggregated request
  2. Weak client-side caching → repeated fetches for data that was already present in memory
  3. High-latency persistence → read/write through redux-persist backed by AsyncStorage introduced extra pressure on the JS thread

To resolve these issue, we batched product API requests and began migrating away from Redux Saga + Redux Persist to TanStack Query, which provides a strong caching layer out-of-the-box. This eliminated duplicate product fetches immediately and lightened the network load considerably, resulting in much snappier navigation transitions when browsing the product lists.

Render performance

Profiling revealed that a substantial portion of JS-thread time was consumed by repeated, unnecessary re-renders in the list components. Many of these renders were triggered by unstable props and expensive data manipulations directly inside the render loop.

To address this, we shifted expensive manipulations into the data-fetching layer, ensuring that the render loop receives presentation-ready data. We also applied targeted memoization and encouraged usage of stable primitive props e.g. passing a stable product ID to a product tile rather than a full product object.

Additionally, we modernized the codebase by migrating remaining class components to functional components. This not only reduced boilerplate and improved consistency, but also unlocked React Hooks, enabling more granular state management, finer control over update boundaries, and lower maintenance overhead.

Collectively these changes significantly reduced JS thread load and delivered improved scroll performance across the PLPs.

The gains from our performance optimizations

By incrementally incorporating each of these optimizations, we achieved a Flashlight score of 90 - representing a 48% improvement in average CPU usage and a huge 91% improvement in high CPU usage duration. The result is a meaningfully smoother and more responsive browsing experience when scrolling through the product catalog.

Streamlining CI/CD with Expo’s Services

Modern mobile app development extends beyond just writing code - it demands quick, reliable and repeated delivery. However, manually juggling local toolchains to build, sign, and distribute binaries introduces friction, complexity, and risk at every step. We solved this by integrating a fully automated build and deployment pipeline into GitLab CI, powered by Expo’s Cloud Services.

Here's how it works: whenever code is merged into a protected branch (main or release/), a single click triggers a preview variant build for iOS or Android - no local setup or credential juggling required.

This eliminates the friction of waiting for builds to finish, wrestling with environment issues, and manually sharing binaries. Instead, the pipeline uploads the build manifest to Expo, shares the URLs to Teams and Expo takes care of the rest. Designers, QA, and product teams get instant access to the latest preview, while us devs can move on to working on the next task without interruption.

When it’s time to release to the App Store or Google Play, the process is just as streamlined. Tagging a release in Git automatically triggers a production build and, thanks to Expo’s auto-submit feature, the finished binary is submitted directly to the relevant app store - again, with zero manual steps. Here’s the job for iOS production:

The impact? We’ve eliminated the need for dedicated build hardware, reduced the risk of human error, and freed up even more valuable developer time.

The release flow is predictable, repeatable and the builds are delivered straight to stakeholders or app stores. For DEPOT, this translates directly into lower operational costs, faster time-to-market, and a development team that can focus on building features instead of wrangling local build toolchains.

Protecting revenue with over-the-air updates

Let me share a real-world example that perfectly illustrates why over-the-air (OTA) updates are a game-changer for any business with a mobile revenue stream. It was late on a Friday afternoon, and DEPOT’s content management team was finalizing a major weekend sale campaign; one projected to deliver a substantial revenue boost. Suddenly, a critical issue surfaced: a misconfigured campaign key in the app threatened to derail the entire promotion just hours before launch.

In a traditional setup, this would have triggered a crisis: the team would face a tough choice between delaying the campaign (and losing out on weekend sales), pushing a risky workaround, or spending the weekend anxiously waiting for an App Store review to approve a hotfix. Any of these outcomes would have meant lost revenue, frustrated stakeholders, and a team on edge.

Instead, because we had OTA updates in place, we were able to diagnose and resolve the issue immediately. Within minutes, a fix was pushed directly to users - no app store resubmission, no waiting, and no disruption to the customer experience (apart from a brief “critical update” banner). This is the true power of React Native: the bug was isolated to the JavaScript layer so we could update every device seamlessly as soon as users relaunched the app. The campaign launched on schedule, the team enjoyed a stress-free weekend, and, most importantly: DEPOT didn't miss out on any anticipated revenue.

This isn’t just a technical win; it’s a business safeguard. This experience is a clear reminder that technical blockers can get in the way of sales targets. An OTA update system like EAS Update is a strategic asset, giving your business the agility to respond instantly to market needs, fix critical issues on the fly, and protect revenue - all without being at the mercy of app store approval timelines. In today’s fast-paced retail environment, that’s not just a technical advantage; it’s a competitive necessity.

Summary

Our journey with DEPOT is proof that React Native, when combined with the right technical expertise, can deliver exceptional performance and stability - even on budget devices. By systematically addressing technical debt, modernizing architecture, and automating critical workflows with EAS, we transformed a fragile legacy codebase into a robust, scalable app that’s ready for the future.

The results speak for themselves: faster release cycles, higher developer velocity, and a user experience that is gradually earning more positive feedback and regaining user trust. Most importantly, these improvements have enabled DEPOT to respond more quickly to business needs and seize new opportunities without being held back by a tech stack that didn't scale with demand.

If your team is facing similar challenges or you’re looking to future-proof your mobile codebase, we’d love to help. This post was brought to you by bitglow - specialists in React Native & Expo 🩵

OTA Updates
Expo Prebuild
EAS

Dive in, and create your first Expo project

Learn more