RORK LABEN
COMPARE — Expo が8月5日に、実際に Expo アプリを作らせる形で AI モデル3種を比較した記事を公開しました。コードと UI の品質では Fable 5 が最良という結果ですSDK — Expo SDK 57 は React Native 0.86 への追随を主眼とした小規模なリリースです。アップグレードを一大プロジェクトにしない方針が示されていますPREBUILD — expo prebuild が既定でネイティブの android / ios ディレクトリをクリアして再生成するようになりました。手を入れているプロジェクトは影響を受けますOTA — Hermes のバイトコード差分により、OTA 更新で JS バンドル全体ではなく差分だけを配信できます。規模が出るほど帯域と費用に効きますSIMULATOR — Rork はクラウド上の iOS シミュレータをブラウザへストリーミングします。Xcode も Mac 実機もなしに Apple 環境での確認ができますHARDWARE — Rork は HealthKit・ARKit / LiDAR・NFC・Dynamic Island・Metal 3D まで届き、ノーコードの通常の範囲を超えた機能に対応しますCOMPARE — Expo が8月5日に、実際に Expo アプリを作らせる形で AI モデル3種を比較した記事を公開しました。コードと UI の品質では Fable 5 が最良という結果ですSDK — Expo SDK 57 は React Native 0.86 への追随を主眼とした小規模なリリースです。アップグレードを一大プロジェクトにしない方針が示されていますPREBUILD — expo prebuild が既定でネイティブの android / ios ディレクトリをクリアして再生成するようになりました。手を入れているプロジェクトは影響を受けますOTA — Hermes のバイトコード差分により、OTA 更新で JS バンドル全体ではなく差分だけを配信できます。規模が出るほど帯域と費用に効きますSIMULATOR — Rork はクラウド上の iOS シミュレータをブラウザへストリーミングします。Xcode も Mac 実機もなしに Apple 環境での確認ができますHARDWARE — Rork は HealthKit・ARKit / LiDAR・NFC・Dynamic Island・Metal 3D まで届き、ノーコードの通常の範囲を超えた機能に対応します
記事一覧/開発ツール
開発ツール/2026-08-09上級

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

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

Hermes7EAS Update7OTA7Expo161React Native222

プレミアム記事

文言を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-06-24
EAS Update を入れたのに『直したはずのバグ』が消えないとき — OTA が静かに届かない原因と運用メモ
EAS Update を導入しても OTA が一部のユーザーに届かないことがあります。runtimeVersion のドリフト、配信したのに採用されない更新、安全なロールバックの選び方を、Rork アプリの運用で実際に効いた計装とともに整理します。
開発ツール2026-05-30
OTAアップデートを段階配信する仕組みを、壊さずに育てる
EAS Updateで全ユーザーに即時配信するのは危険です。チャンネル設計・段階的ロールアウト・自動ロールバックまで、累計5,000万DLの運用で固まった配信アーキテクチャを実装コード付きで共有します。
開発ツール2026-06-28
EAS Update を少人数から配り、クラッシュ率で自動的に止める
OTA 更新は一瞬で全員に届くからこそ、悪い更新も一瞬で全員に届きます。EAS Update を少人数(カナリア)から配り、クラッシュ率を見て広げるか止めるかを自動で判断し、最後は端末側で安全網を張る設計を、動くコードでまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →