By default, the release command publishes a full archive containing the entire update. Diff updates let clients download only the differences needed for an update.
This feature is optional. Without any additional configuration, release publishes only the full archive. If a diff update is unavailable or cannot be applied, the app falls back to the full archive and installs the update unless the failure is a network connection error.
| Archive | Compared against | Contents | Available when |
|---|---|---|---|
| full | Nothing | Entire update | Always |
| binary patch | JS bundle embedded in the app binary | JS bundle diff and all assets | The client is running the target binary version |
| asset diff | Previous OTA update | JS bundle diff, new assets, and a list of assets to delete | The client is running the base OTA update |
A binary patch is generated against the JS bundle embedded in the app binary published to the store.
Every build for the same release target (binary version) must contain the same embedded bundle. Once you archive that embedded bundle, every update targeting the same binary version can provide a binary patch.
When you pass --binary-bundle-path to release, the CLI uses the archived embedded bundle as the base for hermesc -base-bytecode. This aligns the internal layout of the new bytecode with the base bundle, reducing byte movements unrelated to source changes so that the binary patch can focus on the actual changes.
Measurements from real-world apps showed the following results:
| Conditions | Result |
|---|---|
| An approximately 25MB Hermes bundle with 153 files changed over five days | The aligned HDiffPatch was 355–382KB. Without alignment, patches from every codec tested were 3–5 times larger. (PR #150) |
| An approximately 18MB Android bundle with 23 lines of source changes | Even with -base-bytecode, mismatched --minify settings between the binary and update made the patch about 12.5 times as large. (PR #165) |
As the second result shows, passing -base-bytecode alone is not sufficient. The update must also be generated with --minify false to match the embedded binary bundle, and the CLI applies this option by default. Actual results vary by app and change set.
An asset diff is generated against a previously published OTA update. Therefore, only an app running that base OTA update can use it.
Unlike a binary patch, which contains every asset, an asset diff contains only assets absent from the base update. The smaller the asset difference between the base update and the new release, the smaller the asset diff will be.
Publishing diff updates requires the following:
-
Export embedded bundles for binary patches
For every binary version you support, archive the exact JS bundle embedded in the binary published to the store.
-
Configure previous archive downloads for asset diffs
To publish asset diffs as well, implement
bundleDownloaderincode-push.config.ts.
You must also build the patch generator once before generating patches. See Prerequisites: building the patch generator for details.
A binary patch is the difference between a new update and the JS bundle already embedded in the app. You must therefore preserve the exact JS bundle bytecode embedded in the build published to the store.
The library provides an export hook for each platform. After the bundle build finishes, the hook exports:
- The Hermes-compiled JS bundle (bytecode)
- A
binary-patch-base.jsonfile containing bundle metadata and verification values
Apply the Gradle script in your app module's android/app/build.gradle:
apply plugin: "com.android.application"
apply plugin: "com.facebook.react"
react {
// ...
}
apply from: "../../node_modules/@bravemobile/react-native-code-push/android/codepush-export.gradle"The apply from line can go anywhere in the file.
After the JS bundle is built, it is exported to:
android/app/build/codepush/embedded-bundle/<variant>/
To use a different export path, choose one of the following:
- Pass
-PcodePushExportDir=<path>to Gradle - Set
ext.codePushExportDir
If you set ext.codePushExportDir, place it before the apply from line. In either case, the <variant> directory is appended to the path.
In Xcode, open the app target's Build Phases tab and find the Bundle React Native code and images phase. Add the final line below to the end of the existing script:
/bin/sh -c "\"$WITH_ENVIRONMENT\" \"$REACT_NATIVE_XCODE\""
# Add this line
"$SRCROOT/../node_modules/@bravemobile/react-native-code-push/scripts/export-embedded-bundle.sh"The default export path is:
$BUILD_DIR/codepush/embedded-bundle/$CONFIGURATION-$PLATFORM_NAME/
To use a different export path, set the CODEPUSH_EXPORT_DIR environment variable. The $CONFIGURATION-$PLATFORM_NAME directory is still appended to the path.
Note: The Expo config plugin (
app.plugin.js) does not currently apply this hook automatically.
The CI pipeline that builds your store binary must archive the exported JS bundle by binary version.
When publishing a later OTA release, download the JS bundle for the target binary version and use it as the binary patch base bundle.
The following examples are only a reference. Archive the bundles in whatever way fits your deployment pipeline.
Upload the export after ./gradlew :app:assembleRelease finishes:
aws s3 cp --recursive \
android/app/build/codepush/embedded-bundle/release \
"s3://your-bucket/binaries/android/$BINARY_VERSION/"Because $BUILD_DIR exists only during the build, set the export directory to a path known to your CI pipeline:
export CODEPUSH_EXPORT_DIR="$PWD/codepush-export"
xcodebuild \
-workspace ios/YourApp.xcworkspace \
-scheme YourApp \
-configuration Release \
archive # ...
aws s3 cp --recursive \
"$CODEPUSH_EXPORT_DIR/Release-iphoneos" \
"s3://your-bucket/binaries/ios/$BINARY_VERSION/"Download the JS bundle for the target binary version from your archive and pass its path to --binary-bundle-path:
aws s3 cp --recursive \
"s3://your-bucket/binaries/android/1.0.0/" \
./binary/
npx code-push release \
-b 1.0.0 \
-v 1.0.1 \
-p android \
--binary-bundle-path ./binary/index.android.bundleThe release command reads the binary-patch-base.json file archived with the JS bundle and verifies that the supplied base bundle is correct. See Verifying the base bundle for details.
When publishing a binary patch, you can also publish asset diff archives against previous OTA updates. An asset diff archive is generated for each of the N most recent updates. It is not published if its size is greater than or equal to the binary patch archive.
An asset diff contains only:
- The JS bundle binary patch
- New asset files absent from the base update
- A manifest listing asset files to delete
After downloading the update, the app copies the base OTA update, patches the JS bundle, and deletes unnecessary asset files. The resulting contents are identical to the full archive update.
An asset diff is published only when all of the following conditions are met:
- The release is a binary patch release created with
release --binary-bundle-path. bundleDownloaderis implemented incode-push.config.ts.--diff-base-countis greater than0. The default is3.
The CLI uses bundleDownloader to download previous updates that serve as asset diff bases.
bundleDownloader: async (archive, platform, identifier = 'staging') => {
const downloadedFilePath = path.join(os.tmpdir(), archive.packageHash);
const storageKey =
`bundles/${platform}/${identifier}/full-bundle/${archive.packageHash}`;
// Download the archive at storageKey from S3, Supabase, or another
// storage provider to downloadedFilePath.
// Return the downloadedFilePath.
return { downloadedFilePath };
},See Asset diff archives for details about which asset diff archives are generated and published.
When downloading an update, the app tries the smallest archive first.
| Order | Archive | Selected when |
|---|---|---|
| 1 | asset diff | A diff exists against the OTA update currently running |
| 2 | binary patch | The asset diff is unavailable or applying the asset differences fails |
| 3 | full | The binary patch is unavailable or applying the patch fails |
The app starts with the binary patch when no asset diff is available, including when:
- The app is running the bundle embedded in the app binary (the first OTA update)
- No asset diff was generated against the current OTA update
The fallback path depends on the failure:
| Failure | Next action | Reason |
|---|---|---|
asset_merge_failed |
Try the binary patch | The asset differences could not be applied to the installed update. Replacing all assets may still succeed |
package_verification_failed |
Try the binary patch | The final hash of the contents merged from the asset diff does not match. Replacing all assets may still succeed |
The asset diff download URL returns HTTP 400 or higher |
Try the binary patch | The asset diff and binary patch use separate download URLs, so failure to download one does not mean the other is unavailable |
| Applying the bundle patch fails | Download the full archive | The asset diff and binary patch apply the same JS bundle patch, so falling back to the binary patch is likely to fail as well |
| Network connection error | Report the error without falling back | The next option uses the same network, and the full archive is larger, so another attempt would likely fail more slowly |
Connection errors and server response errors are handled differently. For example, a 404 response from the asset diff download URL means the server was reached, so there is no reason to skip the next option—the binary patch or full archive.
The onUpdateArchiveResult callback reports the result of every update the app tried. To collect which archive was selected and why fallback occurred, see Telemetry callbacks.