Expo Modulesを使った、より高速で信頼性の高い動画アップロード
Users • React Native • Development • 2025-10-28 • 3分で読めます
ゲスト投稿:Petr Chalupa(SRTVのソフトウェアエンジニア、React Pragueオーガナイザー)
Boomは動画を軸としたコンペティションプラットフォームです(App Store、Google Play)。クリエイターは毎月11のカテゴリに映像を投稿し、ユーザー投票や審査員による選出で各カテゴリの勝者が決まり、賞金が授与されます。Boomにおいては、アップロードは単なる副機能ではなく、クリエイターがコンテストに参加するための主要な操作です。そのため、信頼できてストレスのないアップロード体験が非常に重要です。
課題:モバイルでの大容量動画アップロード
初期実装はシンプルなJavaScript製のアップローダーでした。MVPには十分でしたが、スケールさせるといくつかの重大な欠点が露呈しました:
- バックグラウンド実行の欠如:JavaScriptの実行はアプリのライフサイクルに紐づいており、アプリがバックグラウンドになるとOSがアップロードを停止する可能性があります。
- ネットワーク中断の取り扱い:一度のネットワークの途切れでアップロード全体が失敗し、ユーザーに再実行を強いることがありました。
目標:高速で信頼できるモバイル動画アップロード
目標は次のとおりです:
- 速さ:ユーザーの端末からサーバーへできるだけ速く動画を転送する。
- 信頼性:アプリの切り替え、ネットワークの断続、その他の中断が発生してもアップロードが継続する。
解決策:Expo Modulesを用いたネイティブのバックグラウンドアップロード
我々はExpo Modulesを使って全く新しいアップロードパイプラインを構築しました。SharedObjectを用いることで長時間生存する状態を持ったアップロードタスクを実現し、AWS S3のmultipart uploadに切り替えることで大容量ビデオのアップロードをより高速かつ信頼性の高いものにしました。
以下は我々のUploadTaskのTypeScriptインターフェースです。
export type UploadTaskEvents = {
onProgress : ( params : { progress : number } ) => void
}
export declare class UploadTask extends SharedObject<UploadTaskEvents> {
constructor ( clipPath : string , coverPath : string )
readonly parts : URL[]
clipUrls? : string[]
completionUrl? : string
coverUrl? : string
preProcess() : void
upload() : Promise<void>
}
次に、iOSネイティブモジュールがUploadTaskクラスをJavaScript側に公開している例です。
import ExpoModulesCore
public class BackgroundUploadModule : Module {
public func definition() -> ModuleDefinition {
Name("BackgroundUpload")
Class(UploadTask.self) {
Constructor { (clip: URL, cover: URL) -> UploadTask in
return UploadTask(clip: clip, cover: cover)
}
Property("parts") { uploadTask in uploadTask.clip.parts }
Property("clipUrls") { uploadTask in uploadTask.clip.uploadUrls }
.set { (uploadTask: UploadTask, uploadUrls: [URL]) in
uploadTask.clip.uploadUrls = uploadUrls
}
Property("completionUrl") { uploadTask in uploadTask.clip.completionUrl }
.set { (uploadTask: UploadTask, completionUrl: URL) in
uploadTask.clip.completionUrl = completionUrl
}
Property("coverUrl") { uploadTask in uploadTask.cover.uploadUrl }
.set { (uploadTask: UploadTask, uploadUrl: URL) in
uploadTask.cover.uploadUrl = uploadUrl
}
Function("preProcess") { uploadTask in try uploadTask.preProcessAssets() }
AsyncFunction("upload") { uploadTask in try await uploadTask.upload() }
}
}
}
React Nativeとの統合例
下はReact Nativeでの統合を簡略化したコード例です。ネイティブモジュール側でチャンク分割、並列度、リトライ、バックグラウンド実行を扱います。
const uploadTask = new BackgroundUpload.UploadTask(videoUri, thumbnailUri)
const progressListener = uploadTask.addListener('onProgress', ({ progress }) => {
updateProgress({ uploadProgress: progress })
})
uploadTask.preProcess()
const { data: uploadInfo } = await createUploadUrls({
variables: { input: { partsCount: uploadTask.parts.length } },
})
uploadTask.clipUrls = uploadInfo.clip.uploadUrls.map(({ uploadUrl }) => uploadUrl)
uploadTask.completionUrl = uploadInfo.clip.completionUrl
uploadTask.coverUrl = uploadInfo.cover.uploadUrl
try {
await uploadTask.upload()
} catch (error) {
logError(error)
} finally {
progressListener.remove()
}
結果:より高速で小さく、信頼できるアップロード
- 速度:100〜300MBのクリップにおけるエンドツーエンドの中央値が約20%改善しました。
- 信頼性:最近のテスト実行では“停止した”アップロードは観測されませんでした。
- 測定方法:end‑to‑end = tap Agree and Post → backend confirms completion
結論:ネイティブアップロードを追加するならExpo Modulesが最適
大容量メディアのアップロードのような高性能で回復力が必要な機能には、JavaScriptとネイティブコードを組み合わせたハイブリッドアプローチが最適です。Expo Modulesはこれらの機能を構築するのに非常に適しており、TurboModulesと比べて保守が容易でボイラープレートが少なく、Expoプロジェクトとの統合もスムーズです。長期的な保守コストを下げつつ、ネイティブの性能を必要な箇所で享受できるため、投資の効果としてより滑らかなユーザー体験と堅牢なアプリが得られました。