RORK LABEN
NATIVE — Rork Max では AR / LiDAR スキャン、Metal を使った 3D、Dynamic Island、Siri Intents、HealthKit、NFC、App Clips、Core ML まで届きますIOS27 — iOS 27 Developer Beta 4 で Siri AI の対象が iPhone 15 Pro / 15 Pro Max・16 シリーズ・17 系へ広がり、応答が初期ベータより明確に速くなりましたDESIGN — iOS・iPadOS・macOS 27 向けの Apple 純正デザインキットが Figma と Sketch 向けに公開されました。新 OS の UI を組む段階で参照できますCANARY — Android 17(Cinnamon Bun)は従来の Developer Preview を廃し、継続更新される Canary ビルドへ移行しました。検証のタイミング設計が変わりますSPARK — Android 17 の Gemini Spark は、アプリを直接操作して配車の予約や注文といった複合的な作業を自動化しますSCALE — Rork は a16z から $2.8M を調達し、月間訪問は74万件規模まで伸びていますNATIVE — Rork Max では AR / LiDAR スキャン、Metal を使った 3D、Dynamic Island、Siri Intents、HealthKit、NFC、App Clips、Core ML まで届きますIOS27 — iOS 27 Developer Beta 4 で Siri AI の対象が iPhone 15 Pro / 15 Pro Max・16 シリーズ・17 系へ広がり、応答が初期ベータより明確に速くなりましたDESIGN — iOS・iPadOS・macOS 27 向けの Apple 純正デザインキットが Figma と Sketch 向けに公開されました。新 OS の UI を組む段階で参照できますCANARY — Android 17(Cinnamon Bun)は従来の Developer Preview を廃し、継続更新される Canary ビルドへ移行しました。検証のタイミング設計が変わりますSPARK — Android 17 の Gemini Spark は、アプリを直接操作して配車の予約や注文といった複合的な作業を自動化しますSCALE — Rork は a16z から $2.8M を調達し、月間訪問は74万件規模まで伸びています
記事一覧/アプリ開発
アプリ開発/2026-08-05中級

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

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

Rork526Expo157JSON2gzipパフォーマンス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/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

アプリ開発2026-06-27
画面を離れてもアニメーションが回り続ける — Rork(Expo)アプリのフォーカス連動でバッテリーを守る
Rork が生成した Expo アプリで、別画面に進んだりホームに戻ったあともグラデーションや呼吸アニメーションが裏で回り続け、ジャンクとして表に出ないままバッテリーを静かに削っていく——その原因は Reanimated の withRepeat が UI スレッドで走り続けることと、ナビゲーションスタックが画面をマウントしたまま保持することにあります。useIsFocused と AppState を束ねたライフサイクル設計で、画面外とバックグラウンドのループを確実に止める実装を、動くコードで示します。
アプリ開発2026-07-16
壁紙の段組みギャラリーにネイティブ広告を差し込む — 広告セルの寿命をプールで設計する
段組みギャラリーにネイティブ広告を1枚入れたところ、20分のスクロールでメモリが伸び続けました。リスト再利用と広告の寿命が噛み合わない理由から、プール方式の実装、差し込み間隔の決め方まで実測とともに残します。
アプリ開発2026-07-14
壁紙を長押しして、プレビューと保存メニューを出す — Rork の Expo アプリで context menu を組む
壁紙ギャラリーのカードを長押ししても何も起きない。iOS利用者はプレビュー付きのメニューを期待します。Pressableだけでは届かない理由から、zeegoでのネイティブcontext menu実装、スクロールとの競合解消、保存・共有アクション、Android向けの自前フォールバックまで、動くコードでまとめます。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →