Anthony Wentzel

Hash the native layer: skipping React Native builds before Expo Fingerprint

Most React Native changes are JavaScript only. In 2022 we fingerprinted native inputs so CI could skip native builds, the idea behind Expo Fingerprint.

From 2022 · 2 min read · Published September 9, 2026

Here's a pattern that saved us a lot of build time in 2022, on a React Native app with a fair amount of custom native code. It is the same idea Expo Fingerprint later made standard.

The observation: most commits to a React Native app only change JavaScript. The native binary, everything Xcode and Gradle produce, is identical to the last build. Yet a typical pipeline recompiles all of it anyway, because it has no way to know nothing changed.

Fingerprint the native inputs

The fix is to hash every file that can affect the native build and compare it to the hash from the last successful build. If it matches, the native layer hasn't changed.

The inputs are more specific than "the ios folder." For iOS we hashed a fixed list of files plus every native source by extension:

config/ios-hash.js
const makeiOSStaticPaths = ({ platformRoot, appName }) =>
  [
    'Podfile.lock',
    `${appName}/SplashScreen.storyboard`,
    `${appName}/Images.xcassets`,
    `${appName}.xcworkspace`,
    `${appName}.xcodeproj`,
  ].map((f) => `${platformRoot}/${f}`)
 
const makeiOSGlobPaths = ({ platformRoot, appName }) => ({
  root: `${platformRoot}/${appName}`,
  exts: ['*.h', '*.m', '*.swift', '*.xcconfig', '*.plist', '*.entitlements', '*.storyboard', '*.lock'],
})

Podfile.lock is the one people forget. Upgrading a JavaScript package that ships native code changes no files in your ios folder, but it does change the lockfile. Leave it out, and a native dependency upgrade gets treated as a JavaScript change.

We also hashed the .env file, since build-time config can end up baked into the binary.

Then inject, don't rebuild

When the fingerprint matched, the pipeline skipped compilation. It took the previously built app, bundled the new JavaScript, and injected it into the existing binary before re-signing. When the fingerprint differed, it did a full native build and stored the new hash.

The tooling came from an excellent write-up by the author of uglydirtylittlestrawberry.co.uk, whose companion scripts we adapted for our monorepo. Credit to them for the approach.

Why it's worth understanding now

Expo has since made this idea a first-class tool with @expo/fingerprint, and EAS can use it as a runtime version policy to decide whether an update is compatible with an installed build. If you're on Expo, use that.

But the mental model is worth keeping either way:

  • A React Native app is two artifacts, a native binary and a JavaScript bundle, that change at very different rates.
  • The native one is expensive and changes rarely. The JavaScript one is cheap and changes constantly.
  • Anything that lets your pipeline treat them separately, whether faster CI, over-the-air updates, or safer rollbacks, starts with knowing which one actually changed.

Related: React Native CI/CD that only builds the platform that changed · Upgrade Expo SDK safely: reproduce it in a sandbox first