2014年頃から個人でアプリ開発を続けてきた中で、壁紙アプリというジャンルをずっと軸の一つに置いてきました。累計5,000万ダウンロードを超えるまでの道のりで、ユーザーが最初に感じる「重さ」ほどアンインストール率に直結するものはないと実感しています。
Rork であるプロトタイプを作っていたとき、スクロールのたびに画像が白くなる現象が気になり始めました。実機で動かすと、高解像度の壁紙画像が表示されるまでに400〜600ms かかっていて、スクロールするたびに白いチラつきが起きていました。Rork が生成したコードを確認すると、標準の <Image> コンポーネントが使われていました。これが問題の根本です。
なぜ Rork の標準 Image コンポーネントは遅くなるのか
React Native の標準 <Image> コンポーネントは基本的な表示には問題ありませんが、画像の多いアプリでは顕著な制約があります。
- メモリキャッシュが弱い: 一度表示した画像でも、スクロールで画面外に出ると再読み込みが発生することがあります
- Progressive loading がない: 画像全体がロードされるまで何も表示されません
- プレースホルダーを自前で実装する必要がある: ロード中の見た目を工夫しようとすると、かなりの実装コストがかかります
Rork は React Native の標準 API を使ってコードを生成するため、特に指定しない限り <Image> が使われます。壁紙アプリのように高解像度の画像を数十枚スクロールさせるケースでは、この設計は体験の足を引っ張ります。
個人開発でAdMobを軸に収益化しているアプリほど、ユーザーが最初の数秒で離れるかどうかが広告表示回数に直結します。画像の表示速度はサイレントキラーです。
expo-image が解決すること
expo-image は Expo チームが開発した画像コンポーネントで、React Native 標準の <Image> の上位互換として設計されています。
主な特徴は次の通りです。
- LRU ディスクキャッシュ: 一度ダウンロードした画像はディスクに保存され、次回はほぼ瞬時に表示されます
- blur hash サポート: 画像の代替として色のぼかし(blur hash)をプレースホルダーとして表示できます。画像が読み込まれる前から「それらしい色」が見えるため、体感速度が大きく改善します
- Crossfade アニメーション: 画像のロード完了時になめらかにフェードインします
- プログレッシブ JPEG 対応: 段階的に画像を表示できます
壁紙アプリに限らず、画像を多用するアプリなら expo-image への切り替えはほぼ確実に効果があります。
Rork で expo-image を使うよう指示するプロンプト
新規プロジェクトを始める場合、プロンプトに明示的に記載するだけで expo-image を使ったコードが生成されます。
画像の表示にはすべて expo-image の Image コンポーネントを使ってください。
React Native の標準 Image は使用しないでください。
cachePolicy="memory-disk" を設定し、
placeholder={{ blurhash: "..." }} も追加してください。
既存プロジェクトを修正する場合は、次のように伝えます。
現在 <Image source={{ uri: ... }} /> で実装されている部分を、
expo-image の <Image> に置き換えてください。
cachePolicy="memory-disk" を設定し、
placeholder={{ blurhash: "..." }} も追加してください。
Rork がコードを生成したあと、package.json に expo-image が依存関係として追加されていることを確認してください。
実装コード:標準 Image から expo-image への切り替え
Before(Rork が生成するデフォルトのコード):
import { Image } from 'react-native';
// 壁紙一覧の各アイテム
function WallpaperItem({ uri }: { uri: string }) {
return (
<Image
source={{ uri }}
style={{ width: '100%', height: 200 }}
resizeMode="cover"
/>
);
}After(expo-image に切り替え後):
import { Image } from 'expo-image';
// blur hash は画像アップロード時にサーバー側で事前生成しておきます
// このサンプルはウォームグレー系のプレースホルダー
const FALLBACK_BLURHASH = 'L6PZfSi_.AyE_3t7t7R**0o#DgR4';
function WallpaperItem({ uri, blurhash }: { uri: string; blurhash?: string }) {
return (
<Image
source={{ uri }}
style={{ width: '100%', height: 200 }}
contentFit="cover" // resizeMode="cover" と同じ意味
placeholder={{ blurhash: blurhash ?? FALLBACK_BLURHASH }}
transition={200} // 200ms のフェードイン
cachePolicy="memory-disk" // メモリ + ディスクにキャッシュ
/>
);
}contentFit は resizeMode に相当します。"cover" → "cover"、"contain" → "contain" と対応しているため、値の書き換えは不要です。
API 面での変更はほぼこれだけです。大半は find-and-replace と少量の追記で済むため、既存アプリへの移行は1〜2時間で完了することが多いです。
blur hash の生成方法
blur hash は画像の「色の要約」をエンコードした短い文字列です。expo-image はこの文字列をデコードして、ぼかした色のプレビューをリアルタイムで描画します。
壁紙アプリの場合、画像は管理者側がアップロードするケースが多いため、アップロード時に blur hash を生成してデータベースに保存しておくのが現実的です。
Node.js でのサーバーサイド生成例:
import { encode } from 'blurhash';
import sharp from 'sharp';
async function generateBlurhash(imagePath: string): Promise<string> {
const { data, info } = await sharp(imagePath)
.raw()
.ensureAlpha()
.resize(32, 32, { fit: 'inside' }) // 処理を軽くするため小さくリサイズ
.toBuffer({ resolveWithObject: true });
return encode(
new Uint8ClampedArray(data),
info.width,
info.height,
4, // X コンポーネント数
4 // Y コンポーネント数
);
}Cloudflare R2 に壁紙を保存している場合は、Workers でアップロードを受け取るタイミングに blur hash 生成を組み込んで、結果を D1 や KV に保存しておくのが効率的です。
blur hash が用意できない段階では、アプリ全体で共通のフォールバック blur hash を定数として定義しておくことで、「全画像が白いまま」という状態は避けられます。
切り替え後の変化:実機での確認
expo-image に切り替えたあと、Rork Companion アプリで実機テストしたところ、次のような変化がありました。
- 初回表示: blur hash のプレースホルダーが即時表示されるため、体感的な待機感がほぼなくなりました
- 2回目以降: ディスクキャッシュにより、同じ画像の再表示がほぼ瞬時になりました
- スクロールバック時: 画面外に出た画像もメモリキャッシュに残るため、スクロールバックしたときのチラつきがなくなりました
ベンチマークツールを使った数値計測ではなく実感の話になりますが、個人開発で壁紙アプリを長く運用してきた感覚として、ユーザーレビューの「重い」「遅い」という声が減るのはこうした細かい改善の積み重ねだと考えています。
アーティストとして、見た目の美しさを追求しても、それを届ける体験が遅ければ意味がありません。ユーザーが壁紙を一枚見つける瞬間まで、技術的な「摩擦」を限りなく小さくしておくことが、私がアプリ開発で大切にしていることの一つです。
関連する記事
画像最適化以外の全体的なパフォーマンス改善に取り組む場合は、Rork アプリ パフォーマンス最適化完全ガイドも参考になります。
大量アイテムのリスト表示が重い場合は、FlatList でカクつき始めたら読む記事をあわせてご覧ください。
壁紙アプリを最初から作りたい方は、RorkでウォールペーパーアプリをApp Storeに公開した話が参考になります。
まず1つの <Image> コンポーネントだけ expo-image に切り替えて、実機で blur hash のプレースホルダーが出ることを確認してみてください。体感の変化が分かれば、残りの移行は自然に進みます。