RORK LABEN
SDK58 — Expo SDK 58 のベータが始まりました。React Native 0.88 の RC を同梱し、ベータ期間は3〜4週間と公式に書かれています11/01 — Google Play の対象 API レベル、延長を申請した場合の配信期限は11月1日です。残り44日ですEASENV — ローカルビルドに渡したはずの秘密が、中身ではなく変数名の文字列のまま届く、という報告が長く開いたままですNEW — 推奨された移行先が、すでに停止していました。廃止表を74行突き合わせた記録ですUISCENE — iOS 27 では新しい画面ライフサイクルが必須です。SDK 57 では自分で有効にする設定で、既定になるのは 58 からですCREDIT — 「AI のエラーには消費しない」がどこまでを指すのかは、同じ修正を何度か頼んだ日の記録を取ると見えてきますSDK58 — Expo SDK 58 のベータが始まりました。React Native 0.88 の RC を同梱し、ベータ期間は3〜4週間と公式に書かれています11/01 — Google Play の対象 API レベル、延長を申請した場合の配信期限は11月1日です。残り44日ですEASENV — ローカルビルドに渡したはずの秘密が、中身ではなく変数名の文字列のまま届く、という報告が長く開いたままですNEW — 推奨された移行先が、すでに停止していました。廃止表を74行突き合わせた記録ですUISCENE — iOS 27 では新しい画面ライフサイクルが必須です。SDK 57 では自分で有効にする設定で、既定になるのは 58 からですCREDIT — 「AI のエラーには消費しない」がどこまでを指すのかは、同じ修正を何度か頼んだ日の記録を取ると見えてきます
記事一覧/アプリ開発
アプリ開発/2026-08-05中級

キー短縮で 19% 減らしても配信は 3% しか軽くならない — カタログ JSON の圧縮実測と配置換え

2万件の壁紙カタログJSONで5形式を実測。キー短縮はgzip後3.2%しか効かず、列指向への配置換えは26.8%減。Brotli・ページング・JSON.parse時間まで、配信サイズ設計の判断材料を数値で並べます。

Rork569Expo209JSON2gzipパフォーマンス33

プレミアム記事

個人開発で運営している壁紙アプリのカタログは、気づけば2万件を超えていました。起動直後に取得するカタログ JSON は素の状態で約 7MB。gzip がかかっても 1MB 近くあり、回線の細い環境では初回表示の遅れが体感できるほどになっていました。

最初に手を付けたのはキー名の短縮です。imageUrlu に、fileSizef に。ファイルは 19% 小さくなり、これで一歩前進だと思っておりました。ところが配信されるサイズ、つまり gzip 後のサイズを測ると、削減はわずか 3.2%。コードの可読性を差し出した見返りとしては、あまりに小さい数字です。

どこを削れば配信が本当に軽くなるのか。思い込みを一つずつ実測で潰していった記録です。

計測の前提 — 2万件の合成カタログ

実測は Node v22.22.3 と標準モジュールの zlib だけで行いました。実データをそのまま載せるわけにはいかないため、実際のカタログと同じフィールド構成を持つ合成データを、シード固定の乱数で生成しています。シードを固定しているので、手元で走らせれば同じバイト数が再現できます。

1件のレコードは、壁紙アプリのカタログとして典型的な12フィールドです。ID・タイトル・カテゴリ・タグ配列・解像度・ファイルサイズ・画像 URL 2本・プレミアムフラグ・作成日時・ソート用スコア。これを 20,000 件並べます。

// gen.js — Node v22.22.3 / 依存なし(zlib は標準モジュール)
const zlib = require('zlib');
const fs = require('fs');
 
