::Product·React Native·Development

Faster iOS Builds with Precompiled XCFrameworks

Starting with SDK 56, Expo packages on iOS ship as prebuilt XCFrameworks, so clean iOS builds skip compiling 70+ packages from source.

Christian Falch

Christian Falch

Engineering

Faster iOS Builds with Precompiled XCFrameworks

Expo SDK 56 introduces support for precompiled Expo modules on iOS using XCFrameworks. This significantly reduces native build overhead for many projects.

Precompiled XCFrameworks are enabled by default both locally and on EAS Build, with no configuration required.

Behind the scenes, this is also the beginning of a much larger transition in the Expo ecosystem: moving away from CocoaPods and toward Swift Package Manager (SwiftPM), Apple’s modern system for dependency management and native builds.

This post dives into:

  • Why we’re making this transition
  • How precompiled Expo modules work
  • The architectural challenges involved
  • And what this unlocks for the future of Expo on iOS

Faster builds without changing your app

Historically, every Expo and React Native library on iOS has been compiled from source inside your app project. That means every local build and every EAS Build recompiles all of these items from scratch:

  • React Native
  • Expo modules
  • 3rd party libraries with native code

Precompiled XCFrameworks in SDK 56 changes that.

Many Expo modules are now distributed as precompiled XCFrameworks through npm and can be linked directly instead of being rebuilt during every app compilation. In practice, this means that we’ll have fewer native compilation steps, faster local iteration, faster EAS Builds, and more reproducible native build environments.

The best part is that this happens automatically. No migration steps are required.

💡 What are XCFrameworks? An XCFramework is Apple’s format for distributing precompiled native libraries. Instead of shipping source code that every app compiles locally, a framework can be distributed as a binary artifact already compiled for: iOS devices, simulators, and multiple architectures. Each XCFramework contains independent framework “slices” for specific platforms and architectures. React Native already uses XCFrameworks internally, and Expo SDK 56 extends this model into the Expo ecosystem.

Why precompiled XCFrameworks matter

This work solves two major long-term problems in the React Native ecosystem.

1. CocoaPods is becoming legacy infrastructure

Expo, React Native, and most React Native libraries currently rely heavily on CocoaPods.

CocoaPods has historically handled dependency resolution, native autolinking, project integration, and build orchestration. But it’s an older Ruby-based system that will be deprecated eventually and will become read-only in December 2026.

Swift Package Manager has become Apple’s standard tooling for native dependencies, builds, package distribution and build extensibility.

React Native itself has already started moving toward SwiftPM support internally, including distributing parts of React Native as precompiled XCFrameworks.

Expo SDK 56 builds on that direction.

2. Native builds are expensive

As apps grow larger, native build times continue to increase.

This is especially noticeable in CI environments, in EAS Build, and in large monorepos with many native dependencies. Precompiled XCFrameworks let us move much of that work earlier in the pipeline. Frameworks are compiled once, packaged, and then reused across builds.

This dramatically reduces repeated native compilation work.

Why this is harder than it sounds

React Native and Expo were originally built around CocoaPods and source-based compilation.

That environment is extremely permissive: headers are globally available, source files can import almost anything, pod targets have loose isolation, and modular boundaries are blurry.

XCFrameworks impose much stricter rules.

Every framework must be: fully modular, self-contained, and isolated from implementation details outside its module boundary.

Many assumptions that work perfectly in CocoaPods simply break when building distributable XCFrameworks.

Rewriting the entire native architecture from scratch wasn’t realistic, so we focused on incremental compatibility layers and build infrastructure instead.

Build time impact

The following numbers were measured on an Apple M4 Max with 64 GB of memory, running clean iOS builds of a stock Expo app while enabling each layer of precompiled XCFrameworks in turn.

In SDK 55 we saw the improvements from using precompiled React Native binaries. This table shows the additional impact precompiled Expo Modules and 3rd party modules can have on a default Expo project:

Build configurationReduction vs previousReduction vs from-source
Everything from source
+ React Native core~44%~44%
+ Expo modules prebuilt~10%~50%
+ Third-party libraries prebuilt~30%~65%
The benefit for larger projects depends on coverage: React Native and Expo modules are always precompiled, but only the most widely used third-party libraries are. So a project relying on less common native dependencies will compile those from source and may see a smaller overall reduction.

