なぜパフォーマンス最適化が重要なのか
Rork で素早くモバイルアプリを作れるようになった今、次のステップはユーザー体験の質を上げることです。アプリの読み込みが1秒遅くなるだけでユーザーの離脱率は大きく上昇します。ここではRork で構築したアプリのパフォーマンスを改善する具体的なテクニックを、レンダリング・メモリ・ネットワークの3つの観点から解説します。
レンダリング最適化
不要な再レンダリングを防ぐ
React Native ベースの Rork アプリでは、コンポーネントの再レンダリングがパフォーマンスの最大のボトルネックになります。React.memo と useMemo を適切に使うことで、不要な再描画を防げます。
// components/ProductCard.tsx
// メモ化でリスト表示のパフォーマンスを改善
import React, { useMemo } from "react";
import { View, Text, Image, StyleSheet } from "react-native";
interface Product {
id: string;
name: string;
price: number;
imageUrl: string;
discount?: number;
}
// React.memo で props が変わらない限り再レンダリングしない
const ProductCard = React.memo(({ product }: { product: Product }) => {
// 割引価格の計算をメモ化(product.price / product.discount が変わらない限り再計算しない)
const finalPrice = useMemo(() => {
if (product.discount) {
return Math.round(product.price * (1 - product.discount / 100));
}
return product.price;
}, [product.price, product.discount]);
return (
<View style={styles.card}>
<Image source={{ uri: product.imageUrl }} style={styles.image} />
<Text style={styles.name}>{product.name}</Text>
<Text style={styles.price}>¥{finalPrice.toLocaleString()}</Text>
</View>
);
});
const styles = StyleSheet.create({
card: { padding: 12, borderRadius: 8, backgroundColor: "#fff", marginBottom: 8 },
image: { width: "100%", height: 160, borderRadius: 6 },
name: { fontSize: 16, fontWeight: "600", marginTop: 8 },
price: { fontSize: 14, color: "#e74c3c", marginTop: 4 },
});
export default ProductCard;
// 使用例: FlatList で大量のカードをレンダリング
// <FlatList data={products} renderItem={({item}) => <ProductCard product={item} />} />FlatList の最適化
大量のアイテムを表示する場合、FlatList の設定が重要です。
// screens/ProductListScreen.tsx
// FlatList の最適化設定
import { FlatList } from "react-native";
import ProductCard from "../components/ProductCard";
export function ProductListScreen({ products }: { products: Product[] }) {
return (
<FlatList
data={products}
renderItem={({ item }) => <ProductCard product={item} />}
keyExtractor={(item) => item.id}
// パフォーマンス最適化の設定
removeClippedSubviews={true} // 画面外のコンポーネントをアンマウント
maxToRenderPerBatch={10} // 1バッチあたりの最大レンダリング数
windowSize={5} // 表示ウィンドウの倍数(デフォルト21→5に削減)
initialNumToRender={8} // 初期レンダリング数
getItemLayout={(_, index) => ({ // アイテムの高さが固定ならレイアウト計算をスキップ
length: 200,
offset: 200 * index,
index,
})}
/>
);
}
// 期待効果: 1000件のリストで初期表示が約60%高速化メモリ管理
画像のメモリ最適化
モバイルアプリでメモリを最も消費するのは画像です。Rork アプリでは、画像のキャッシュ戦略と適切なサイズ指定がメモリ使用量を大幅に削減します。
// utils/imageOptimizer.ts
// 画像のリサイズとキャッシュ管理
interface ImageConfig {
uri: string;
width: number;
height: number;
quality?: number; // 1-100
}
function getOptimizedImageUrl(config: ImageConfig): string {
const { uri, width, height, quality = 80 } = config;
// CDN の画像変換パラメータを活用(Cloudflare Images の例)
if (uri.includes("imagedelivery.net")) {
return `${uri}/w=${width},h=${height},q=${quality},fit=cover`;
}
// imgix の場合
if (uri.includes("imgix.net")) {
return `${uri}?w=${width}&h=${height}&q=${quality}&auto=format`;
}
return uri;
}
// 使用例
const thumbnailUrl = getOptimizedImageUrl({
uri: "https://imagedelivery.net/abc123/product-photo",
width: 300,
height: 200,
quality: 75,
});
console.log(thumbnailUrl);
// 期待出力: https://imagedelivery.net/abc123/product-photo/w=300,h=200,q=75,fit=coveruseEffect のクリーンアップ
メモリリークの主要因は、アンマウント時にクリーンアップされないサブスクリプションやタイマーです。
// hooks/useAutoRefresh.ts
// 適切なクリーンアップでメモリリークを防止
import { useEffect, useRef } from "react";
export function useAutoRefresh(callback: () => void, intervalMs: number) {
const callbackRef = useRef(callback);
callbackRef.current = callback;
useEffect(() => {
const timer = setInterval(() => {
callbackRef.current();
}, intervalMs);
// コンポーネントがアンマウントされたらタイマーを確実にクリア
return () => {
clearInterval(timer);
};
}, [intervalMs]);
}
// 使用例:
// useAutoRefresh(() => fetchLatestData(), 30000); // 30秒ごとに自動更新
// コンポーネントが破棄されるとタイマーも自動的にクリアされるネットワーク効率化
リクエストのデバウンスとキャッシュ
検索機能のように頻繁にリクエストが発生する場面では、デバウンスとキャッシュの組み合わせが効果的です。
// hooks/useDebouncedSearch.ts
// デバウンス + キャッシュで検索パフォーマンスを最適化
import { useState, useEffect, useRef } from "react";
interface SearchResult {
id: string;
title: string;
}
const searchCache = new Map<string, { data: SearchResult[]; timestamp: number }>();
const CACHE_TTL = 5 * 60 * 1000; // 5分
export function useDebouncedSearch(query: string, delayMs = 300) {
const [results, setResults] = useState<SearchResult[]>([]);
const [loading, setLoading] = useState(false);
const abortRef = useRef<AbortController | null>(null);
useEffect(() => {
if (query.length < 2) {
setResults([]);
return;
}
// キャッシュチェック
const cached = searchCache.get(query);
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
setResults(cached.data);
return;
}
const timer = setTimeout(async () => {
// 前のリクエストをキャンセル
abortRef.current?.abort();
abortRef.current = new AbortController();
setLoading(true);
try {
const res = await fetch(`/api/search?q=${encodeURIComponent(query)}`, {
signal: abortRef.current.signal,
});
const data: SearchResult[] = await res.json();
searchCache.set(query, { data, timestamp: Date.now() });
setResults(data);
} catch (error) {
if (error instanceof Error && error.name !== "AbortError") {
console.error("Search failed:", error.message);
}
} finally {
setLoading(false);
}
}, delayMs);
return () => clearTimeout(timer);
}, [query, delayMs]);
return { results, loading };
}
// 使用例:
// const { results, loading } = useDebouncedSearch(searchText);
// 期待動作: 300ms のデバウンス + 5分キャッシュで重複リクエストを削減パフォーマンス計測
最適化の効果を数値で確認するために、計測を組み込みましょう。
// utils/performanceMonitor.ts
// シンプルなパフォーマンス計測ユーティリティ
class PerformanceMonitor {
private marks = new Map<string, number>();
start(label: string) {
this.marks.set(label, performance.now());
}
end(label: string): number {
const start = this.marks.get(label);
if (!start) return -1;
const elapsed = Math.round(performance.now() - start);
this.marks.delete(label);
console.log(`[Perf] ${label}: ${elapsed}ms`);
return elapsed;
}
async measure<T>(label: string, fn: () => Promise<T>): Promise<T> {
this.start(label);
const result = await fn();
this.end(label);
return result;
}
}
export const perf = new PerformanceMonitor();
// 使用例:
// const data = await perf.measure("fetchProducts", () => api.getProducts());
// 期待出力: [Perf] fetchProducts: 245ms全体を振り返って
Rork アプリのパフォーマンス最適化は、レンダリング(React.memo / FlatList 設定)、メモリ(画像最適化 / クリーンアップ)、ネットワーク(デバウンス / キャッシュ)の3つの柱で構成されます。すべてを一度に実装する必要はなく、計測結果に基づいて最もインパクトのある箇所から着手しましょう。
12年の個人開発で見えてきたこと
リリース直後にチェックしているもの
- クラッシュレポートの上位エラーを24時間ごとに確認しているか
- AdMob/StoreKit の収益ダッシュボードに異常検知が出ていないか
- ユーザーレビューに技術的な指摘が含まれていないか
12年の個人開発から見る Rork の使いどころ
リリース直後にチェックしているもの
- クラッシュレポートの上位エラーを24時間ごとに確認しているか
- AdMob/StoreKit の収益ダッシュボードに異常検知が出ていないか
- ユーザーレビューに技術的な指摘が含まれていないか