◉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、残り28日。API 連携を足したアプリは移行先の確認を●SDK 58 ISSUE — Android ウィジェットが定期更新されない、Stripe が静的リンクになるなど初期報告が出ています●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、残り28日。API 連携を足したアプリは移行先の確認を●SDK 58 ISSUE — Android ウィジェットが定期更新されない、Stripe が静的リンクになるなど初期報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/アプリ開発
◇ アプリ開発/2026-08-03上級

1.10.0 の端末が強制アップデートに落ちる — 最小サポートバージョン判定を4実装で実測する

リモート設定で最小サポートバージョンを引き上げるゲートを、バージョン比較の4実装で実測しました。localeCompare が同値と言い切る箇所、fail-open の境界、審査中バージョンを閾値にした事故までを記録しています。

Expo234React Native242リモート設定3バージョン管理アプリ運用7

✦ プレミアム記事

強制アップデートの画面が出たまま先へ進めない、という問い合わせが届いたのは、バージョン 1.10.0 を配信した数日後でした。

こちらの管理画面では最小サポートバージョンを 1.9.0 に設定しています。1.10.0 は当然それより新しいはずです。落ちる理由がありません。

手元の端末で再現しませんでした。1.10.0 を入れた自分の iPhone は素通りします。再現したのは、ゲートの判定関数を切り出して Node で走らせたときでした。"1.10.0" < "1.9.0" が true を返します。

文字列として比べれば "1" の次の文字が "1" と "9" の勝負になり、"1" < "9" で 1.10.0 の負けです。当たり前の話なのですが、9 系のマイナーバージョンを踏むまでの1年半、この条件は一度も偽になりませんでした。バグは静かに待っていました。

個人開発でアプリを6本運用していると、この手の「メジャーバージョンが二桁に届いた瞬間に初めて出る欠陥」を何度か踏みます。今回は判定の実装そのものを実測で選び直したので、その過程を残しておきます。

バージョン文字列を比べる方法は4通りあり、3つは壊れる

まず素朴に思いつく実装を並べます。文字列比較、localeCompare の numeric オプション、parseFloat、セグメントごとの数値比較の4つです。

const sgn = (n) => (n < 0 ? -1 : n > 0 ? 1 : 0);
 
// ① 文字列としてそのまま比較
const naive = (a, b) => sgn(a < b ? -1 : a > b ? 1 : 0);
 
// ② localeCompare の numeric オプション
const loc = (a, b) => sgn(a.localeCompare(b, undefined, { numeric: true }));
 
// ③ 小数として読む
const flt = (a, b) => sgn(parseFloat(a) - parseFloat(b));
 
// ④ ドット区切りを数値セグメントとして比較
function segs(a, b) {
  const A = a.split("."), B = b.split(".");
  const n = Math.max(A.length, B.length);
  for (let i = 0; i < n; i++) {
    const x = parseInt(A[i] ?? "0", 10) || 0;
    const y = parseInt(B[i] ?? "0", 10) || 0;
    if (x !== y) return sgn(x - y);
  }
  return 0;
}

これを、実運用で実際に現れるバージョン文字列の組み合わせに当てました。Node v22.22.3 での結果です。-1 は左が小さい、1 は左が大きい、0 は同値を意味します。

比較ペア① 文字列② localeCompare③ parseFloat④ セグメント
1.9.0 vs 1.10.01-11-1
1.0 vs 1.0.0-1-100
1.02.0 vs 1.2.0-10-10
2.0.0 vs 2.0.0.1-1-10-1
1.4.0 vs 1.41100
1.4.0-beta.1 vs 1.4.01101
10.0.0 vs 9.99.99-1111
1.2.10 vs 1.2.9-1101
3.0.0 vs 3.01100
1.10 vs 1.9-11-11

太字が期待と食い違った箇所です。①は12ペア中6つ、③は5つを取り違えます。

私が事故を起こしたのは①でした。10.0.0 vs 9.99.99 も同じ形で壊れます。メジャーが二桁へ届いた瞬間、そのアプリの全ユーザーが一斉に「古い」と判定されます。

③の parseFloat は 1.2.10 を 1.2 として読むため、パッチバージョンが丸ごと消えます。1.2.10 vs 1.2.9 が同値になる理由がこれです。二番目のドット以降が存在しないものとして扱われます。

localeCompare が「同じです」と言い切る二つの場所

②の localeCompare(b, undefined, { numeric: true }) は、①と③が壊れた数値まわりを全て正しく処理します。1.9.0 vs 1.10.0 も 10.0.0 vs 9.99.99 も期待どおりです。ここまで見ると、これで十分に思えます。

私も一度これで書き直しかけました。実際に表へ落としてから気づいた箇所が二つあります。

一つ目。1.02.0 と 1.2.0 を 0(同値) と判定します。numeric コレーションは数字の並びを数値として読むため、先頭のゼロが無視されます。ビルド番号を桁揃えして 1.02.0 のように振る運用をしていると、二つの別バージョンが区別できません。

二つ目のほうが実害があります。1.4.0 と 1.4 を比べると 1(左が大きい) を返します。セグメント数の違いが、そのまま大小の差として扱われます。

Android の versionName に 1.4 のような二桁表記を使っている場合、リモート設定の閾値を 1.4.0 と書いた瞬間、1.4 の端末は「閾値より古い」と判定されます。同じバージョンを指しているのに、表記のゆれだけで強制アップデート対象へ落ちます。

これは私の予想と逆でした。localeCompare を「数値として賢く比べてくれるもの」と捉えていたのですが、実際には文字列の照合順序に数値の読み方を足しただけで、バージョン番号のセマンティクスは知りません。セグメントが欠けたら短いほうが小さい、というのは辞書順の規則であってバージョン番号の規則ではありません。

④のセグメント比較はこの二つを正しく処理します。ただし 1.4.0-beta.1 vs 1.4.0 で 1 を返します。parseInt("0-beta") が 0 を返し、そのあと4番目のセグメントに 1 が残るため、ベータ版がリリース版より新しいと判定されます。プレリリースを配る運用があるなら、④も素のままでは使えません。

✦

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

この記事の続きを読む

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

この記事で得られること
✦バージョン比較の4実装を12ペアで実測した対照表。localeCompare が「1.02.0 と 1.2.0 は同値」「1.4.0 は 1.4 より大きい」と判定する箇所まで含めて掲載
✦16ケースを通した比較関数の完全なコードと、20万回で286.4ms という測定値
✦リモート設定の取得が遅れたときに fail-open へ倒す実装と、1,500ms のバジェットで切り替わる境界の実測
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

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

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

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

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

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

関連記事

◇ アプリ開発2026-08-25
iOS 27 が正式版になる前に、予備の iPhone で公開中の Rork アプリを触っておく
iOS 27 の正式版が配信されたいま、予備の iPhone で公開中の Rork アプリを触る手順と、見る順番、JS 側で潰せる箇所、次のベータに備えた予備機の持ち方をまとめました。
◇ アプリ開発2026-10-15
adjustsFontSizeToFit で特定の1行だけ極小になるとき、疑うのは文字列ではなく枠の高さです
一覧の10行目だけ文字が4ptまで縮む。データも文字列も正常なのに起きる iOS の症状を、Yoga の丸めと縮小判定のソースで確かめ、Node で再現率を数えた記録です。
◇ アプリ開発2026-08-16
グラフが壊れて見えるのは、データが多いときではなく最初の数日でした
記録アプリの折れ線グラフが、データの少ない最初の数日だけ線が消える。原因はY軸の幅が0になり座標がNaNになることでした。実測した挙動と、描画前に置く小さな正規化層を紹介します。