// シード固定の乱数(mulberry32)。実行のたびに同じカタログが再現される
function mulberry32(a) {
  return function () {
    a |= 0; a = (a + 0x6D2B79F5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}
const rand = mulberry32(20260805);
const cats = ['nature','city','space','abstract','animal','flower','sea','sky','night','minimal'];
const words = ['sakura','yozora','umi','yama','hikari','kaze','ame','yuki','hoshi','tsuki','asa','yugure','mori','kawa','sora','kumo'];
const pick = (a) => a[Math.floor(rand() * a.length)];
 
const N = 20000;
const items = [];
for (let i = 0; i < N; i++) {
  const id = i + 1;
  const cat = pick(cats);
  items.push({
    id,
    title: `${pick(words)}-${pick(words)}-${String(id).padStart(5, '0')}`,
    category: cat,
    tags: [pick(words), pick(words), pick(words)],
    width: [1080, 1170, 1290][Math.floor(rand() * 3)],
    height: [1920, 2532, 2796][Math.floor(rand() * 3)],
    fileSize: Math.floor(rand() * 4000000) + 200000,
    imageUrl: `https://cdn.example.com/wallpapers/${cat}/${id}/original.jpg`,
    thumbUrl: `https://cdn.example.com/wallpapers/${cat}/${id}/thumb.jpg`,
    premium: rand() < 0.2,
    createdAt: new Date(1600000000000 + Math.floor(rand() * 150000000000)).toISOString(),
    sortScore: Math.round(rand() * 100000) / 100,
  });
}

このベースラインに対して、次の4つの変換を用意しました。

// B: キー名を1文字に短縮(imageUrl → u など)
const KM = { id:'i', title:'t', category:'c', tags:'g', width:'w', height:'h',
             fileSize:'f', imageUrl:'u', thumbUrl:'v', premium:'p', createdAt:'d', sortScore:'s' };
const B = items.map((o) => { const r = {}; for (const k in o) r[KM[k]] = o[k]; return r; });
 
// C: URL 2本を落とす(category と id から端末側で組み立て直せるため)
const C = items.map(({ imageUrl, thumbUrl, ...rest }) => rest);
 
// D: 列指向 — オブジェクトの配列を「フィールドごとの配列」に転置する
const keys = Object.keys(items[0]);
const D = {};
for (const k of keys) D[k] = items.map((o) => o[k]);
 
// E: 列指向 + URL 除去の合わせ技
const E = {};
for (const k of keys.filter((k) => k !== 'imageUrl' && k !== 'thumbUrl')) E[k] = items.map((o) => o[k]);
 
// それぞれ raw / gzip(level 6, 9) / Brotli(quality 11) のバイト数を測る
for (const [name, v] of Object.entries({ A: items, B, C, D, E })) {
  const s = JSON.stringify(v);
  console.log(name, {
    raw: Buffer.byteLength(s),
    gz6: zlib.gzipSync(s, { level: 6 }).length,
    gz9: zlib.gzipSync(s, { level: 9 }).length,
    br11: zlib.brotliCompressSync(s, { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 11 } }).length,
  });
}

gzip の level 6 は CDN やリバースプロキシの既定値に相当します。Brotli は quality 11、つまり事前圧縮で使う最高設定です。

5つの形式と実測結果

結果を先に並べます。削減率はすべてベースライン A の同じ列に対する値です。

形式rawgzip(6)gzip(9)Brotli(11)gzip(6) 削減率
A ベースライン(オブジェクト配列)7,035,702943,796891,579691,526
B キー名短縮5,695,702913,189883,867664,434−3.2%
C URL 除去4,185,850745,072704,625559,871−21.1%
D 列指向4,695,841691,065648,090556,131−26.8%
E 列指向 + URL 除去2,285,963534,016523,630451,554−43.4%

raw の削減率と gzip 後の削減率が、まるで対応していません。B は raw で −19.0% なのに gzip 後は −3.2%。C は raw −40.5% に対して gzip 後 −21.1%。逆に D は raw −33.3% で、gzip 後は −26.8% と比較的よく残ります。

「ファイルを小さくすればその分だけ配信も軽くなる」という直感は、圧縮のかかった配信経路では成り立たないということです。

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

この記事の続きを読む

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

この記事で得られること
20,000件のカタログで5つのペイロード形式を実測比較(raw・gzip・Brotli・JSON.parse 時間の対照表つき)
キー名短縮が gzip 後は 3.2% しか効かない理由と、列指向への配置換えだけで 26.8% 減に届く理由
40分割ページングのサイズ増はわずか 2.9% — 分割をためらわなくてよい実測根拠
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

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

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

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

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

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

関連記事

アプリ開発2026-06-27
画面を離れてもアニメーションが回り続ける — Rork(Expo)アプリのフォーカス連動でバッテリーを守る
Rork が生成した Expo アプリで、別画面に進んだりホームに戻ったあともグラデーションや呼吸アニメーションが裏で回り続け、ジャンクとして表に出ないままバッテリーを静かに削っていく——その原因は Reanimated の withRepeat が UI スレッドで走り続けることと、ナビゲーションスタックが画面をマウントしたまま保持することにあります。useIsFocused と AppState を束ねたライフサイクル設計で、画面外とバックグラウンドのループを確実に止める実装を、動くコードで示します。
アプリ開発2026-09-18
prebuild が requires the standard Expo SDK 57 Swift AppDelegate で止まったとき、まず確かめる3行
iOS 27 のために ios.enableSceneSupport を有効にすると prebuild が止まることがあります。原因は SDK の版ではなく AppDelegate.swift の形でした。確かめる3行と、手で書くときに SDK 58 の手順を真似てはいけない理由をまとめます。
アプリ開発2026-09-16
消したはずのトークンが再起動で戻る — SecureStore の削除失敗を検知する設計
サインアウト直後に getItemAsync が null を返しても、Android のディスクには値が残っていることがあります。削除の成否を読み出しで確かめていた実装を、戻り値で判断する形へ書き直した記録です。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます