RORK LABEN
TOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しましたTOOLING — Rorkの開発者向けリポジトリが動き続けています。rork-xcodeが7月16日、rork-deviceが7月15日、rork-plistが7月13日に更新されましたOPUS46 — RorkでClaude Opus 4.6が稼働しています。Rork MaxはClaude Codeを土台にアプリを組み立てる設計ですSIM — ブラウザ上で動くクラウドのiOSシミュレータを備え、実機へのインストールは1クリック、App Storeへの提出は2クリックと案内されていますMAX — Rork MaxはReact Nativeではなく純粋なSwiftを出力します。iPhone・iPad・Apple Watch・Apple TV・Vision Pro、そしてiMessageまでが射程ですNATIVE — HealthKit、ARKitとLiDAR、NFC、Dynamic Island、Live Activities、Metalによる3D、Core MLのオンデバイス推論まで扱えますSEED — RorkはLeft Lane Capitalが主導する1,500万ドルのシードラウンドを実施し、Peak XVとa16z Speedrunが参加しました
記事一覧/開発ツール
開発ツール/2026-03-20上級

Rork 実践テクニック集【後編】— 収益化・本番品質・CI/CD・パフォーマンス最適化

Rork で作ったアプリを本番運用に乗せて収益化するまでに必要な、StoreKit 2 / RevenueCat、EAS CI/CD、Detox、SQLite + CRDT を中心に、2014年から個人開発を続けてきた中で身についた判断軸を添えて整理しました。

Rork515実践テクニック2上級7収益化66CI/CD8パフォーマンス30StoreKit8後編premium3

プレミアム記事

Rork で動くアプリを作って App Store にも上げた、というところまでは前編で辿り着けたとして、ここから先「ちゃんと残るアプリ」にするための判断は、公式ドキュメントだけでは決めきれない領域に入っていきます。後編は、その境目をどう乗り越えてきたかをまとめた、実装ノートに近い記事です。

私自身は 2013 年から個人で iOS / Android アプリの開発と運用を続けていて、壁紙系・癒し系・引き寄せ系などのアプリで累計 5,000 万ダウンロードに到達しました。AdMob 単体で月収 100 万円台に乗ったタイミングもありましたが、そこに至るまでには、StoreKit のレシート検証で本番事故を起こしたり、Detox の flake で CI が壊れ続けたりと、毎月のように落とし穴を踏んできました。Rork で短期間にプロトタイプから本番リリースまで一気に駆け抜けられるようになった今だからこそ、後編で扱うトピックは「最初に踏む」ことに意味があると感じています。

取り組みの背景 — 後編で扱うトピック

前編のアプリ設計と基本実装を踏まえて、ここでは収益化・本番品質・パフォーマンス・AI 機能・成長戦略を一本のロードマップとして扱います。網羅的に並べることが目的ではなく、「Rork で作ったアプリを 1 年後に振り返ったときに、最初にこれをやっておけばよかった」と私自身が感じてきた順序で並べました。

具体的には、次のテーマを順番に扱います。

  • StoreKit 2 と RevenueCat で課金導線を組む(introductory offer・ATT・サーバー検証)
  • Stripe サブスクリプションを Cloudflare Workers から扱うバックエンド
  • AdMob のバナー/インタースティシャル/リワードの 3 層配置と eCPM の現実的な目線
  • フリーミアム → 月額 → 年額の価格設計と、私が壁紙系アプリで失敗してきた価格テスト
  • Detox による E2E テスト、Sentry でのクラッシュ可観測性、EAS Build / Update での段階配信
  • React.memo・FlatList・画像キャッシュのパフォーマンス基準
  • SQLite + CRDT のオフラインファーストと、競合解消で詰みやすい箇所
  • Gemini API ストリーミング、Cloudflare Workers AI でのバックエンド分離
  • ASO と Rork Max 2クリック公開を組み合わせた初動の作り方

ここで紹介するコードは、Rork の生成結果に手を入れる「最後の 2 割」に当たる部分が中心です。前編の生成 → 微修正までの流れと併せて、後編のテクニックを参照していただくのが想定です。


