An empirical benchmark of binary diff algorithms for React Native OTA updates, comparing:
- bsdiff 4.3 — the classic binary diff (bzip2-compressed), used by many OTA systems
- bsdiff + lzma — the identical bsdiff delta streams (control/diff/extra), recompressed with
xz -9einstead of bzip2, to separate the compressor effect from the algorithm effect - hdiff — HDiffPatch via node-hdiffpatch 2.2.1 (lzma2), the default diff engine of react-native-update
- hdiff + HBC transform — the same hdiff, applied after a Hermes-bytecode-aware delta-friendly reversible transform (the production path of
pushy diff --hbcTransformin react-native-update-cli)
Hermes bytecode (HBC) is full of tables carrying absolute offsets (functionHeaders, smallStringTable, overflowStringTable, …). A one-line JS change inserts a few bytes in the middle of the file — and every offset after the insertion point shifts by N. Exact-match diff engines then see thousands of "changed" table entries that are really the same entry plus a constant.
The transform delta-encodes those offset bitfields (wrapping subtraction, modulo field width), so a global shift collapses into a single-point change. It is:
- exactly reversible:
T⁻¹(T(x)) === xfor any input, verified on every run - layout-table driven: the layout description ships inside each patch (
__diff.json), so the client-side C interpreter has zero Hermes-version-specific code — new Hermes versions only need a new table entry on the CLI side - fail-safe: every section bound and bitfield range is validated before any byte is touched; on mismatch the pipeline falls back to plain hdiff
Fixtures are real bundles of react-native-update's example app (React Native 0.86.0, Hermes HBC v98, dependencies include react-native-paper, react-native-svg, @sentry/react-native, react-native-camera-kit), built with react-native bundle --dev false --minify true + hermesc -O -emit-binary:
| Scenario | Change against base |
|---|---|
| S1 | One-line text change (edit a string literal) |
| S2 | Small feature: +1 new component, ~60 LOC (react-native-paper Card/Chip/Divider) |
| S3 | Medium feature: +2 new screens, ~300 LOC (DataTable, SegmentedButtons, Searchbar, …) |
Base bundle: 3.24 MB JS / 4.42 MB HBC.
Measured on Apple M2, Node v24.11.1. Every patch is verified by an actual round-trip (bspatch / hdiffpatch.patch + inverse transform must reproduce the exact new bytes) before its size is reported.
| Scenario | Full (gzip) | bsdiff | bsdiff+lzma | hdiff | hdiff + HBC transform | vs bsdiff+lzma | vs hdiff |
|---|---|---|---|---|---|---|---|
| S1: one-line text change | 1901.5 KB | 93.7 KB | 87.7 KB | 89.0 KB | 63.5 KB | −28% | −29% |
| S2: small feature (~60 LOC) | 1913.9 KB | 411.6 KB | 330.9 KB | 328.0 KB | 285.5 KB | −14% | −13% |
| S3: medium feature (~300 LOC) | 1973.7 KB | 551.6 KB | 430.6 KB | 431.6 KB | 398.4 KB | −7% | −8% |
| Scenario | Full (gzip) | bsdiff | bsdiff+lzma | hdiff |
|---|---|---|---|---|
| S1: one-line text change | 807.2 KB | 0.3 KB | 0.7 KB | 0.1 KB |
| S2: small feature (~60 LOC) | 813.1 KB | 7.4 KB | 7.4 KB | 5.8 KB |
| S3: medium feature (~300 LOC) | 837.7 KB | 38.4 KB | 37.4 KB | 28.7 KB |
| Scenario | bsdiff | hdiff | hdiff + HBC transform |
|---|---|---|---|
| S1 | 774 ms | 254 ms | 221 ms |
| S2 | 1099 ms | 414 ms | 369 ms |
| S3 | 1212 ms | 436 ms | 418 ms |
The transform costs single-digit milliseconds; diffing the transformed pair is actually faster than diffing the raw bytes, because the inputs match better.
- Incremental updates beat full-bundle OTA by ~5–30× on bandwidth (e.g. 63.5 KB vs 1.9 MB for a one-line change).
- With the compressor normalized, bsdiff+lzma and hdiff land within ~1% of each other on raw HBC — the remaining 7–29% cut delivered by the HBC-aware transform is pure algorithmic gain, not a compressor artifact, with the biggest wins exactly where OTA updates are most frequent: small changes.
- hdiff generates patches 2–4× faster than bsdiff on these inputs; the transform itself costs single-digit milliseconds.
npm install --ignore-scripts # node-hdiffpatch ships prebuilds
brew install bsdiff # or any bsdiff 4.3
node bench.mjsFixtures are committed, so the benchmark itself needs no React Native toolchain. To regenerate fixtures from source, see fixtures/GENERATION.md.
bench.mjs— the benchmark: runs all four algorithm variants on every scenario, verifies every patch round-trip, prints the tables abovevendor/hbcTransform.mjs— the HBC transform, vendored (types stripped) from react-native-update-clisrc/utils/hbcTransform.ts(BSD-3-Clause)fixtures/— the real app bundles described aboveout/— generated patches andresults.json/results.md
本仓库对 React Native 热更新场景下的二进制 diff 算法做实证评测:bsdiff 4.3、bsdiff+lzma 对照组、hdiff(HDiffPatch/lzma2,react-native-update 的默认 diff 引擎)、以及 hdiff + Hermes 字节码(HBC)针对性可逆变换(pushy diff --hbcTransform 的生产路径)。
原理:HBC 中大量表结构携带绝对偏移,JS 一处小改动会让其后所有偏移整体位移,精确匹配型 diff 无法复用这些条目。对偏移位域做前项差分(wrapping)后,整体位移在差分域退化为单点变化,diff 显著变小。变换严格可逆、按布局描述表驱动(随补丁下发,客户端零版本分支)、变换前全量校验、失败自动回退。
测试数据:react-native-update 示例应用(RN 0.86.0,HBC v98)的真实产物,三个典型迭代场景:S1 单行文案修改、S2 小功能(+1 组件约 60 行)、S3 中等功能(+2 页面约 300 行)。另设 bsdiff+lzma 对照组(同一份 bsdiff 差分流改用 xz -9e 压缩),用于区分"压缩器差异"与"算法差异"。
结论(Apple M2 实测,所有补丁均经过往返校验):
- 增量更新比全量 OTA 节省 约 5~30 倍流量(单行改动 63.5 KB vs 全量 1.9 MB);
- 压缩器拉平后(bsdiff+lzma 与 hdiff 在原始 HBC 上差距不到 1%),HBC 针对性变换带来的 7%~29% 缩减是纯算法收益,越是高频的小改动收益越大;
- hdiff 生成补丁比 bsdiff 快 2~4 倍,变换本身开销仅个位数毫秒。
复现方式见上文 Reproduce。
MIT (vendored vendor/hbcTransform.mjs retains its original BSD-3-Clause license).