ClaudeExpo2026/05/06 15:30

Shipping a performance-first video feed: How Tendbble built a real-time social app with Expo

要点だけを先に読めるように短く再構成したセクションです。

元記事

Quick Digest

要約

要点だけを先に読めるように短く再構成したセクションです。

claudeja

Shipping a performance-first video feed: How Tendbble built a real-time social app with Expo の要約

Key Points

  • ポイント1: This is a guest post from Pierre Cangemi - a React Native and TypeScript specialist with over ten years of experience building mobile apps - and the Co-founder and CTO of Tendbble.
  • ポイント2: … Last week, a user told me the app "feels like it was built by a big team." We're just two frontend devs.
  • ポイント3: Working on one codebase.

Summary

この記事は 2026-05-06 に公開された「Shipping a performance-first video feed: How Tendbble built a real-time social app with Expo」の内容を日本語で簡潔にまとめたものです。

Key Points

  • ポイント1: This is a guest post from Pierre Cangemi - a React Native and TypeScript specialist with over ten years of experience building mobile apps - and the Co-founder and CTO of Tendbble.
  • ポイント2: … Last week, a user told me the app "feels like it was built by a big team." We're just two frontend devs.
  • ポイント3: Working on one codebase.

Full Translation

翻訳

原文の流れを保ったまま読める翻訳セクションです。

claudejamodel: claude-haiku-4-5

パフォーマンス重視のビデオフィードの配信:Tendbbleがどのようにして Expo でリアルタイム社会アプリを構築したか

これは、10年以上のモバイルアプリ開発経験を持つReact NativeおよびTypeScriptの専門家であり、Tendbbleの共同創業者兼CTOであるPierre Cangiemiからのゲスト投稿です。

先週、あるユーザーが「このアプリは大きなチームによって構築されたように感じる」とコメントしてくれました。私たちはフロントエンド開発者が2人です。1つのコードベースで作業しています。両方のプラットフォームに配信しています。数ヶ月前、以前のExpoの経験があっても、これを実現できるかどうか確信がありませんでした。

Tendbbleはメディアが豊富なソーシャルビデオ共有アプリで、数十億ドル規模の既存企業と直接競合しています。パフォーマンスが「あると良い」ではなく「製品そのもの」である種類の製品です。ユーザーは、スクロール、キャプチャ、トランジションのたびに、私たちをInstagram、Snapchat、TikTokと比較します。

アプリを限界まで押し上げる必要がありました:リアルタイムビデオフィード、ジェスチャー駆動のオーバーレイを備えたカスタムカメラ、両方のプラットフォームでの流動的なアニメーション、すべて1つのコードベースから、フロントエンド開発者2人で実現する必要がありました。

アプリでは、ユーザーは協調的で時間制限のある投稿で一緒に瞬間をキャプチャします。フィードはビデオの無限スクロールです。カメラは主要な作成サーフェスです。リアルタイムコメント、リアクション、位置情報共有、およびマップビューがすべてを結びつけます。すべての画面はアニメーション満載で、すべてのインタラクションは瞬時に感じられる必要があります。これが私たちが学んだことです。

チャレンジ:2つのプラットフォーム上のビデオ豊富なフィード

Tendbbleのコア体験は、ビデオ投稿のフィードをスクロールすることです。各カードには、HLSアダプティブストリーミングを備えたビデオを含めることができ、ユーザー情報、リアクション、コメントがオーバーレイされます。典型的なセッションでは、ユーザーは50~100の投稿をスクロールする可能性があります。

素朴なアプローチは、表示されているすべてのカードに対してビデオプレイヤーをマウントし、システムに処理させることです。iOSでは、これはしばらくの間機能します。ミッドレンジのAndroidデバイスでは、これは災害です。複数のビデオプレイヤーがデコードリソースを競い合うと、フレームドロップ、異なるビデオからのオーディオの重複、メモリの上昇が発生し、最終的にOSがアプリを強制終了します。