アプリ収益化の実装

StoreKit 2 でサブスクリプション課金(Swift code)

Rork Max(SwiftUI)でネイティブな課金を実装する際の標準パターンです。

StoreKit 2 による商品設定と購入処理:

import StoreKit
import SwiftUI
 
@MainActor
class PurchaseManager: ObservableObject {
  @Published var availableProducts: [Product] = []
  @Published var purchasedProductIDs: Set<String> = []
  @Published var isLoading = false
 
  private let productIDs = [
    "com.yourapp.subscription.monthly",
    "com.yourapp.subscription.annual",
    "com.yourapp.lifetime",
  ]
 
  init() {
    Task {
      await fetchProducts()
      await updatePurchasedProducts()
      await listenForTransactions()
    }
  }
 
  func fetchProducts() async {
    isLoading = true
    do {
      self.availableProducts = try await Product.products(for: productIDs)
    } catch {
      print("Failed to fetch products: \(error)")
    }
    isLoading = false
  }
 
  func purchase(_ product: Product) async throws -> Transaction? {
    let result = try await product.purchase()
 
    switch result {
    case .success(let verification):
      let transaction = try verification.payloadValue
      await updatePurchasedProducts()
      await transaction.finish()
      return transaction
 
    case .userCancelled:
      print("User cancelled purchase")
      return nil
 
    case .pending:
      print("Purchase pending - awaiting user action")
      return nil
 
    @unknown default:
      return nil
    }
  }
 
  func updatePurchasedProducts() async {
    var purchasedIDs: Set<String> = []
 
    for await result in Transaction.currentEntitlements {
      let transaction = try result.payloadValue
 
      if transaction.revocationDate == nil {
        purchasedIDs.insert(transaction.productID)
      }
    }
 
    self.purchasedProductIDs = purchasedIDs
  }
 
  func listenForTransactions() async {
    for await result in Transaction.updates {
      let transaction = try result.payloadValue
      await updatePurchasedProducts()
      await transaction.finish()
    }
  }
}
 
struct SubscriptionView: View {
  @ObservedObject var purchaseManager: PurchaseManager
  @State private var selectedProductID: String?
  @State private var showError = false
  @State private var errorMessage = ""
 
  var body: some View {
    VStack(spacing: 20) {
      Text("プレミアム購入")
        .font(.title)
        .fontWeight(.bold)
 
      ForEach(purchaseManager.availableProducts, id: \.id) { product in
        SubscriptionCard(
          product: product,
          isSelected: selectedProductID == product.id,
          onSelect: { selectedProductID = product.id },
          onPurchase: {
            Task {
              do {
                _ = try await purchaseManager.purchase(product)
              } catch {
                errorMessage = error.localizedDescription
                showError = true
              }
            }
          }
        )
      }
 
      if let selectedID = selectedProductID,
         let product = purchaseManager.availableProducts.first(where: { $0.id == selectedID }) {
        Button(action: {
          Task {
            do {
              _ = try await purchaseManager.purchase(product)
            } catch {
              errorMessage = error.localizedDescription
              showError = true
            }
          }
        }) {
          Text("購入する")
            .fontWeight(.semibold)
            .frame(maxWidth: .infinity)
            .padding(12)
            .background(Color.blue)
            .foregroundColor(.white)
            .cornerRadius(8)
        }
      }
 
      Spacer()
    }
    .padding(16)
    .alert("エラー", isPresented: $showError) {
      Button("OK") { showError = false }
    } message: {
      Text(errorMessage)
    }
  }
}
 
struct SubscriptionCard: View {
  let product: Product
  let isSelected: Bool
  let onSelect: () -> Void
  let onPurchase: () -> Void
 