Making Expo Modules Core compatible with Swift Package Manager

The first major step was adapting expo-modules-core.

Because nearly every Expo module depends on it, it sits at the root of the dependency graph. If it cannot build as a modular XCFramework, nothing else can.

This required solving several architectural issues.

Removing illegal header exports

Historically, some Expo Modules Core public headers exposed React Native headers directly:

#import <React/RCTView.h>

That works in CocoaPods because all headers are globally available during compilation.

Framework interfaces do not allow this.

💡A framework cannot publicly expose headers from another framework unless those dependencies are themselves modularized correctly.

Most of these issues were solved by refactoring public interfaces, isolating React Native internals, and restructuring APIs so non-modular dependencies do not leak into exported interfaces.

Breaking Swift ↔ Objective-C cycles

Swift Package Manager is significantly stricter about mixed-language targets than CocoaPods.

Expo Modules Core historically contained cyclic dependencies where: Objective-C referenced Swift types, while Swift referenced Objective-C types.

SwiftPM requires clear dependency direction between targets. To solve this, we introduced new interface abstractions, separated implementation layers, and refactored several internal APIs.

In a few cases, we also used Objective-C runtime reflection to dynamically invoke Swift implementations without introducing illegal compile-time dependencies.

Separating source trees for SwiftPM

Swift Package Manager is also strict about source ownership. The same source file cannot belong to multiple targets inside the same package graph.

Expo’s repository structure was never designed around that assumption. Instead of permanently reorganizing repositories, we generate temporary isolated source structures during builds using symlinks, generated folders, and build-time source separation.

This preserves the existing repository layout while still satisfying SwiftPM’s isolation requirements.

React Native and Virtual File System overlays

Another major challenge was React Native’s header structure.

React Native’s current XCFramework support still depends heavily on the legacy CocoaPods-generated header layout. That layout does not naturally exist in Swift Package Manager builds.

To bridge this gap, we worked on adding support for Clang Virtual File System (VFS) overlays inside React Native.

A VFS overlay lets the compiler “see” a virtual header layout different from the physical filesystem structure.

This allows us to preserve existing include paths, avoid massive source refactors, and present a modular structure to the compiler without physically reorganizing React Native’s source tree.

This is a common technique when modernizing large legacy native codebases into distributable modular frameworks.

Automatically generating Package.swift files

Swift Package Manager uses Package.swift manifests to define targets, dependencies, platforms, and build settings. Maintaining these manually across Expo packages would quickly become difficult and error-prone.

To solve this, we added new tooling to expo-tools that automatically generates Package.swift manifests, isolated source structures, dependency graphs, and XCFramework packaging steps.

This infrastructure is designed to run fully in CI and eventually power large-scale precompiled package distribution.

Supporting both CocoaPods and SwiftPM

This transition will take time.

The React Native ecosystem still depends heavily on CocoaPods, and many libraries are not yet compatible with Swift Package Manager. Because of that, SDK 56 focuses on coexistence rather than replacement.

Expo modules can currently switch between building from source or consuming precompiled XCFrameworks.

This lets existing apps continue working, CocoaPods remain supported, while the ecosystem gradually modernizes underneath.

If needed, precompiled modules can also be disabled:

EXPO_USE_PRECOMPILED_MODULES=0

Long-term, we expect more of Expo’s autolinking, native integration, and build tooling to move into Swift Package Manager itself.

Part of a broader modernization effort

Precompiled XCFrameworks are part of a broader modernization effort happening across Expo SDK 56 which also introduces inline native modules, continued React Native modernization work and infrastructure improvements for future native tooling.

Together, these changes move Expo closer to faster native builds, cleaner modular architecture, and deeper integration with Apple’s modern development ecosystem.

Looking ahead

Our current focus is stabilizing compatibility, expanding package coverage, validating build performance improvements, and continuing upstream collaboration with React Native.

This is one of the largest infrastructure migrations we’ve undertaken on the iOS side of Expo.

But it unlocks a much more scalable future for React Native development on Apple platforms faster builds, better tooling, cleaner native boundaries, and eventually a world without CocoaPods.

SDK 56 is the beginning of that transition.

SDK 56
Precompiled Expo Modules

Share article