ビデオサブシステムは自動的に管理されないことを早期に学びました。ビデオフィードを構築している場合、何が再生されるか、いつ初期化されるか、いつ破棄されるかを決定するのはあなたである必要があります。

重要なアイデア:常に一度に1つのビデオ

私たちのブレークスルーは概念的にシンプルでした:アプリ全体で一度に1つのビデオだけが再生できるというグローバルルールを強制することです。画面ごとではなく、アプリ全体です。投稿がビューポートに入ると、再生を要求します。それ以前に再生されていたものは自動的に解放されます。この単一の制約により、オーディオの重複が完全に排除され、ピークメモリ使用量が半分に削減され、Androidでのフィードが劇的にスムーズになりました。

しかし、私たちはそこで止まりませんでした。また、決済遅延を追加しました。投稿が400ミリ秒間表示されるまで、ビデオプレイヤーを作成しません。高速スクロール中、投稿はプレイヤー初期化のコストを正当化するほど十分に長く表示されません。blurhashプレースホルダー付きのサムネイルがギャップをカバーします。ユーザーは気づきません。

プリロードについては、タイトに保ちます:スクロール方向に1つ先の投稿、後ろに0つ。expo-videoの組み込みHLSサポートと組み合わせると、再生は瞬時に感じられ、メモリに負荷をかけません。

20~30のアクティブなビデオサブスクリプションから3~4に削減しました。

カメラ体験の構築

カメラはTendbbleでユーザーがコンテンツを作成する場所です。彼らは写真とビデオをキャプチャし、ジェスチャー駆動のポジショニングと回転を備えたテキストオーバーレイを適用し、協調的な投稿に共有します。ネイティブカメラアプリと同じくらい高速で応答性が高く感じられる必要がありました。それがユーザーが私たちを測定する基準だからです。

3つの特定の理由でreact-native-vision-cameraをexpo-cameraの上に選択しました:細粒度のコーデック選択(iOSではH.265、AndroidではH.264)、ジェスチャー駆動のズームシステムを備えた複数の物理レンズへのアクセス、キャプチャ中のメモリを大幅に削減するバッファ圧縮オプション。

カメラ画面には、押下時間に基づいて写真とビデオを区別するキャプチャボタンが含まれています:写真の場合はタップ、ビデオの場合は長押しで、Skiaレンダリングされた進捗リングが残りの記録時間を表示します。ダブルタップでカメラを反転します。ピンチジェスチャーはズームを制御し、0.5x、1x、2xでマイルストーンスナップします。

テキストオーバーレイについては、写真とビデオにキャプションを合成するカスタムExpoネイティブモジュールを構築しました。キャプションエディターを使用すると、ユーザーはジェスチャーでテキストをドラッグ、回転、スケーリングでき、ネイティブモジュールはそれらのオーバーレイを最終メディアに完全な解像度で焼き込みます。これにより、多くのアプリが頼る画面キャプチャベースのアプローチの品質低下を回避します。

見つけるのに数週間かかったメモリリーク

配信後、何か問題があることに気づきました。カメラを繰り返し開いたり閉じたりしたユーザーは、アプリが遅くなり、最終的にクラッシュするのを見ました。メモリが上昇し、戻ってきませんでした。Instrumentsでプロファイリングすると、犯人が明らかになりました:react-native-vision-camera v4.7.3は、ビューが階層から削除されたときにAVCaptureSessionを停止しません。React Navigationは、バック ナビゲーションのパフォーマンスのためにメモリ内のスクリーンを生きたままにします。それらを即座に割り当て解除しません。したがって、カメラはバックグラウンドで見えないまま実行され続け、ユーザーが移動したにもかかわらず、メモリとGPUリソースを消費していました。

ライブラリは、ビューが階層を離れたときのクリーンアップがなく、キャプチャセッションを停止する初期化解除がありませんでした。