  var body: some View {
    VStack(alignment: .leading, spacing: 12) {
      HStack {
        VStack(alignment: .leading) {
          Text(product.displayName)
            .font(.headline)
            .fontWeight(.semibold)
 
          Text(product.description)
            .font(.caption)
            .foregroundColor(.gray)
            .lineLimit(2)
        }
 
        Spacer()
 
        Text(product.displayPrice)
          .font(.title3)
          .fontWeight(.bold)
          .foregroundColor(.blue)
      }
 
      HStack {
        Image(systemName: isSelected ? "checkmark.circle.fill" : "circle")
          .foregroundColor(isSelected ? .blue : .gray)
 
        Spacer()
 
        Button("詳細", action: onSelect)
          .font(.caption)
      }
    }
    .padding(12)
    .background(Color(UIColor.systemGray6))
    .cornerRadius(8)
    .onTapGesture(perform: onSelect)
  }
}

RevenueCat で課金管理を統合(TypeScript code)

サーバーサイドの課金検証と分析を RevenueCat で一元管理します。

// lib/revenuecat.ts
import Purchases, {
  CustomerInfo,
  PurchasesError,
} from 'react-native-purchases';
 
// RevenueCat 初期化
export const initializeRevenueCat = async () => {
  Purchases.debugLogsEnabled = true;
 
  if (Platform.OS === 'ios') {
    await Purchases.configure({ apiKey: 'appl_YOUR_API_KEY' });
  } else if (Platform.OS === 'android') {
    await Purchases.configure({ apiKey: 'goog_YOUR_API_KEY' });
  }
};
 
// サブスクリプション商品の取得
export const fetchSubscriptionOfferings = async () => {
  try {
    const offerings = await Purchases.getOfferings();
 
    if (offerings.current != null) {
      const monthlyPackage = offerings.current.monthly;
      const annualPackage = offerings.current.annual;
      const lifetimePackage = offerings.current.lifetime;
 
      return {
        monthly: monthlyPackage?.product,
        annual: annualPackage?.product,
        lifetime: lifetimePackage?.product,
      };
    }
  } catch (error) {
    console.error('Error fetching offerings:', error);
  }
};
 
// 購入処理
export const purchaseSubscription = async (packageKey: string) => {
  try {
    const offerings = await Purchases.getOfferings();
 
    if (offerings.current != null) {
      const package_ = offerings.current[packageKey];
 
      if (package_ != null) {
        const customerInfo = await Purchases.purchasePackage(package_);
 
        // 購入成功 - サブスクリプション有効期限を確認
        const entitlements = customerInfo.entitlements.active;
        if (entitlements['premium'] != null) {
          console.log('Premium subscription activated');
          return { success: true, expiryDate: entitlements['premium'].expirationDate };
        }
      }
    }
  } catch (error) {
    if (error instanceof PurchasesError) {
      if (error.code === 'PurchaseCancelledError') {
        console.log('Purchase cancelled by user');
      } else {
        console.error('Purchase error:', error.message);
      }
    }
  }
};
 
// 購入履歴と有効期限の確認
export const checkSubscriptionStatus = async (): Promise<{
  isPremium: boolean;
  expiryDate?: string;
}> => {
  try {
    const customerInfo: CustomerInfo = await Purchases.getCustomerInfo();
 
    const hasPremium = customerInfo.entitlements.active['premium'] != null;
 
    if (hasPremium) {
      const expiryDate =
        customerInfo.entitlements.active['premium'].expirationDate;
      return {
        isPremium: true,
        expiryDate: expiryDate || undefined,
      };
    }
 
    return { isPremium: false };
  } catch (error) {
    console.error('Error checking subscription:', error);
    return { isPremium: false };
  }
};
 
// ユーザー ID 設定(ログイン時に呼び出し)
export const setUserID = async (userID: string) => {
  await Purchases.logIn(userID);
};
 
// ユーザーログアウト
export const logoutUser = async () => {
  await Purchases.logOut();
};

Stripe サブスクリプションのサーバーサイド実装

Rork のバックエンド(Node.js + Supabase)から Stripe を制御します。

// server/services/stripeService.ts
import Stripe from 'stripe';
 
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY);
 
interface SubscriptionPlan {
  id: string;
  name: string;
  priceId: string;
  monthlyPrice: number;
  features: string[];
}
 
