Introduction
We successfully migrated two of our largest apps, Shopify Mobile and Shopify Point of Sale (POS) to React Native's New Architecture while maintaining weekly releases and serving millions of merchants. This migration involved a complex codebase with hundreds of screens and native modules, extensive custom components, and deep integration with first-party libraries like FlashList.
Key outcomes:
- Maintained development velocity throughout migration
- Zero feature development disruption
- Identified and solved common migration issues at scale
This post shares our migration approach, the key decisions we made, and the lessons we learned so other teams can benefit from our experience. We'll cover the practical strategies we used to successfully migrate the Shopify mobile app while maintaining development velocity and app stability.
Migration Strategy: Keeping the Ship Moving
Guiding Principles
Our migration strategy was built on three core principles:
- Minimize code changes first, refactor later: Make only the minimum necessary code changes to enable the New Architecture. Optimization and refactoring could come after. Migrating as quickly as possible was crucial to stop introducing new code that would break the build process on the new architecture.
- Maintain dual architecture compatibility during development: Support both old and new architectures throughout the migration process to enable continuous testing and prevent regressions. Remove old architecture support after shipping.
- Maintain performance and stability parity: Ensure the new architecture matched or exceeded the old architecture's performance and stability, particularly on TTI (time to interactive) metrics and crash free sessions before shipping to production.
Maintaining Development Velocity
At Shopify, we ship app updates weekly, making continuous feature development critical during migration. For large apps, pausing development is risky—technical difficulties could delay essential features or bug fixes.
Our approach balanced migration progress with merchant needs:
- Dual architecture testing: Leveraging TopHat, we generated builds for both architectures on every PR, enabling easy testing without local rebuilds and preventing new architecture breaking changes from merging.
- Conditional functionality: For third-party dependencies that did not simultaneously support both architectures on a single version, we used feature flags to conditionally disable functionality in development mode on the new architecture.
Key insight for library maintainers: Provide a version that supports both architectures. Managing conditional versioning isn't trivial—dual support significantly eases migration burden. This is why we maintained both architectures in FlashList v2 alpha versions.
Technical Deep Dive
Migration Process
- Native Modules Strategy: We did not migrate any modules to TurboModules during the migration. Instead, we only made changes to modules that did not work on the new architecture (mostly modules that dealt with UIManager). We used feature flags to fork the implementation, ensuring compatibility with both architectures. While TurboModules represent a step forward, they are not mandatory (yet), and with 40+ modules in the Shopify App, we decided to re-evaluate them after the migration. This allows us to determine which modules are still useful and how we can improve their APIs by leveraging the new capabilities of TurboModules.
- Dependency Management: We updated incompatible dependencies to dual-architecture versions. We also patched or removed unmaintained libraries. This provided an opportunity to reduce our dependency footprint.
- Upgrade to The Latest React Native Version: The new architecture is under intense development with continuous bug fixes and performance improvements. We prioritized upgrading to the latest React Native version available at the time, and ensuring it worked stable before starting app-specific optimizations. This approach ensured we benefited from upstream improvements rather than fixing issues that might already be resolved. The majority of bugs we encountered were fixed with version upgrades. Therefore we recommend updating to the latest possible version before starting the migration effort.
- App Code Changes: We made minimal changes to ensure feature code worked on both architectures. We also added deprecation warnings to temporary code paths for post-migration cleanup.
Common Migration Issues
The migration required adapting to the new architecture's synchronous rendering model and updated native module interfaces, which impacted how our existing components and native integrations functioned.
Here are the most common issues we faced and their solutions:
State Batching Exposing Component Issues
The new architecture batches state updates together instead of processing them individually. Some components that worked before relied on intermediate state values that no longer exist due to batching, causing them to break.
Solution: Refactor components to not depend on specific timing of state updates. This type of tech debt happens naturally in large codebases and the migration provides an opportunity to clean it up.
Blank Screen of Doom
In our experience, blank screens almost always indicated a TurboModule doing something wrong—either using hacky implementations or old architecture-specific APIs. This is most common with modules that manipulate UI. This caused the JS app to not render anything, making it particularly hard to debug.
Solution: Check modules that interact with UIManager. As a debugging strategy, comment out components and providers one by one in your main App component until you can isolate which one is causing the issue. The UIManager migration discussion was particularly helpful for resolving these issues.
Shadow Tree Manipulation Changes
Manipulating React Native views from native UIManagers can lead to severe UI issues, such as tap gestures not working due to desync between what React Native thinks the view hierarchy looks like and what it actually is. This is something we faced with Mobile Bridge's TransportableView.
In the case of Mobile Bridge, we used native modules to swap WebView components that were declared in React Native code on both iOS and Android. At best, this resulted in WebViews not loading, and at worst, in crashes. We solved this problem by completely removing the WebView from React Native and managing its lifecycle entirely on the native side. This was the simplest approach for our situation since we wanted to maintain compatibility with both architectures and reuse as much of our existing implementation as possible.
Solution: Migrate hybrid components to pure React Native, move them entirely to native or rewrite them using custom shadow nodes.
View Flattening Side Effects
View flattening optimizes components out when deemed unnecessary for the final layout. However, we found cases where components with refs were being optimized out (<View ref={ref}>)
, causing the ref
to always be null
. This created problems when other components tried to use that ref, commonly seen with the ActionSheetIOS
API.
View flattening also caused issues on our end-to-end testing suite, where Appium was no longer able to find some views. To address this we created a Babel plugin to automatically set collapsable={false}
to components with an explicit test identifier.
Solution: Add collapsable={false}
to views that have a ref to guarantee they won't be optimized out. Note that Android already had view flattening prior to the new architecture, so these bugs are more likely to appear on iOS.
Legacy Native Modules on the Main Thread
We had a few “App Hang” issues on iOS during app launch due to legacy native modules that were being loaded in the main thread, while animations were happening. This caused a deadlock, and resulted in a crash. This issue was not easily reproducible in development, and often required closing and launching the app repeatedly for minutes. However, we were seeing numerous crash events in production.
Solution: If you still have legacy native modules, make sure to set requiresMainQueueSetup
to false unless strictly necessary. In most cases, it is not.
Animation Performance Concerns
The Shopify app uses Reanimated extensively for navigation animations. We encountered severe frame rate drops on both platforms—issues unique to our scale and animation complexity.
These challenges highlighted the realities of early adoption on a large project. However, the response from both Software Mansion (Reanimated's maintainers) and Meta was exceptional. Both teams worked closely with us to identify and address performance issues that likely would have gone unnoticed in smaller or less complex applications.
Software Mansion provided early access to patches that addressed most of the performance issues. We're currently collaborating with both Meta and Software Mansion to integrate these fixes into Reanimated and React Native itself, ensuring the broader community benefits from the improvements discovered through our large-scale testing. The performance improvements and bug fixes resulting from this partnership will benefit all teams using Reanimated with the new architecture.
Recommendation: For the majority of apps, we still recommend using Reanimated. These performance concerns appear to be noticeable primarily with complex animations at scale, and depending on where they're applied, may not significantly impact users.
Rollout Approach
Because this is not something we can ship behind a remote feature flag, we used a careful gradual rollout strategy to monitor stability and performance.
It was important to consider the differences in rollout control between the App Store and Play Store.
The Google Play Store offers fine-grained rollout control with the ability to set specific percentages and halt new installations at any time. The App Store, in contrast, uses a pre-determined gradual rollout schedule. When paused, users can still manually update to the new version. Additionally, the approval process for new submissions to the App Store can take up to 24 hours, making hotfixes slower and riskier.
For this reason, we prepared a schedule where we start with Android, gradually increasing the rollout. iOS started a day later, once we had more confidence on the stability of the release.
Rollout Schedule:
- Day 1 - Android 8% and iOS 0%: the goal was to get early signals from Android since it can be fully stopped at any time.
- Day 2 - Android 30%, iOS 1%: substantial increase in Android rollout percentage, while keeping low adoption numbers on iOS to give us time to react.
- Day 3 - both platforms 100%: at this point we have high confidence no major issues are found. Since we can't increase iOS to a specific percentage, we elect to do a full rollout on both platforms so we have data at scale to work with.
Emergency Response Plan:
Our stability target (crash free sessions) for the Shopify app is 99.95%. For this release, we established three response tiers based on stability thresholds:
- Stability above 99.80%: fix forward on next weekly release
- Stability between 99.00% and 99.80%, or broken critical flow with a known fix: pause rollout and hotfix
- Stability below 99.00% or broken critical flow without a quick fix: rollback
Rollback was treated as a last resort because it had severe consequences for our migration timeline. We needed the data that only comes at scale when releasing the app to the public. Rolling back was not only operationally heavy, but it also would have set us back significantly in our migration progress. If we could pause the rollout early and address stability issues instead, we preferred that approach.
What Went Well
The migration was ultimately successful with minimal disruption to our development workflow. We maintained our weekly shipping cadence throughout the entire migration period, delivering new features to merchants without interruption.
While the focus was on making that transition, there were a few immediate wins from just migrating to Fabric:
- App launch times improved by about 10% on Android, and 3% on iOS.
- We were able to simplify our screen rendering by leveraging measure before paint, which led to smoother and faster screen loads cases. You can read more about this on our FlashList v2 blog post.
- The new batched state reduced unnecessary re-renders, which made things like tab switching snappier.
Overall, our team's expertise with the new architecture grew significantly during the process. What started as unfamiliarity became deep understanding, positioning us well for future React Native updates and optimizations.
The broader React Native ecosystem also benefited from our learnings. We rebuilt FlashList from scratch, providing practical code examples on how to optimize rendering performance on the New Architecture. We also worked closely with Meta and Software Mansion to uncover, fix bugs and performance issues that are now being addressed upstream.
Hopefully this blog post can help other teams navigate similar challenges, creating lasting value beyond our own product.
What Did Not Go Well
While we measured performance extensively and made optimizations before rollout, we encountered some challenges that only became apparent at production scale. Despite our due diligence in internal testing, the real-world impact was more significant than anticipated.
Performance Degradation: Some screens needed some performance tuning post release, since the changes to the rendering path broke some of the assumptions that were made when we created these components. We saw increases of up to 20% in load times on some complex components. These were not to blame on the New Architecture itself. It just revealed cracks in some component designs that were previously invisible.
Stability Challenges: Session stability (crash-free sessions) dropped from our 99.95% target initially, recovering after a couple of weeks of bug fixing. We still see low occurrence crashes deep into RN or 3rd party library code, but nothing too impactful. We're working with Meta and library maintainers to find solutions.
ANR Spikes: Application Not Responding crashes increased on both platforms, some from our custom Reanimated/React Native patches that fixed other performance issues. Some deadlocks were also introduced due to some native modules being initialized on the main thread. This didn't result in a problem on the old architecture, but in some rare cases it crashed the app on the new.
Given this was a fundamental change to our most important framework, it could have been much worse. We remain optimistic about React Native's future and believe the new architecture provides a better foundation for addressing these performance concerns.
Recommendations for Other Teams
The React Native new architecture migration is challenging but achievable with proper planning.
These recommendations are based on our experience migrating a large-scale production app serving millions of users. Depending on your project's size and complexity, you may need to adjust these approaches, but they provide a solid foundation for planning your migration.
- Audit dependencies early - Identify compatibility issues before starting migration.
- Upgrade to the latest RN version before touching any code - Avoid wasting time on issues that were already fixed upstream. Release the upgrade before starting the migration to minimize the blast radius of changes.
- Get the ball rolling - Focus on enabling Fabric in development as soon as possible. That exposure will give your team valuable signals on what areas of your app need attention. Maintain compatibility with the old architecture.
- Leverage the community - Many issues you'll encounter have already been discussed in GitHub issues on the React Native repository or in other areas of the community. Search existing issues and discussions before trying to solve problems from scratch.
- Minimize changes initially - Prioritize bug fixes, and perform targeted optimizations before releasing.
- Strategic native modules - Only migrate modules with clear benefits to start.
- Plan for stability and performance degradation with phased rollouts - Accept temporary stability reduction and leverage app store gradual rollout features for risk mitigation.
- Fix forward when possible - Avoid rollbacks to maintain momentum.
What's Next
With the new architecture foundation in place, our focus shifts from compatibility to optimization. We can now leverage capabilities that were impossible before, addressing our current performance challenges while building better user experiences.
Our next priorities include:
- Strategic TurboModule migration: Convert high-frequency modules (user preferences, feature flags) and performance-critical paths to eliminate serialization costs
- Synchronous layout adoption: Build better components leveraging synchronous layout to eliminate visual jumps and improve performance on complex layouts
- Performance optimization: Use lazy TurboModule loading and optimized rendering to address our TTI and app launch time challenges
Finally, we’d like to give special thanks to Meta and Software Mansion for being amazing partners, and helping us on every step of the way. We also want to thank the React Native community. This work wouldn't have been possible without the experiences shared on GitHub discussions, blog posts and open source projects.
The new architecture represents React Native's future. While migration requires significant effort, the long-term benefits make it worthwhile. We're excited about what comes next. Between synchronous layouts eliminating UI jank and TurboModules providing blazing-fast native interop, we can continue to craft the delightful and fluid experiences our merchants expect. The future of React Native looks brighter than ever, and Shopify is deeply invested in its continued success.
If you’re interested in joining us on our mission to make commerce better for everyone, check out our careers page.
About the Author
Thiago Magalhaes is a Staff Engineer at Shopify.
X: @tmgsca33