patch-packageで修正し、2つの単純なフックを追加しました:1つはビューが親から削除されたときにセッションを停止し、もう1つはセーフネットとして初期化解除時にセッションを停止します。

Vision Camera修正前

Vision Camera修正後

レッスンは修正よりも広かったです:ネイティブリソースは、期待するようにReactのコンポーネントライフサイクルに従いません。React Navigationでネイティブカメラ、ビデオ、またはオーディオライブラリを使用している場合、スクリーンがフォーカスを失ったときに実際に何が起こるかを監査してください。答えはあなたを驚かせるかもしれません。

Reanimated と Skia で 60fps を獲得する

アプリ全体で200以上のファイルでReanimated v4を使用しています。フィードのモードスイッチャーからリアルタイムカウントダウンタイマーまで、モザイクタイルエディターまで、すべてを強化します。React Native Skiaとカスタムレンダリングを組み合わせると、数百人のエンジニアによってサポートされているアプリのポーランドに一致するインタラクションを構築するためのツールが得られます。

私たちが学んだ最も重要なレッスン:UIスレッドは神聖です。アニメーションがJSスレッドに依存するたびに(たとえ短時間でも)、JSスレッドがデータをフェッチしたり、ナビゲーション遷移を処理したり、ビジネスロジックを実行したりしているときにフレームドロップのリスクがあります。

リアルタイムカウントダウンタイマーは良い例です。ライブクロック、進捗リング、カウントダウンとショットカウント間の切り替えを表示し、すべてフレームごとに更新されます。これのいずれもReactの再レンダリングをトリガーしません。システム全体はReanimated workletとフレームコールバックを使用してUIスレッド上で実行されます。テキスト更新はTextInputのアニメーション化されたプロップを通じて行われ、Reactを完全にバイパスします。結果は、JSスレッドが高負荷の下にあっても完全にスムーズなアニメーションです。

ジェスチャー構成:タップ、長押し、ドラッグが共存する必要がある場合

これらのジェスチャーが競合なく共存するようにするには、慎重な構成が必要でした。タップジェスチャーを同時の長押し+パン組み合わせに対してレース化し、手動アクティベーション、パンは長押しが発火した後にのみアクティベートされます。ハプティックフィードバックはレート制限され、高速ジェスチャー更新中にオーディオスレッドが過負荷になるのを防ぎます。

手動アクティベーションゲートを備えた単純なジェスチャーを複雑なジェスチャーに対してレース化することは、アプリ全体で再利用可能であることが判明しました。同じアプローチが、スワイプ可能なモードセレクターとドラッグ対応モーダルを強化します。

CSSができないことのためのSkia

Skiaを選択的に使用し、ビューの代替ではなく、不可能またはぎこちないレンダリングのために使用します。処理カード上の回転グラデーション境界線は、Reanimated共有値によって駆動されるSkia掃引グラデーション(JSの関与なしで60fps回転)です。Squircle形状は、CSSボーダー半径が複製できない種類の滑らかなコーナーのために手続き的に生成されたベジェパスを使用します。キャプションエディター内のテキストオーバーレイは、デュアルパスレンダリング、ストロークレイヤーの下に塗りつぶしレイヤーを使用し、キャプションがビデオの背景上で明確に読み取れるようにします。

重要な洞察:レンダリングにはSkiaを、値を駆動するにはReanimatedを使用します。2つは自然に構成されます。Reanimatedはタイミング、補間、ジェスチャー応答を提供します。Skiaは視覚的な出力を提供します。どちらも他方の仕事をしようとしません。

React Compiler:無料のパフォーマンスブースト

React 19とReact Compilerを採用すると、ほぼゼロの労力でパフォーマンスが向上しました。コンパイラーは自動的にメモ化を処理し、コードベース全体に散在する手動のuseMemo、useCallback、またはReact.memoはもうありません。ほとんどの手書きメモ化を削除し、コンパイラーに引き継がせました。