export const SUBSCRIPTION_PLANS: Record<string, SubscriptionPlan> = {
  monthly: {
    id: 'plan_monthly',
    name: 'Monthly Pro',
    priceId: 'price_monthly_usd',
    monthlyPrice: 4.99,
    features: ['基本機能', 'クラウド同期', '優先サポート'],
  },
  annual: {
    id: 'plan_annual',
    name: 'Annual Pro',
    priceId: 'price_annual_usd',
    monthlyPrice: 3.33,
    features: ['すべてのMonthlyの機能', '25%割引'],
  },
  lifetime: {
    id: 'plan_lifetime',
    name: 'Lifetime Access',
    priceId: 'price_lifetime_usd',
    monthlyPrice: 39.99,
    features: ['永続アクセス', 'すべての機能', '無制限サポート'],
  },
};
 
export const stripeService = {
  // Stripe カスタマー作成
  async createCustomer(email: string, name: string) {
    const customer = await stripe.customers.create({
      email,
      name,
      metadata: {
        signupDate: new Date().toISOString(),
      },
    });
    return customer;
  },
 
  // サブスクリプション作成
  async createSubscription(
    customerId: string,
    planKey: 'monthly' | 'annual' | 'lifetime'
  ) {
    const plan = SUBSCRIPTION_PLANS[planKey];
 
    const subscription = await stripe.subscriptions.create({
      customer: customerId,
      items: [{ price: plan.priceId }],
      payment_behavior: 'default_incomplete',
      expand: ['latest_invoice.payment_intent'],
    });
 
    return subscription;
  },
 
  // サブスクリプション更新(プランアップグレード)
  async upgradeSubscription(
    subscriptionId: string,
    newPlanKey: 'monthly' | 'annual' | 'lifetime'
  ) {
    const subscription = await stripe.subscriptions.retrieve(subscriptionId);
    const plan = SUBSCRIPTION_PLANS[newPlanKey];
 
    const updated = await stripe.subscriptions.update(subscriptionId, {
      items: [
        {
          id: subscription.items.data[0].id,
          price: plan.priceId,
        },
      ],
      proration_behavior: 'create_prorations',
    });
 
    return updated;
  },
 
  // サブスクリプションキャンセル
  async cancelSubscription(subscriptionId: string) {
    const canceled = await stripe.subscriptions.del(subscriptionId);
    return canceled;
  },
 
  // 請求書の一覧取得
  async getInvoices(customerId: string) {
    const invoices = await stripe.invoices.list({
      customer: customerId,
      limit: 12,
    });
    return invoices.data;
  },
 
  // 支払い状態確認
  async getPaymentStatus(customerId: string) {
    const invoices = await stripe.invoices.list({
      customer: customerId,
      limit: 1,
    });
 
    const latestInvoice = invoices.data[0];
    if (!latestInvoice) return null;
 
    return {
      status: latestInvoice.status,
      amount: latestInvoice.amount_due,
      dueDate: latestInvoice.due_date,
    };
  },
};

AdMob バナー + インタースティシャル + リワードの3層構成

無料アプリの広告戦略:バナー(常時)→ インタースティシャル(アクション後)→ リワード(オプト)

// lib/admobService.ts
import { GoogleMobileAds, BannerAd, BannerAdSize, InterstitialAd, AdEventType, RewardedAd, RewardedAdEventType } from 'react-native-google-mobile-ads';
 
const ADMOB_APP_ID = 'ca-app-pub-xxxxxxxxxxxxxxxx';
const BANNER_AD_ID = 'ca-app-pub-3940256099942544/6300978111'; // テスト用
const INTERSTITIAL_AD_ID = 'ca-app-pub-3940256099942544/1033173712'; // テスト用
const REWARDED_AD_ID = 'ca-app-pub-3940256099942544/5224354917'; // テスト用
 
export const initializeAdMob = async () => {
  await GoogleMobileAds.initialize();
};
 
// インタースティシャル広告の初期化と表示
let interstitialAd: InterstitialAd;
 
const createInterstitialAd = () => {
  interstitialAd = InterstitialAd.createForAdRequest(INTERSTITIAL_AD_ID, {
    requestNonPersonalizedAdsOnly: false,
  });
 
  interstitialAd.addAdEventListener(AdEventType.LOADED, () => {
    console.log('Interstitial ad loaded');
  });
 
  interstitialAd.addAdEventListener(AdEventType.ERROR, (error) => {
    console.error('Interstitial ad error:', error);
  });
 
  interstitialAd.load();
};
 
