◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り23日。API 連携を足したアプリは移行先の確認を●SDK 58 — React Native 0.88 に更新されました。Android のリリース版で R8 が既定オンになり、iOS は SceneDelegate を生成します●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SONNET 4.5 — Claude API での提供終了は 11/30、残り23日。API 連携を足したアプリは移行先の確認を●SDK 58 — React Native 0.88 に更新されました。Android のリリース版で R8 が既定オンになり、iOS は SceneDelegate を生成します●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-08-09上級

Hermes バイトコードの差分更新を実測する — モジュール1本の追加が更新サイズを29倍にした

2.4MB の Hermes バイトコードで OTA 差分を実測しました。1行の変更は 2,104 バイト、モジュールを1本足すと 61,197 バイト。よく言われるモジュールID安定化はほぼ効かず、効いたのは別の2つでした。

Hermes10EAS Update8OTA7Expo234React Native243

✦ プレミアム記事

文言を1つ直しただけの OTA 更新で、配信サイズが妙に大きい月がありました。

個人開発でアプリを何本か抱えていると、EAS の帯域は地味に効いてきます。文字列を1つ変えただけの更新が、数十KB を持っていく。感覚としては「そんなに変わっていないはずなのに」という違和感でした。

私自身、この手の違和感を放置して後で困ったことが何度かあります。今回は先に潰しておくことにしました。

原因を推測で語るのは気持ちが悪かったので、手元で測ることにしました。バンドルを合成し、Hermes でバイトコードに落とし、変更の種類ごとに差分サイズを並べます。それだけの単純な測定台です。

結果は、私の予想を2つとも外しました。

何を測ろうとしたのか

OTA 更新の配信量を決めるのは、最終的には「前のバージョンと今のバージョンの差分が何バイトか」です。

JS バンドルをまるごと配るなら、gzip 後のサイズがそのまま配信量になります。一方、差分だけを配れるなら、変更の大きさに比例した量で済むはずです。Hermes のバイトコード差分が話題になるのも、この「比例するはず」という期待があるからです。

私が知りたかったのは、その比例がどこで壊れるかでした。

事前の予想はこうでした。モジュールIDが連番で振られている以上、モジュールを1本足せば以降のIDが全部ずれます。ずれたIDは依存配列に埋まっていますから、差分は全体へ広がって膨らむはずです。だから createModuleIdFactory でIDを安定させれば解決する——そう考えておりました。

この予想は、後で見るとおり外れます。

測定台を作る

実プロジェクトで比較すると、変更が混ざって切り分けができません。そこで、Metro の出力に似せた合成バンドルを生成しました。

480 モジュール、各モジュールに 34 個の小さな関数と 28 個の依存呼び出しを持たせ、__d(function(g,r,i,a,m,e,d){...}, ID, [DEPS]); の形で並べます。生成後のサイズは約 1.97MB、Hermes に通すと約 2.40MB のバイトコードになりました。手元のアプリの実バンドルと同じ桁です。

# gen.py — python3 gen.py で実行できます
# pip install bsdiff4 --break-system-packages が必要です
import random, hashlib, gzip, json, bsdiff4
 
random.seed(20260809)
N = 480
NAMES = [f"src/features/{chr(97 + i % 26)}{i}/module{i}" for i in range(N)]
 
def body(i, tweak=False):
    """Metro 出力に似た、それなりの大きさを持つモジュール本体を作ります。"""
    lines = ["'use strict';Object.defineProperty(e,'__esModule',{value:!0});"]
    for k in range(28):
        lines.append(f"var _v{k}=r(d[{k % 6}]).default;")
    # tweak=True のときだけ、この1行の文字列リテラルが変わります
    label = "saved to library" if tweak else "saved"
    lines.append(f"var LABEL_{i}='{label}';")
    for k in range(34):
        lines.append(
            f"function h{i}_{k}(a,b){{var s=a+{k}*{i};if(s>{k * 7 + 3})"
            f"{{return _v{k % 28}(s,'k{k}');}}return b?b(s):s;}}"
        )
    exports = ",".join(f"h{k}:h{i}_{k}" for k in range(34))
    lines.append(f"e.default={{LABEL:LABEL_{i},{exports}}};")
    return "".join(lines)
 
def build(ids, order, tweak_at=None, extra=None, extra_pos=None):
    """ids: モジュール index -> ID(数値でも文字列でも可)"""
    out = []
    for idx in order:
        deps = [ids[(idx + 1 + j) % N] for j in range(6)]
        out.append(
            f"__d(function(g,r,i,a,m,e,d){{{body(idx, tweak=(idx == tweak_at))}}},"
            f"{json.dumps(ids[idx])},{json.dumps(deps)});\n"
        )
    if extra is not None:
        out.insert(len(out) if extra_pos is None else extra_pos, extra)
    return ("var __d=function(){};\n" + "".join(out)).encode()
 
order = list(range(N))
num_ids = {i: i for i in order}
hash_ids = {i: hashlib.sha1(NAMES[i].encode()).hexdigest()[:8] for i in order}
 
NEW_BODY = 'function(g,r,i,a,m,e,d){"use strict";e.default=function(x){return x*2;};}'
new_num = f'__d({NEW_BODY},480,[]);\n'
new_hash = f'__d({NEW_BODY},{json.dumps(hashlib.sha1(b"src/features/new/doubler").hexdigest()[:8])},[]);\n'
 
variants = {
    # 基準(数値ID・ハッシュID)
    "v1":  build(num_ids, order),
    "v1c": build(hash_ids, order),
    # A: 1行の文字列変更だけ
    "vA":  build(num_ids, order, tweak_at=300),
    # B: モジュールIDを全件+1ずらすだけ(コードは無変更)
    "vB":  build({i: i + 1 for i in order}, order),
    # C: 末尾にモジュールを1本追加するだけ(数値ID)
    "vC":  build(num_ids, order, extra=new_num),
    # D: 同じ追加をハッシュIDで
    "vD":  build(hash_ids, order, extra=new_hash),
}
for name, data in variants.items():
    open(f"{name}.js", "wb").write(data)
    print(f"{name}.js  {len(data):,} B  gzip={len(gzip.compress(data, 9)):,} B")

バイトコード化には Hermes のコンパイラをそのまま使いました。hermes-engine-cli に Linux 向けの hermesc が同梱されているので、Mac がなくても測れます。

npm install hermes-engine-cli bsdiff4 2>/dev/null
H=./node_modules/hermes-engine-cli/linux64-bin/hermesc
chmod +x "$H"
for f in v1 v1c vA vB vC vD; do
  "$H" -emit-binary -O -out "$f.hbc" "$f.js"
done
ls -l *.hbc

差分器は2種類用意しました。バイト単位の減算を行う bsdiff 系(bsdiff4)と、LZ 系の zstd --patch-from です。1つの差分器だけで結論を出すと、その実装の癖を法則と取り違えます。

# measure.py
import bsdiff4, subprocess, os
 
def bs(a, b):
    return len(bsdiff4.diff(open(a, "rb").read(), open(b, "rb").read()))
 
def zs(a, b):
    subprocess.run(["zstd", "-19", "--long=27", f"--patch-from={a}", b,
                    "-o", "/tmp/p.zst", "-f", "-q"], check=True)
    return os.path.getsize("/tmp/p.zst")
 
cases = [
    ("1行の文字列変更のみ",        "v1", "vA"),
    ("モジュールIDを全件ずらすのみ", "v1", "vB"),
    ("モジュール1本追加(数値ID)",  "v1", "vC"),
    ("モジュール1本追加(ハッシュID)", "v1c", "vD"),
]
for label, a, b in cases:
    print(f"{label:28s} JS_bsdiff={bs(a+'.js', b+'.js'):>7,}  "
          f"HBC_bsdiff={bs(a+'.hbc', b+'.hbc'):>7,}  "
          f"HBC_zstd={zs(a+'.hbc', b+'.hbc'):>7,}")
✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦同一バンドルに対する4種類の変更で、bsdiff と zstd の差分サイズを並べた実測表
✦「モジュールIDを安定させる」定番の助言が差分サイズにほとんど効かなかった測定結果とその理由
✦バンドル分割で差分を 61,197 バイトから 1,247 バイトへ落とした手順と、そのまま実行できる測定スクリプト
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

⬡ 開発ツール2026-10-25
効果音が40回目で鳴らなくなる——expo-audio の createAudioPlayer は remove() だけでは片づきません
expo-audio で再生のたびに createAudioPlayer を作ると、Android では約40回で音が止まり、didJustFinish も届かなくなります。remove() と release() の役割の違い、作り方を変える三つの選択肢、止まったときの見張り方を整理します。
⬡ 開発ツール2026-10-13
リリース前に、書き出した Expo バンドルの中から自分の鍵を探してみる
ソースに鍵がなくても、出来上がったアプリの中に鍵が残ることがあります。Expo SDK 57 で実際に expo export を実行し、バンドルの中身を grep と 60 行ほどの Node スクリプトで調べて、見つかる鍵と見つからない鍵の違いを確かめました。
⬡ 開発ツール2026-09-10
画面を暗くする機能を足したら、アプリを閉じたあとも iPhone が暗いままでした
expo-brightness の setBrightnessAsync は、Android では現在のアクティビティにだけ効き、iOS では端末の明るさそのものを変えます。戻す責任が片側にしかない理由と、AppState で後始末をするフックの実装をまとめました。