<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Sunny Luo</title>
    <description>The latest articles on DEV Community by Sunny Luo (@sunnylqm).</description>
    <link>https://dev.to/sunnylqm</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1285489%2F94c16043-86b9-4ffd-91bb-d46df578e2ae.jpeg</url>
      <title>DEV Community: Sunny Luo</title>
      <link>https://dev.to/sunnylqm</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sunnylqm"/>
    <language>en</language>
    <item>
      <title>Migrating from CodePush to react-native-update: a step-by-step guide</title>
      <dc:creator>Sunny Luo</dc:creator>
      <pubDate>Thu, 22 Oct 2026 08:51:01 +0000</pubDate>
      <link>https://dev.to/sunnylqm/migrating-from-codepush-to-react-native-update-a-step-by-step-guide-3967</link>
      <guid>https://dev.to/sunnylqm/migrating-from-codepush-to-react-native-update-a-step-by-step-guide-3967</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I maintain &lt;a href="https://github.com/reactnativecn/react-native-update" rel="noopener noreferrer"&gt;react-native-update&lt;/a&gt;, an open-source OTA SDK for React Native, and the hosted service behind it, &lt;a href="https://cresc.dev" rel="noopener noreferrer"&gt;Cresc&lt;/a&gt;. This guide is written for teams that have already decided to leave CodePush. If you're still deciding, I list the other options honestly at the end.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;App Center was retired on March 31, 2025, and the &lt;code&gt;react-native-code-push&lt;/code&gt; repository is now archived. If your app still calls &lt;code&gt;codePush.sync()&lt;/code&gt;, those checks have nowhere to go: the app keeps running whatever bundle it has and stops getting updates.&lt;/p&gt;

&lt;p&gt;This guide moves a React Native app from CodePush to react-native-update. Your release workflow stays the same: ship JS and asset changes without a store review, roll out gradually, and roll back if something breaks. On a typical RN 0.7x+ app the code changes take an afternoon.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. How the concepts map
&lt;/h2&gt;

&lt;p&gt;Most of the work is translating concepts, so here's the cheat sheet first:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;CodePush&lt;/th&gt;
&lt;th&gt;react-native-update&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;App Center app (&lt;code&gt;owner/MyApp-iOS&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;App per platform (&lt;code&gt;cresc createApp --platform ios&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;Same idea: one app record per platform&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment key in &lt;code&gt;Info.plist&lt;/code&gt; / &lt;code&gt;strings.xml&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;appKey&lt;/code&gt; in &lt;code&gt;update.json&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;update.json&lt;/code&gt; is generated by the CLI; commit it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Staging&lt;/code&gt; deployment&lt;/td&gt;
&lt;td&gt;Test via QR code / deep link before binding&lt;/td&gt;
&lt;td&gt;Scan a QR in the dashboard to load an unreleased version on a test device&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;Production&lt;/code&gt; deployment&lt;/td&gt;
&lt;td&gt;Binding a version to native package(s)&lt;/td&gt;
&lt;td&gt;Users get an update only once it's bound to their native version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--target-binary-version "~1.1.0"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--packageVersionRange "~1.1.0"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Both semver ranges&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;--rollout 25&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--rollout 25&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Same name. Bumping to 100 promotes to full&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;appcenter codepush release-react&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;cresc bundle --platform ios&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Builds, publishes and (optionally) binds in one step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codePush(options)(App)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;&amp;lt;UpdateProvider client={...}&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;checkFrequency: ON_APP_START / ON_APP_RESUME / MANUAL&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;checkStrategy: "onAppStart" / "onAppResume" / null&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;"both"&lt;/code&gt; (the default) does both&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;installMode: ON_NEXT_RESTART&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;updateStrategy: "silentAndLater"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;installMode: IMMEDIATE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;updateStrategy: "silentAndNow"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;updateDialog: true&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;updateStrategy: "alwaysAlert"&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;codePush.notifyAppReady()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;markSuccess()&lt;/code&gt; (automatic by default)&lt;/td&gt;
&lt;td&gt;Unmarked updates roll back on next launch&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;appcenter codepush rollback&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Pause, then re-bind the previous version&lt;/td&gt;
&lt;td&gt;Dashboard or &lt;code&gt;cresc update&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The biggest difference: there are &lt;strong&gt;no deployments&lt;/strong&gt;. A published version does nothing until you bind it to one or more native versions, optionally at a rollout percentage. You do your testing before binding, using the QR code.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Swap the packages
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm uninstall react-native-code-push
npm i react-native-update
npm i &lt;span class="nt"&gt;-g&lt;/span&gt; react-native-update-cli
&lt;span class="nb"&gt;cd &lt;/span&gt;ios &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; pod &lt;span class="nb"&gt;install&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On RN 0.60+ autolinking handles the native module. Then delete the CodePush config:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ios/&amp;lt;App&amp;gt;/Info.plist&lt;/code&gt;: remove &lt;code&gt;CodePushDeploymentKey&lt;/code&gt; (and &lt;code&gt;CodePushServerURL&lt;/code&gt; if you self-hosted)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;android/app/src/main/res/values/strings.xml&lt;/code&gt;: remove &lt;code&gt;CodePushDeploymentKey&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;android/app/build.gradle&lt;/code&gt; / &lt;code&gt;settings.gradle&lt;/code&gt;: remove any manual &lt;code&gt;codepush.gradle&lt;/code&gt; / &lt;code&gt;:react-native-code-push&lt;/code&gt; lines if you added them for an old RN version&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  3. Point the native bundle URL at the new SDK
&lt;/h2&gt;

&lt;p&gt;This is the same hook CodePush used, with a different provider.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iOS, Objective-C&lt;/strong&gt; (&lt;code&gt;AppDelegate.mm&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- #import &amp;lt;CodePush/CodePush.h&amp;gt;
&lt;/span&gt;&lt;span class="gi"&gt;+ #import "RCTPushy.h"
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  - (NSURL *)bundleURL
  {
  #if DEBUG
    return [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index"];
  #else
&lt;span class="gd"&gt;-   return [CodePush bundleURL];
&lt;/span&gt;&lt;span class="gi"&gt;+   return [RCTPushy bundleURL];
&lt;/span&gt;  #endif
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On RN &amp;lt; 0.74 the method is &lt;code&gt;sourceURLForBridge:&lt;/code&gt; instead of &lt;code&gt;bundleURL&lt;/code&gt;; the change is the same.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;RN 0.88+&lt;/strong&gt;: the iOS template moved to the UIScene life cycle, so &lt;code&gt;bundleURL()&lt;/code&gt; now lives in &lt;code&gt;SceneDelegate.swift&lt;/code&gt; instead of &lt;code&gt;AppDelegate&lt;/code&gt;. Make the change below there. If you upgraded to 0.88 with the Upgrade Helper and moved the entry point, carry &lt;code&gt;RCTPushy.bundleURL()&lt;/code&gt; over too, or Release builds keep loading the embedded bundle and updates never apply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;iOS, Swift&lt;/strong&gt; (&lt;code&gt;AppDelegate.swift&lt;/code&gt;, or &lt;code&gt;SceneDelegate.swift&lt;/code&gt; on RN 0.88+):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gi"&gt;+ import react_native_update
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  override func bundleURL() -&amp;gt; URL? {
  #if DEBUG
    RCTBundleURLProvider.sharedSettings().jsBundleURL(forBundleRoot: "index")
  #else
&lt;span class="gd"&gt;-   CodePush.bundleURL()
&lt;/span&gt;&lt;span class="gi"&gt;+   RCTPushy.bundleURL()
&lt;/span&gt;  #endif
  }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Android, Kotlin, RN ‚â§ 0.81&lt;/strong&gt; (&lt;code&gt;MainApplication.kt&lt;/code&gt;):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;- import com.microsoft.codepush.react.CodePush
&lt;/span&gt;&lt;span class="gi"&gt;+ import cn.reactnative.modules.update.UpdateContext
&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;  override val reactNativeHost: ReactNativeHost =
      object : DefaultReactNativeHost(this) {
&lt;span class="gd"&gt;-       override fun getJSBundleFile(): String = CodePush.getJSBundleFile()
&lt;/span&gt;&lt;span class="gi"&gt;+       override fun getJSBundleFile(): String? =
+           UpdateContext.getBundleUrl(this@MainApplication)
&lt;/span&gt;        // ...
      }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Android, Kotlin, RN 0.82+&lt;/strong&gt; uses &lt;code&gt;ReactHost&lt;/code&gt;, so pass the path as a parameter:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight kotlin"&gt;&lt;code&gt;&lt;span class="k"&gt;override&lt;/span&gt; &lt;span class="kd"&gt;val&lt;/span&gt; &lt;span class="py"&gt;reactHost&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nc"&gt;ReactHost&lt;/span&gt; &lt;span class="k"&gt;by&lt;/span&gt; &lt;span class="nf"&gt;lazy&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nf"&gt;getDefaultReactHost&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="n"&gt;applicationContext&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;packageList&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;PackageList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;packages&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;jsBundleFilePath&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UpdateContext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getBundleUrl&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two Android details that bite people:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;If you use &lt;code&gt;react-native-screens&lt;/code&gt; (react-navigation does), register &lt;code&gt;RNScreensFragmentFactory&lt;/code&gt; in &lt;code&gt;MainActivity.onCreate&lt;/code&gt;. Without it you can get a blank screen after an OTA restart.&lt;/li&gt;
&lt;li&gt;Set &lt;code&gt;crunchPngs false&lt;/code&gt; in the release build type. PNG crunching makes binary diffs unpredictable and the patches bigger.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Expo&lt;/strong&gt; (SDK 50+, after &lt;code&gt;npx expo prebuild&lt;/code&gt;): with Expo 48+ and react-native-update ‚â• 10.28.2 the bundle URL is configured automatically, so you can skip this section. Don't keep &lt;code&gt;expo-updates&lt;/code&gt; installed alongside it, because the two conflict.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Replace the JS wrapper
&lt;/h2&gt;

&lt;p&gt;Create the app records once. This writes &lt;code&gt;update.json&lt;/code&gt;, which holds the &lt;code&gt;appKey&lt;/code&gt; for each platform:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cresc login
cresc createApp &lt;span class="nt"&gt;--platform&lt;/span&gt; ios
cresc createApp &lt;span class="nt"&gt;--platform&lt;/span&gt; android
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then swap the wrapper. A typical CodePush setup:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;codePush&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-native-code-push&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;codePushOptions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;checkFrequency&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;codePush&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CheckFrequency&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ON_APP_RESUME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;installMode&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;codePush&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;InstallMode&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ON_NEXT_RESTART&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="nf"&gt;codePush&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;codePushOptions&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Platform&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-native&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;Cresc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;UpdateProvider&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-native-update&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;updateConfig&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;./update.json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Cresc&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;appKey&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;updateConfig&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;Platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;OS&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;appKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;checkStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;onAppResume&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;updateStrategy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;silentAndLater&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="k"&gt;default&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Root&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;UpdateProvider&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;App&lt;/span&gt; &lt;span class="o"&gt;/&amp;gt;&lt;/span&gt;
    &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/UpdateProvider&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you had a custom update dialog built on &lt;code&gt;codePush.sync()&lt;/code&gt; callbacks, set &lt;code&gt;updateStrategy: null&lt;/code&gt; and drive your UI from the &lt;code&gt;useUpdate()&lt;/code&gt; hook:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useUpdate&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;react-native-update&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;UpdateBanner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;updateInfo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;downloadUpdate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;switchVersion&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;switchVersionLater&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;progress&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="nf"&gt;useUpdate&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;updateInfo&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;update&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// updateInfo.name / description / metaInfo come from the published version.&lt;/span&gt;
  &lt;span class="c1"&gt;// `if (await downloadUpdate()) await switchVersion()`: it resolves to false&lt;/span&gt;
  &lt;span class="c1"&gt;// when the download didn't complete, so check it before offering a restart.&lt;/span&gt;
  &lt;span class="c1"&gt;// progress.received / progress.total drive a progress bar.&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Mandatory updates (CodePush's &lt;code&gt;-m&lt;/code&gt;) don't have a dedicated flag. Either use &lt;code&gt;updateStrategy: "silentAndNow"&lt;/code&gt;, or publish with &lt;code&gt;--metaInfo '{"mandatory":true}'&lt;/code&gt; and branch on &lt;code&gt;JSON.parse(updateInfo.metaInfo)&lt;/code&gt; in your own UI (&lt;code&gt;metaInfo&lt;/code&gt; arrives as a string). There's a &lt;a href="https://cresc.dev/docs/bestpractice#using-meta-info" rel="noopener noreferrer"&gt;worked example in the docs&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;This covers the common options. For the rest (&lt;code&gt;ON_NEXT_RESUME&lt;/code&gt;, &lt;code&gt;disallowRestart&lt;/code&gt;, &lt;code&gt;getUpdateMetadata&lt;/code&gt;, &lt;code&gt;clearUpdates&lt;/code&gt;, &lt;code&gt;restartApp&lt;/code&gt; and the App Center CLI commands), see the &lt;a href="https://cresc.dev/codepush-api-migration" rel="noopener noreferrer"&gt;full CodePush API mapping&lt;/a&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Rollback safety: what replaces &lt;code&gt;notifyAppReady()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;CodePush rolled back an update if &lt;code&gt;notifyAppReady()&lt;/code&gt; wasn't called after it launched. react-native-update works the same way through &lt;code&gt;markSuccess()&lt;/code&gt;, but &lt;strong&gt;the provider calls it for you&lt;/strong&gt; 1 second after mount (&lt;code&gt;autoMarkSuccess: true&lt;/code&gt; by default). If the new bundle crashes before that, the next launch rolls back to the previous version and reports a &lt;code&gt;rollback&lt;/code&gt; event.&lt;/p&gt;

&lt;p&gt;If your app does critical work after the first frame (navigation restore, remote config), mark success later instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Cresc&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="nx"&gt;appKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;autoMarkSuccessDelayMs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="c1"&gt;// or return false to skip marking this launch:&lt;/span&gt;
  &lt;span class="na"&gt;healthCheck&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;remoteConfigLoaded&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  5. Upload the native baseline
&lt;/h2&gt;

&lt;p&gt;This step has no CodePush equivalent, and it's what makes the patches small. Updates are shipped as &lt;strong&gt;binary diffs against the exact build users have installed&lt;/strong&gt;, so upload the same artifact you send to the stores:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cresc uploadIpa build/MyApp.ipa
cresc uploadApk android/app/build/outputs/apk/release/app-release.apk
&lt;span class="c"&gt;# or, if you distribute an AAB:&lt;/span&gt;
cresc uploadAab android/app/build/outputs/bundle/release/app-release.aab
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The native version (&lt;code&gt;CFBundleShortVersionString&lt;/code&gt; / &lt;code&gt;versionName&lt;/code&gt;) becomes the package version that your &lt;code&gt;--packageVersionRange&lt;/code&gt; targets. &lt;strong&gt;Bump it on every native rebuild.&lt;/strong&gt; Two different builds with the same version number still work, but you lose diff reuse and the patches get bigger.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Publish from CI
&lt;/h2&gt;

&lt;p&gt;Create an API key in the dashboard (Write permission is enough), store it as &lt;code&gt;CRESC_API_TOKEN&lt;/code&gt;, and run the CLI. It detects CI's missing TTY and runs non-interactively; &lt;code&gt;--no-interactive&lt;/code&gt; makes that explicit. Passing a native range and a rollout publishes and binds in one step:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="c1"&gt;# .github/workflows/ota.yml&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;OTA release&lt;/span&gt;
&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;rollout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Rollout&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;percentage"&lt;/span&gt;
        &lt;span class="na"&gt;default&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;10"&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;platform&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;ios&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;android&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v6&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;22&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm ci &amp;amp;&amp;amp; npm i -g react-native-update-cli&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Bundle, publish and bind&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;CRESC_API_TOKEN&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.CRESC_API_TOKEN }}&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;cresc bundle --platform ${{ matrix.platform }} \&lt;/span&gt;
            &lt;span class="s"&gt;--name "${GITHUB_SHA::7}" \&lt;/span&gt;
            &lt;span class="s"&gt;--description "OTA release ${GITHUB_SHA::7}" \&lt;/span&gt;
            &lt;span class="s"&gt;--packageVersionRange "&amp;gt;=2.0.0 &amp;lt;3.0.0" \&lt;/span&gt;
            &lt;span class="s"&gt;--rollout ${{ inputs.rollout }} \&lt;/span&gt;
            &lt;span class="s"&gt;--no-interactive&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The CodePush equivalent was roughly &lt;code&gt;appcenter codepush release-react -a owner/MyApp-iOS -d Production -t "&amp;gt;=2.0.0 &amp;lt;3.0.0" --rollout 10&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Two behaviors you get without extra configuration:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;On Hermes apps, the CLI compiles each release against an earlier bytecode of the same app and applies a Hermes-aware transform before diffing. In &lt;a href="https://github.com/sunnylqm/hbc-diff-benchmark" rel="noopener noreferrer"&gt;our benchmark&lt;/a&gt; a one-line fix ships as a 3.4 KB patch, where a plain bsdiff of the same change is 93.7 KB.&lt;/li&gt;
&lt;li&gt;The source map is archived with each version, so JS crashes from that release show up symbolicated in the dashboard.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  7. Test, roll out, roll back
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Before binding&lt;/strong&gt;, enable "Use Deep Link" in the dashboard for your app's URL scheme and scan the version's QR code on a test device. That loads the exact unreleased bundle, which replaces what you used &lt;code&gt;Staging&lt;/code&gt; for. (The Android activity needs &lt;code&gt;launchMode="singleTask"&lt;/code&gt; and an intent filter for the scheme.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bind at a low percentage.&lt;/strong&gt; Buckets are a stable hash of the device ID, so the same users stay in the canary as you widen it. Each native version can have one canary and one full release at the same time.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the version's health&lt;/strong&gt; in the dashboard: download failures, patch failures, rollbacks and symbolicated JS errors.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Promote&lt;/strong&gt; by raising the rollout to 100, which replaces the previous full release.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If it's bad&lt;/strong&gt;, pause the native package's updates and re-bind the previous version, or revert in git and publish a new version. Devices that crashed on launch have already rolled themselves back.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;If JS is dead on some devices&lt;/strong&gt; (white screen, crash before the update check runs), publish the fix with &lt;a href="https://cresc.dev/docs/publish#force-boot-brick-rescue" rel="noopener noreferrer"&gt;Force Boot&lt;/a&gt;. Since 10.52.1 the SDK runs a native cold-start check that doesn't depend on JS, so those devices can still pull the fix.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  8. The transition period
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Users on old binaries keep their last CodePush bundle.&lt;/strong&gt; Nothing breaks, but they won't get updates until they install the new store build. Prioritize getting the first react-native-update build out.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You can't send an OTA to CodePush users.&lt;/strong&gt; The migration needs one native release. Plan it like any store submission.&lt;/li&gt;
&lt;li&gt;If you self-hosted a CodePush server, you can keep it running for old binaries while new binaries use react-native-update. The two don't interact.&lt;/li&gt;
&lt;li&gt;Once nobody is on the old binaries, delete the App Center tokens from CI and remove the CodePush server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Other options, honestly
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Self-host CodePush.&lt;/strong&gt; Before App Center retired, Microsoft open-sourced a &lt;a href="https://github.com/microsoft/code-push-server" rel="noopener noreferrer"&gt;standalone CodePush server&lt;/a&gt;. If you'd rather run your own infrastructure and keep the existing SDK, this is the smallest code change. Note that the server and the client SDK are both archived now, so you take on hosting and maintaining both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expo Updates (EAS Update).&lt;/strong&gt; A great fit if you already use EAS for builds. It ships full bundles instead of diffs and bills by update MAU and bandwidth, which matters at scale.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;react-native-update / Cresc.&lt;/strong&gt; Hermes-aware binary diffs, staged rollouts, automatic rollback and built-in crash symbolication. The SDK, CLI and admin dashboard are open source and there's a free tier.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Questions about a specific setup (brownfield, monorepo, custom bundle names) are welcome in the comments.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>codepush</category>
      <category>mobile</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Why Hermes bytecode defeats binary diffing — and how we got a one-line OTA hotfix down to 3.4 KB</title>
      <dc:creator>Sunny Luo</dc:creator>
      <pubDate>Thu, 08 Oct 2026 10:55:23 +0000</pubDate>
      <link>https://dev.to/sunnylqm/why-hermes-bytecode-defeats-binary-diffing-and-how-we-got-a-one-line-ota-hotfix-down-to-34-kb-2cn2</link>
      <guid>https://dev.to/sunnylqm/why-hermes-bytecode-defeats-binary-diffing-and-how-we-got-a-one-line-ota-hotfix-down-to-34-kb-2cn2</guid>
      <description>&lt;p&gt;&lt;em&gt;Disclosure: I maintain &lt;a href="https://github.com/reactnativecn/react-native-update" rel="noopener noreferrer"&gt;react-native-update&lt;/a&gt;, the open-source SDK behind Cresc, an OTA update service for React Native. The benchmark code and data are public, and the technique applies to anyone shipping Hermes bundles.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you ship React Native over-the-air updates, you've probably assumed binary diffing solves the bandwidth problem: compute a bsdiff between the old and new bundle, send the patch, done. With plain JS bundles that's mostly true. &lt;strong&gt;With Hermes bytecode it isn't&lt;/strong&gt;, and the reason is interesting.&lt;/p&gt;

&lt;p&gt;We measured on real release bundles of a React Native 0.86 app (Hermes HBC v98, with common dependencies like Paper, SVG, Sentry and CameraKit; ~1.5 MB gzipped):&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Release scenario&lt;/th&gt;
&lt;th&gt;Full bundle (gzip)&lt;/th&gt;
&lt;th&gt;Classic bsdiff&lt;/th&gt;
&lt;th&gt;Hermes-aware diff&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;One-line text change&lt;/td&gt;
&lt;td&gt;1,544.6 KB&lt;/td&gt;
&lt;td&gt;93.7 KB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;3.4 KB&lt;/strong&gt; (28× smaller)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Small feature (+1 component, ~60 LOC)&lt;/td&gt;
&lt;td&gt;1,555.8 KB&lt;/td&gt;
&lt;td&gt;411.6 KB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;50.2 KB&lt;/strong&gt; (8.2× smaller)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Medium feature (+2 screens, ~300 LOC)&lt;/td&gt;
&lt;td&gt;1,604.7 KB&lt;/td&gt;
&lt;td&gt;551.6 KB&lt;/td&gt;
&lt;td&gt;
&lt;strong&gt;97.8 KB&lt;/strong&gt; (5.6× smaller)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A one-line change producing a &lt;strong&gt;94 KB&lt;/strong&gt; bsdiff patch is the giveaway: something is amplifying tiny source edits into large binary changes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 1: the string table is re-sorted on every compile
&lt;/h2&gt;

&lt;p&gt;Hermes stores strings in a table and refers to them by ID throughout the bytecode. On every compile it sorts that table by identifier frequency, so a one-line change perturbs the frequencies and renumbers dozens or hundreds of string IDs. Every instruction that references a shifted ID changes too.&lt;/p&gt;

&lt;p&gt;Result: two builds that differ by one line of JS end up differing in bytes all over the file. A generic diff algorithm sees thousands of scattered edits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; Hermes has a delta mode (&lt;code&gt;hermesc -base-bytecode=&amp;lt;base&amp;gt;&lt;/code&gt;). Compiling every release against the root bytecode of its version chain pins existing string IDs in place, so only code that actually changed shows up in the diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  A free 21%: move debug info out
&lt;/h2&gt;

&lt;p&gt;Hermes embeds debug info in the bytecode by default. Compiling with &lt;code&gt;-output-source-map&lt;/code&gt; moves it into an external &lt;code&gt;.map&lt;/code&gt;, which shrinks the bytecode by about 21% before any diffing happens. You keep the map for symbolicating crashes; it just doesn't ship in every patch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Problem 2: offset tables amplify every insertion
&lt;/h2&gt;

&lt;p&gt;Hermes bytecode is full of tables that store absolute offsets: function headers, string storage, and so on. Insert a few bytes near the start and every offset after that point changes. Again, a tiny edit becomes a wide diff.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fix:&lt;/strong&gt; before diffing, apply a reversible transform that delta-encodes the offset fields (wrapping subtraction), so a global shift collapses into a single-point change. The client applies the patch and then runs the inverse transform to get the exact target bytecode. On top of the first two layers, this cuts patches by a further 9–36%.&lt;/p&gt;

&lt;p&gt;The three layers are independent and their gains stack. They matter most for small changes, which is exactly the hotfix case OTA exists for.&lt;/p&gt;

&lt;h2&gt;
  
  
  Making it safe
&lt;/h2&gt;

&lt;p&gt;Clever transforms are only worth it if they never brick an app:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Bit-exact round trip:&lt;/strong&gt; every patch is checked so that &lt;code&gt;inverseTransform(patch(transform(old))) == new&lt;/code&gt;, byte for byte.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Instruction-level equivalence:&lt;/strong&gt; delta-mode builds are disassembled and compared with plain builds instruction by instruction. On any mismatch the delta build is discarded and the plain build ships. (Benchmarking this turned up a verifier bug that wrongly flagged &lt;code&gt;UIntSwitchImm&lt;/code&gt; jump tables and silently threw away valid delta builds. That's fixed now.)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No Hermes-specific client code:&lt;/strong&gt; the table layout descriptors ship inside the patch metadata, so the client's patcher is version-agnostic, and supporting a new Hermes version is a CLI-side config change.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Reproduce it
&lt;/h2&gt;

&lt;p&gt;The benchmark harness and raw data are here: &lt;a href="https://github.com/sunnylqm/hbc-diff-benchmark" rel="noopener noreferrer"&gt;https://github.com/sunnylqm/hbc-diff-benchmark&lt;/a&gt;. I'd love to see numbers from other apps, especially large ones. If your results differ, open an issue.&lt;/p&gt;

&lt;h2&gt;
  
  
  Using it
&lt;/h2&gt;

&lt;p&gt;The diffing is built into &lt;a href="https://github.com/reactnativecn/react-native-update" rel="noopener noreferrer"&gt;react-native-update&lt;/a&gt; (open source, around since 2016) and the hosted service &lt;a href="https://cresc.dev" rel="noopener noreferrer"&gt;Cresc&lt;/a&gt;. There's a free tier, and teams moving off CodePush can use the &lt;a href="https://cresc.dev/codepush-migration-checklist" rel="noopener noreferrer"&gt;migration checklist&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Questions about Hermes internals or OTA pipelines are welcome in the comments.&lt;/p&gt;

</description>
      <category>reactnative</category>
      <category>hermes</category>
      <category>mobile</category>
      <category>javascript</category>
    </item>
  </channel>
</rss>