export const showInterstitialAd = async () => {
  if (!interstitialAd) {
    createInterstitialAd();
    return;
  }
 
  try {
    if (await interstitialAd.isLoaded()) {
      await interstitialAd.show();
      // 広告表示後、次の広告をロード
      createInterstitialAd();
    }
  } catch (error) {
    console.error('Error showing interstitial ad:', error);
  }
};
 
// リワード広告(ユーザーがプレミアム機能を無料で試せる)
let rewardedAd: RewardedAd;
 
const createRewardedAd = () => {
  rewardedAd = RewardedAd.createForAdRequest(REWARDED_AD_ID);
 
  rewardedAd.addAdEventListener(RewardedAdEventType.LOADED, () => {
    console.log('Rewarded ad loaded');
  });
 
  rewardedAd.addAdEventListener(RewardedAdEventType.EARNED_REWARD, (reward) => {
    console.log('User earned reward:', reward);
    // リワード付与処理(例:1日無料トライアルを付与)
    handleRewardEarned();
  });
 
  rewardedAd.addAdEventListener(RewardedAdEventType.ERROR, (error) => {
    console.error('Rewarded ad error:', error);
  });
 
  rewardedAd.load();
};
 
export const showRewardedAd = async () => {
  if (!rewardedAd) {
    createRewardedAd();
    return;
  }
 
  try {
    if (await rewardedAd.isLoaded()) {
      await rewardedAd.show();
    }
  } catch (error) {
    console.error('Error showing rewarded ad:', error);
  }
};
 
const handleRewardEarned = async () => {
  // ユーザーの premium_expires_at を 1 日延長
  const user = await authService.getCurrentUser();
  if (!user) return;
 
  const currentExpiry = await supabase
    .from('user_subscriptions')
    .select('premium_expires_at')
    .eq('user_id', user.id)
    .single();
 
  const newExpiry = new Date(currentExpiry.data.premium_expires_at);
  newExpiry.setDate(newExpiry.getDate() + 1);
 
  await supabase
    .from('user_subscriptions')
    .update({ premium_expires_at: newExpiry.toISOString() })
    .eq('user_id', user.id);
};
 
// バナー広告コンポーネント
export const BannerAdComponent = () => {
  return (
    <BannerAd
      unitId={BANNER_AD_ID}
      size={BannerAdSize.ANCHORED_ADAPTIVE_BANNER}
      onAdFailedToLoad={(error) => console.error('Banner ad error:', error)}
    />
  );
};

フリーミアム→月額$4.99→年額$39.99 の価格設計

価格設計の戦略:

// pricing.ts
export const PRICING_TIERS = {
  free: {
    name: 'フリー',
    price: 0,
    priceDisplay: '無料',
    features: [
      '基本的なタスク管理',
      '5個までのプロジェクト',
      '広告あり',
      'メールサポート(回答は営業日ベース)',
    ],
    limitations: {
      maxProjects: 5,
      maxTeamMembers: 1,
      storageGB: 1,
      monthlyExports: 5,
    },
  },
 
  pro_monthly: {
    name: 'Pro(月額)',
    price: 4.99,
    priceDisplay: '$4.99/月',
    billingPeriod: 'monthly',
    features: [
      'すべてのフリー機能',
      '無制限プロジェクト',
      '無制限チームメンバー',
      '広告なし',
      '優先メールサポート(24時間以内)',
      '10GB ストレージ',
      'API アクセス',
      'カスタムテンプレート',
    ],
    limitations: {
      maxProjects: null,
      maxTeamMembers: null,
      storageGB: 10,
      monthlyExports: null,
    },
    savingsVsMonthly: 0,
  },
 
  pro_annual: {
    name: 'Pro(年額)',
    price: 39.99,
    priceDisplay: '$39.99/年',
    billingPeriod: 'annual',
    features: [
      'すべての Pro 機能',
      '年間購読で 33% OFF',
      '高度なレポート機能',
      'Slack インテグレーション',
      '優先電話サポート',
    ],
    limitations: {
      maxProjects: null,
      maxTeamMembers: null,
      storageGB: 50,
      monthlyExports: null,
    },
    savingsVsMonthly: 0.33, // 33% 割引
  },
 
  lifetime: {
    name: 'ライフタイム',
    price: 199.99,
    priceDisplay: '$199.99',
    billingPeriod: 'one_time',
    features: [
      'すべての Pro 機能',
      '永続アクセス(生涯更新無料)',
      'プレミアムサポート(優先度最高)',
      '200GB ストレージ',
      'エンタープライズ機能',
      'ホワイトラベル対応',
    ],
    limitations: {
      maxProjects: null,
      maxTeamMembers: null,
      storageGB: 200,
      monthlyExports: null,
    },
  },
};
 
