Documentation
Everything to ship over-the-air updates with Pulsar — from install to multi-deployment testing.
Hermes bytecode
Hermes is the JavaScript engine React Native uses by default. Your store build doesn’t carry your JavaScript as text: the build compiles it ahead of time to Hermes bytecode, which the engine loads and runs directly. From CLI 1.6, pulsar release does the same for over-the-air updates.
Before CLI 1.6, every OTA was plain JavaScript. Once one was installed, the app parsed and compiled the whole bundle, often several megabytes, on every cold start. On low-end Android phones the app was slow to open, and some phones stopped it before it finished loading. With a bytecode update, the app starts about as fast as the store build does.
Bytecode only runs on a Hermes engine with the same bytecode version, and React Native upgrades change it: React Native 0.82 and 0.83 use v96, 0.84 to 0.86 use v98, and a later upgrade can change it again. Bytecode sent to a build with another version would crash the app at launch. So every bytecode release carries a JavaScript copy of the same release, with the same assets, and SDK 1.9 tells Pulsar which bytecode version each build runs when it checks for updates.
- A build that reports the same bytecode version gets the bytecode.
- Every other build gets the JavaScript copy: builds that don’t report a version (an SDK older than 1.9, or no Hermes) and builds on another React Native version.
- If the JavaScript copy is missing, for example because the upload was cut off, the release reaches only builds that report its version. Every other build stays on its current release.
Pulsar doesn’t send bytecode to a build that hasn’t reported a matching version. It looks inside every upload it can read: bytecode uploaded without its version, for example by another tool, is labelled and only reaches builds that report that version. It can’t look inside an encrypted upload, so for private bundles it relies on the CLI, which reads the version before it encrypts.
Only builds on SDK 1.9 or later report a bytecode version, so your first bytecode releases go mostly to the JavaScript copy. SDK 1.9 sends the report from JavaScript and has no native changes over 1.8, so a store build on SDK 1.8 starts reporting once it installs an OTA built with SDK 1.9, and gets bytecode from the next release on.
Before uploading, the CLI prints how many active devices on your --target report the matching version and get the bytecode; the rest get the JavaScript copy. The Store builds panel shows how many devices on each store build report each bytecode version.
- Hermes is on unless your project turns it off:
hermesEnabled=falseinandroid/gradle.properties,enableHermes: falseinandroid/app/build.gradleon React Native 0.70 and older, or:hermes_enabled => falseinios/Podfile. --no-hermesreleases JavaScript only, as before CLI 1.6.--hermescompiles even when your project says Hermes is off.- The compiler comes from your app’s
node_modules(hermes-compiler/hermesc,react-native/sdks/hermescon React Native 0.82 and older, orhermes-engineon 0.68 and older), so the bytecode matches your React Native version. If it’s missing, the CLI stops and suggests--no-hermes. It never ships JavaScript in its place. - The CLI checks that your Pulsar server can serve bytecode releases this way, and stops before uploading if it can’t.
pulsar release MyApp Production --platform android --target 1.0.0
# JavaScript only
pulsar release MyApp Production --platform android --target 1.0.0 --no-hermes- The CLI runs Metro twice: once minified for the JavaScript copy, the same kind of bundle earlier releases shipped, and once unminified for the bytecode, the way React Native’s own release build compiles it. Both builds carry the same assets.
- When the app has a signing key, both builds are signed. Each build gets its own source map, so on apps without private bundles crash stacks map to your files whichever one a device runs.
- For private bundles, both builds are encrypted, and
--plain-copy-for-old-buildsuploads the JavaScript build as the plain copy. - Diffs are built for the bytecode, including from the previous release’s JavaScript copy. Builds that get the JavaScript copy download it whole.
--max-growthand the size warning compare bytecode with the last bytecode release and the JavaScript copy with the last JavaScript.- The dashboard tags these releases bytecode v96 + JS copy, and
pulsar historytags them[bytecode v96 + JS copy].pulsar promoteandpulsar rollbackkeep both builds, and warn when a release has no JavaScript copy. - Pulsar won’t roll back to a bytecode release that has no JavaScript copy, since every build that doesn’t run its bytecode would stay on the release you’re rolling back from. Release the fix again instead.
- On a deployment that needs approval, the JavaScript copy has to be uploaded before the release is approved, as the CLI does. Once a release is approved or rejected, a copy can’t be added, because the approval didn’t cover it.
pulsar migratedoesn’t copy a release that is already Hermes bytecode, since it has no JavaScript copy for other builds. Release it again from source.
- Downloads are bigger. Bytecode compresses less well than minified JavaScript. On one app we measured, the full download was about 1.9 times larger.
- It doesn’t hide your code. Every bytecode release also ships its JavaScript copy, and any client can get that copy by not reporting a bytecode version, so your code is as easy to read as it was before. The benefit is the faster cold start. Bytecode on its own makes code harder to read, not unreadable: Hermes decompilers exist, and strings such as URLs and messages stay visible. Keep secrets on your server.
- It is not a way to hide anything from app review. Your store build already ships bytecode, and every OTA must still follow the store rules. Apple and Google Play allow downloaded JavaScript that doesn’t change the app’s purpose, so keep what you ship over the air within what was reviewed.