影響は、高速スクロールと画面遷移中に最も顕著でした。不要な再レンダリングが以前はフレームドロップに複合していた場所です。コンパイラーは、手動で追跡していたパフォーマンスバグの全体的なカテゴリーを排除しました。すべてのミリ秒のJSスレッド時間が重要なメディア豊富なアプリの場合、そのヘッドルームは重要です。

トレードオフは規律です:コンポーネントとフックはReactの純粋性ルールに厳密に従う必要があります。レンダリング中の副作用なし、プロップまたは状態の変異なし。私たちはすでにこれらのパターンに従っていたため、移行はスムーズでしたが、より緩い規約を持つチームはいくつかのクリーンアップ作業を期待する必要があります。

プラットフォーム固有のパフォーマンスバジェット

初期段階では、両方のプラットフォームに対して1つのアニメーションバジェットをターゲットにする間違いを犯しました。ProMotionディスプレイを備えたiOSデバイスは120fpsでレンダリングできます。多くのAndroidデバイスは複雑なアニメーション中に60fpsを保つのに苦労しています。

現在、プラットフォーム固有のパフォーマンス構成を維持しています。iOSでは、チューニングされた減衰と剛性を備えたスプリング物理学、積極的なプリロード、120fpsターゲットのアニメーションタイミングを使用します。ローエンドのAndroidでは、スプリングアニメーションを完全に無効にし、ジェスチャースロットル間隔を増やし、より単純なイージング曲線を使用し、リストウィンドウサイズを削減します。

アプリは両方のプラットフォームで応答性を感じますが、ハードウェアがサポートできないバジェットに達しようとしてAndroidでフレームをドロップしていません。これはマインドセットの変化でした。プラットフォームごとに異なるアニメーション品質を配信することは妥協ではなく、優れたエンジニアリングです。

見えない仕事:オフラインファーストとメモリ圧力

署名付きURLとキャッシュ永続性

クエリキャッシュを24時間の最大年齢で永続化し、ユーザーはアプリをオフラインで開いてもフィードを見ることができます。ただし、メディアURLはAWS署名付きURLで、1時間のTTLです。一晩のバックグラウンド後、アプリは期限切れのURLを含む1日古いキャッシュデータを再水和します。フィードはレンダリングされますが、すべての画像とビデオが壊れており、灰色のプレースホルダーがあります。

修正は再水和時にクエリの鮮度をチェックします。キャッシュされたデータが45分以上古い場合、すべてを無効にして強制的に再フェッチします。古いレイアウトはスケルトンとして簡潔に表示されますが、新しいデータが読み込まれている間、壊れたメディアよりもはるかに優れています。シンプルなソリューションですが、バグは長いバックグラウンド期間の後にのみ現れました。これは開発中に捕捉するのを難しくしました。

メモリ圧力管理

メディア豊富なアプリの場合、メモリ管理はオプションではありません。ネイティブメモリ警告に応答する優先度ベースのクリーンアップシステムを構築しました。ビデオバッファが最初にクリアされ、次に画像デコードキャッシュ、次にクエリキャッシュが、UIブロッキングを防ぐために遅延でスタッガーされます。アプリのバックグラウンドでは、クリーンアップを積極的にトリガーします。長いスクロールセッション中に、expo-imageのデコードされたビットマップキャッシュを定期的にフラッシュして、蓄積を防ぎます。

次に進む場所

体験をさらに押し上げるために、いくつかの領域を探索しています:

  • フィードカードと詳細ビュー間の共有要素遷移。インフラストラクチャはReanimatedで有効になっていますが、まだ完全には活用されていません
  • スクロール速度予測を使用したスマーターなビデオプリロード。遅いブラウジング中により積極的にプリフェッチし、高速フリック中は少なくする
  • アプリキル後に確実に再開するバックグラウンドアップロードキュー。expo-task-managerに基づいて構築
  • エッジキャッシュされたメディア。署名付きURL依存性を削減し、コールドスタートパフォーマンスを向上させる

最後に

この過去の最大のレッスン