// LTV(ライフタイムバリュー)を最大化する価格設定の考え方
export const CONVERSION_STRATEGY = {
  freeToMonthly: {
    // 無料ユーザーを Pro(月額)に転換する施策
    triggers: [
      'プロジェクト数が 5 に達した時点で 1 つ増やしたい',
      '10 日連続で使用しておりエンゲージメント高い',
      'API を使おうとする動作をした',
    ],
    incentive: '初月 50% OFF クーポン',
  },
 
  monthlyToAnnual: {
    // Pro(月額)ユーザーを Pro(年額)に転換する施策
    triggers: [
      '3ヶ月以上の継続購読',
      'Pro 機能を 80% 以上活用している',
    ],
    incentive: '年額に変更で 3ヶ月分無料に相当',
  },
 
  anyToLifetime: {
    // いずれかのプランから Lifetime への転換施策
    triggers: [
      '年 1 回のセール時期',
      'ユーザー評価 4.8 以上を達成',
    ],
    incentive: 'Bundle: Pro 月額 + Lifetime で特別割引',
  },
 
  retention: {
    // チャーン防止施策
    triggers: [
      '30日以上未使用',
      'Pro 登録後 30 日で未活動',
    ],
    action: '30% OFF リカバリーオファーメール',
  },
};

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

この記事の続きを読む

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

この記事で得られること
2014年から累計5,000万DLを運用してきた中で、AdMob 月収100万円台に到達したアプリで何が効いたか/何を捨てたかを具体的に書きました。
StoreKit 2 + RevenueCat の introductory offer / ATT 連携、Detox の flake 対策、EAS Update の段階配信など、公式ドキュメントだけでは詰みやすい箇所に動くコードと数値を添えて整理しています。
前編のアーキテクチャを引き継いで、収益化・品質保証・パフォーマンス・AI 機能を一本のロードマップとして読めるように構成しました。
Stripe による安全な決済 · いつでもキャンセル可能

この記事を購入する

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

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

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

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

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

関連記事

開発ツール2026-07-10
React Compiler を Expo に入れて、手書き memo を 41 箇所消しました
Rork が生成した React Native 画面に React Compiler を導入し、再レンダリング回数を Profiler で実測しました。手書き memo/useCallback の削除判断、効かない箇所の見分け方、CI での回帰検知までをまとめています。
開発ツール2026-06-29
New Architecture へ移行したら本番だけ画面がカクつくとき — interop 層への静かなフォールバックを計測して切り分ける運用メモ
New Architecture へ切り替えた Rork アプリが、開発中は快適なのにリリースビルドの実機でだけ一覧スクロールがカクつく。原因は旧式ネイティブモジュールが interop 層へ静かにフォールバックしていたことでした。検知の計測と段階的な切り戻しの実務メモです。
開発ツール2026-06-25
壁紙アプリの画像キャッシュが静かに膨らんでメモリで落ちる — Rork運用で効いた計測と上限設計の運用メモ
画像が主役のRorkアプリで、ディスクキャッシュとメモリ常駐が少しずつ膨らみ、OOMクラッシュとして表面化する問題への対処。実測フック・キャッシュ上限・配信側リサイズまで、運用で効いた順に実装コード付きで整理します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →