---
title: "難易度・地獄：スマホカメラのパフォーマンス最適化"
description: "カメラはスマホで一番手ごわい部分です。毎秒何十枚ものフレームが、一枚残らず CPU・GPU・メモリのリソースを奪い合ってきます。"
author: "kidneyweakx"
published: 2026-06-02T00:00:00.000Z
updated: 2026-10-29T11:37:17.685Z
lang: ja
tags: ["react-native", "vision-camera", "ios", "效能"]
canonical: https://kidneyweakx.com/blog/%E5%9C%B0%E7%8D%84%E9%9B%A3%E5%BA%A6-%E6%89%8B%E6%A9%9F%E7%9B%B8%E6%A9%9F%E6%95%88%E8%83%BD%E5%84%AA%E5%8C%96?lang=ja
alternate_zh_tw: https://kidneyweakx.com/blog/%E5%9C%B0%E7%8D%84%E9%9B%A3%E5%BA%A6-%E6%89%8B%E6%A9%9F%E7%9B%B8%E6%A9%9F%E6%95%88%E8%83%BD%E5%84%AA%E5%8C%96.md
alternate_en: https://kidneyweakx.com/blog/%E5%9C%B0%E7%8D%84%E9%9B%A3%E5%BA%A6-%E6%89%8B%E6%A9%9F%E7%9B%B8%E6%A9%9F%E6%95%88%E8%83%BD%E5%84%AA%E5%8C%96.md?lang=en
---

# 難易度・地獄：スマホカメラのパフォーマンス最適化

> カメラはスマホで一番手ごわい部分です。毎秒何十枚ものフレームが、一枚残らず CPU・GPU・メモリのリソースを奪い合ってきます。

この一年で、カメラをがっつり使うアプリを 2 つ作りました。Solid(ar)ity は QR を読み取り、パスポートの MRZ（パスポートの一番下にある、あの読めない 2 行の文字です）を読む必要があります。もう一つの聖地巡礼アプリは、カメラ映像にアニメのスクリーンショットを重ねてリアルタイムで位置合わせをします。どちらも React Native + VisionCamera です。

作り終えての感想は一言だけ。**カメラという道は、難易度・地獄です**。

<br />

## 地獄の第一層：1 フレーム (Frame) ごとのコストが高い

VisionCamera の frame processor を使えば、フレームごとにコードを走らせられます。聞こえはいいのですが、計算してみてください。30fps なら 33 ミリ秒ごとにフル解像度の画像を 1 枚処理し終えなければならず、間に合わなければフレーム落ち、発熱、バッテリー消費です。

**解決策：まず縮小する。**

ほとんどの解析は、そもそもフル解像度なんて要りません。巡礼アプリでは画面の明るさと色温度を計算したいので、GPU resizer で画面を 64×64 まで落としてから平均値を取っています。OCR のように細部が必要なものでも、ROI（関心領域）だけを切り出して、画面全体を丸ごと渡したりはしません。

**GPU でできることは、CPU に戻さない。** この一つだけで、ほかのどんなごちゃごちゃした最適化よりも多くのパフォーマンスを稼げました。

## 地獄の第二層：Xcode Cloud はシェーダーをコンパイルしてくれない

縮小に使っている VisionCameraResizer は Metal シェーダーです。ローカルでは問題なく動いていたのに、Xcode Cloud でビルドした途端に落ちました。CI 環境に Metal toolchain がないからです。

最終的な解決策は、**シェーダーを事前に metallib にコンパイルして**、プロジェクトと一緒にバージョン管理に入れておくこと。これで CI がその場でコンパイルする必要がなくなります。この手の問題で「シェーダーを事前コンパイルしろ」と教えてくれるエラーメッセージは一つもなく、一層ずつ自分で掘っていくしかありません。ただ、その代わりにローカルのビルド時間全体が長くなってしまったのですが……。

<br />

## 地獄の第三層：クラッシュするのに、理由を教えてくれない

ここが本当の地獄です。Solid(ar)ity のパスポート MRZ スキャンが、iPhone XR（A12）でランダムにクラッシュしていました。クラッシュレポートにあるのはこの 1 行だけ。

```
abort() called
```

例外の型もなし、メッセージもなし、JS のスタックもなし。何もありません。

<br />

長いこと追いかけて、ようやく犯人を 2 人見つけました。

**犯人その 1：worklet の中の `console.log`**

OCR の worklet は VisionCamera の async-runner runtime 上で動いていて、Reanimated の UI runtime **ではありません**。この runtime には JS のスケジューリング用のグローバル関数がないので、

* `console.log(...)` が例外を投げる（内部で `scheduleOnRN` を通るため）
* 例外が `runAsync` からどんどん上に伝播し、Nitro を突き抜けて、そのまま `std::terminate` でアプリごと落とす

そう、**デバッグのために log を 1 行足したら、その log 自体がアプリをクラッシュさせる原因だった**のです。今年遭遇した中で一番ばかげたバグでした。

**犯人その 2：A12 で Apple Vision が C++ 例外を投げる**

Vision の文字認識を `.accurate` モードで使うと、A12 のニューラルエンジン上で推論の途中に **C++ 例外**を投げます。NSException ではありません。なので Objective-C の `@catch (NSException *)` ではまったく捕まえられず、必ず `catch (...)` を使う必要があります。

## どうやって這い上がったか

**1. クラッシュに自分から名乗らせる**

アプリ起動時に、グローバルな `std::set_terminate` handler を仕込みました。捕まえられなかった例外はすべて、型とメッセージをクラッシュレポートの Application Specific Information に書き込み、さらに os\_log を 1 本出します。これでクラッシュレポートはもう `abort() called` だけではなくなり、どの関数で爆発したのかを直接教えてくれるようになりました。**この一手だけで、デバッグが数時間から数分になりました。**

**2. worklet は「絶対に外に漏れない」ように書く**

worklet の本体はすべて try/catch/finally で包み、catch と finally では SharedValue に書き込むか、そのまま握りつぶすだけ。例外を投げる可能性があるものは絶対に呼びません。`frame.dispose()` ですら保護が必要です。

結果を JS に返したい場合は SharedValue に書き込み、JS 側で 100 ミリ秒ごとにポーリングします。見た目はダサいですが、すべての runtime 間で安全に使える唯一の経路です。

ここは本当に、React Native でネイティブをやることを選んだ痛みだと言うしかありません

**3. ネイティブモジュールはフレームごとに例外を投げない**

Nitro のメソッドが一時的な失敗（このフレームはピントが合っていない、文字が検出されなかった）に遭遇したら、空の結果を返して、呼び出し側にそのフレームをスキップさせます。**毎フレーム境界をまたいで例外を投げていたら、いつかどこかのフレームで terminate されます**。

<br />

## TLDR

1. **カメラのパフォーマンスの第一原則は、仕事を減らすこと**：解像度を落とす、ROI を切り出す、GPU でできるなら GPU でやる
2. **自分のコードがどの runtime で動いているかを把握する**：同じ JS 関数でも、UI runtime では問題なく、async-runner ではアプリごと殺す
3. **クラッシュには自己紹介させる**：まず診断ツールを整えてから、バグを捕まえにいく。そうしないと `abort() called` を前に霊視するしかなくなる
4. **古いスマホは絶対に 1 台持っておく**：シミュレーターでも新しいスマホでも再現しない。A12 こそが正体を暴く照魔鏡。開発者が十分に貧乏で、iPhone XR しか買えなくて助かりました

カメラはまさに難易度・地獄ですが、這い上がってきたあとには、React Native の下で実際に何が起きているのか、理解が一段どころではなく深まりました。
