Shopify — Migrating to React Native's New Architecture¶
Summary¶
Shopify migrated its two largest mobile apps — Shopify Mobile and Shopify Point of Sale (POS) — to React Native's New Architecture (Fabric renderer + TurboModules + synchronous layout) while maintaining weekly releases to millions of merchants. The core migration strategy was minimize code changes first, refactor later; maintain dual-architecture compatibility throughout development (both old and new architectures buildable from the same codebase); and upgrade React Native to the latest version before starting the migration to ride upstream bug fixes. They generated dual-architecture builds on every PR via TopHat, used feature flags to fork module implementations where third-party libraries didn't yet support both architectures, and phased the production rollout across Android (fine-grained percentage control) and iOS (pre-determined schedule) with explicit stability-threshold response tiers. Immediate wins from Fabric: app launch time improved ~10% on Android, ~3% on iOS; batched state updates reduced unnecessary re-renders. Costs: crash-free session rate temporarily dropped from the 99.95% target and recovered over a couple of weeks of bug-fixing; load times on some complex components regressed up to 20% due to rendering-path assumption breakage; ANR (Application Not Responding) spikes from legacy native modules loading on the main thread.
Key takeaways¶
- "Minimize code changes first, refactor later" was the forcing function. Making only the minimum changes needed to enable the new architecture — and saving TurboModule conversions / component refactors for after shipping — was what stopped new code from being written that would break the migration build. A slow migration accumulates new incompatible code faster than it can be fixed. (Source: sources/2025-09-12-shopify-migrating-to-react-natives-new-architecture)
- Dual-architecture compatibility during development is non-negotiable for weekly-release apps. Shopify shipped weekly to merchants throughout the migration. This required the app to build under both the old and new architectures on every PR — enforced via TopHat-driven dual builds — so that new feature code written by hundreds of engineers couldn't silently break the new-architecture build. Without dual-arch CI, the main branch would diverge and block feature work. (See concepts/dual-architecture-compatibility, patterns/dual-architecture-ci-builds.)
- Upgrade to the latest React Native version before starting the migration. "The majority of bugs we encountered were fixed with version upgrades." The new architecture is under heavy upstream development; starting on an old RN version means debugging issues that are already fixed upstream. Release the upgrade before the migration to minimize the blast radius of concurrent change. (See systems/react-native.)
- Don't migrate native modules to TurboModules during the architecture migration itself. Shopify has 40+ native modules. They explicitly deferred TurboModule conversion: only modules that wouldn't work on the new architecture (mostly those touching UIManager) were touched, and those were fixed with feature-flagged forks — one implementation for each architecture — rather than full TurboModule conversion. TurboModule is a forward step but not mandatory for enabling the new architecture; the two concerns are orthogonal and can be sequenced.
- The new architecture's synchronous rendering model exposes latent
bugs the old architecture tolerated. Five named failure modes:
(1) state batching — components that relied on intermediate
state values between
setStatecalls break when updates are batched; (2) "blank screen of doom" — TurboModules doing UIManager manipulation fail silently, requiring comment-out-one-component-at-a-time bisection; (3) shadow-tree manipulation from native UIManagers causes view-hierarchy desync, breaking tap gestures on WebView swaps — solution was to move WebView lifecycle entirely to native code (Mobile Bridge's TransportableView); (4) view flattening (concepts/view-flattening) optimizes out<View ref={ref}>instances when the ref is needed (breaksActionSheetIOSand end-to-end test selectors); solved by a Babel plugin that auto-injectscollapsable={false}on any view with atestID; (5) legacy native modules initialized on the main thread deadlock with animations during app launch — setrequiresMainQueueSetup: falseunless strictly necessary. - Production rollout schedule was asymmetric by platform because of store policy. Play Store supports fine-grained % and full halt; App Store has a pre-determined schedule and can be paused but not reduced (users can still update manually), and approval for new submissions takes up to 24 hours. So Shopify ran Android ahead of iOS by one day to get early signal on the platform they could fully roll back: Day 1 — Android 8%, iOS 0%; Day 2 — Android 30%, iOS 1%; Day 3 — both 100%. iOS went to 100% directly because intermediate percentages weren't meaningfully controllable. (See concepts/platform-asymmetric-rollout-control, patterns/phased-mobile-rollout-with-stability-tiers.)
- Pre-commit to stability thresholds and response tiers. Shopify's baseline is 99.95% crash-free sessions. Before rollout they defined: *above 99.80% → fix forward on next weekly release; 99.00–99.80% or broken critical flow with known fix → pause rollout
- hotfix; below 99.00% or broken critical flow without quick fix → rollback. Rollback was treated as a last resort* — not for stability reasons but because losing at-scale production signal would set the migration back significantly. A remote feature flag couldn't gate this change (it's a native binary property), so rollback means shipping a new binary.
- Measuring in-house does not surface everything production surfaces. "We measured performance extensively and made optimizations before rollout" but still saw load-time regressions up to 20% on some complex components in production, ANR spikes on both platforms, and low-occurrence crashes in RN / 3rd-party library code that only appeared at scale. Production scale is an irreplaceable signal; this is why the team preferred pause + hotfix over rollback.
- Library maintainers should ship versions that support both architectures simultaneously. Shopify calls out dual-arch support as "significantly eases migration burden" for consumers — and did it themselves for FlashList v2 alpha. When third-party deps only support one architecture per version, consumers are forced into conditional-version management, which is non-trivial.
- Large-scale production adoption surfaces upstream bugs that maintainers can't see. Shopify's Reanimated usage for navigation animations produced severe frame-rate drops unique to their scale of animation complexity. Both Meta and Software Mansion (Reanimated's maintainers) provided early-access patches; fixes are being upstreamed into Reanimated and React Native. See one Shopify-originated RN PR. (See patterns/upstream-fixes-to-community.)
- Some wins were automatic from Fabric alone — no TurboModule conversion required. App launch time improved ~10% on Android, ~3% on iOS immediately on switching to Fabric. Measure-before-paint simplified screen rendering (see FlashList v2). Batched state reduced unnecessary re-renders, making tab switches snappier.
Operational numbers¶
- Fleet scale: Shopify Mobile + Shopify POS — "hundreds of screens", "40+ native modules" in the Shopify app.
- Release cadence: weekly (maintained throughout migration).
- Stability target: 99.95% crash-free sessions.
- Response tiers: 99.80% / 99.00% / below-99.00% → fix-forward / pause-hotfix / rollback.
- Rollout schedule: Day 1 Android 8% / iOS 0%; Day 2 Android 30% / iOS 1%; Day 3 both 100%.
- Platform-level Fabric wins (no TurboModule conversion): app launch time ~10% better on Android, ~3% on iOS.
- Production cost: up to 20% slower load times on some complex components post-rollout (revealed latent component-design issues); ANR spikes from main-thread native-module init; crash-free sessions dropped from 99.95% target initially, recovered over a couple of weeks of bug-fixing.
- Upstream contribution: one public Shopify RN fix is facebook/react-native PR #49849.
Caveats / not disclosed¶
- No absolute app-launch-time numbers — only 10% / 3% deltas.
- No per-release stability-recovery timeline (e.g., weekly crash-free progression).
- No disclosure of the Babel plugin's implementation — they state
it auto-injects
collapsable={false}on components with atestID, but the plugin is not open-sourced in the post. - No disclosure of the feature-flag system used to fork module implementations — presumably Shopify's internal feature-flag platform.
- No ANR spike magnitude — described only as "increased on both platforms".
- No disclosure of TopHat internals — dual-arch builds on every PR are called out but the build-farm architecture isn't covered (TopHat is covered separately in Shopify TopHat 2021 post).
- No quantitative disclosure of how many components had to be refactored for state-batching bugs or how many had view-flattening issues.
- Forward-looking items (strategic TurboModule migration, synchronous layout adoption, lazy TurboModule loading for TTI) are described as future work, not yet measured.
Source¶
- Original: https://shopify.engineering/react-native-new-architecture
- Raw markdown:
raw/shopify/2025-09-12-migrating-to-react-natives-new-architecture-fd5976f5.md
Related¶
- systems/react-native — Meta's cross-platform mobile framework; the target of the migration.
- systems/flashlist — Shopify's high-performance list component; v2 was rebuilt from scratch on the new architecture as the canonical dual-architecture library example.
- systems/reanimated — Software Mansion's animation library; hit frame-rate issues on Shopify's scale that triggered upstream fixes.
- systems/tophat-shopify — Shopify's internal mobile-build-and-test platform; the mechanism for dual-arch builds on every PR.
- concepts/dual-architecture-compatibility — the forcing-function discipline that keeps both architectures buildable during migration.
- concepts/view-flattening — the new architecture's rendering optimization that removes "unnecessary" views; root cause of the ref-is-null / testID-not-findable failure mode.
- concepts/state-batching — the synchronous-rendering property that collapses intermediate state values.
- concepts/legacy-native-module-main-queue-deadlock —
requiresMainQueueSetup: true - animations-during-launch = deadlock; the ANR root cause on iOS.
- concepts/platform-asymmetric-rollout-control — Play Store fine-grained % + halt vs App Store pre-determined schedule; forces the Android-first rollout schedule.
- concepts/stability-threshold-rollout-tiers — tiered response playbook with explicit crash-free-session thresholds mapped to fix-forward / pause-hotfix / rollback actions.
- patterns/dual-architecture-ci-builds — the CI shape that enforces dual-architecture compatibility every PR.
- patterns/feature-flagged-dual-implementation — how Shopify forked native-module implementations where third-party libs didn't support both architectures.
- patterns/phased-mobile-rollout-with-stability-tiers — canonical form of the rollout playbook.
- patterns/native-side-lifecycle-for-hybrid-component — Mobile Bridge TransportableView fix: move hybrid React-Native/native components entirely to the native side to avoid shadow-tree manipulation.
- patterns/babel-plugin-automatic-collapsable-false — compile-time auto-fix for the view-flattening-breaks-testID problem.
- patterns/upstream-fixes-to-community — Shopify ↔ Meta ↔ Software Mansion collaboration pattern; discover bugs at scale, send fixes upstream.
- companies/shopify — company page.