Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Vue.jsで作る空間解析アプリとCapacitorプラグインのData Bridge

Avatar for Hal Hal
October 24, 2026

Vue.jsで作る空間解析アプリとCapacitorプラグインのData Bridge

Vue Fes Japan 2026で登壇した際のスライドです

Avatar for Hal

Hal

October 24, 2026

More Decks by Hal

Other Decks in Programming

Transcript

  1. Vu e Fe s Ja p a n 2 0

    2 6 Vu e . j s で 作 る 空 間 解 析 ア プ リ と C a p a c i to r プ ラ グ イ ン の D a ta B r i d g e Hal
  2. Hal ソフトウェアエンジニア Vue.js / Nuxt / Hono / Photo /

    Cinema / Tacos どこにでもいる人になりつつある
  3. な ぜ We b フ ロ ン ト エ ン

    ド で ネイティブアプリを作るのか
  4. なぜ Web FW でネ イ ティブ ア プ リ? ネイティブな体験を提供したい

    今のスキル範囲から小さく始めたい デバイスのポテンシャルを引き出し、より立体的な体験を提供したい e.g. NFC ・ ARKit ・ Live Activity Web から呼べる API も増えてきたが、 iOS では使えないものもある( e.g. Web NFC ) 出典 : MDN 「 Web NFC API 」
  5. どのフレームワーク を選ぶ ? React Native JS エンジン JS ⇔ Native

    Vue で書け る? Web として も出せる? ネイティブ層 ( Hermes ) JSI ・ TurboModules NativeScript Lynx Capacitor ネイティブ層( V8 ) PrimJS ・ JSC (Dual Thread) WebView ( JSC ・ V8 ) NativeModules JSON Bridge ◯ ◯( PWA も) ランタイムが直接呼び出 し(メタデータ) ✕ ( React のみ) nativescript-vue vue-lynx ( コミュニティ OSS) react-native-web 自前で工夫が必要 Lynx for Web
  6. どのフレームワーク を選ぶ ? React Native JS エンジン JS ⇔ Native

    Vue で書け る? Web として も出せる? ネイティブ層 ( Hermes ) JSI ・ TurboModules NativeScript Lynx Capacitor ネイティブ層( V8 ) PrimJS ・ JSC (Dual Thread) WebView ( JSC ・ V8 ) ランタイムが直接呼び出 今回は Capacitor を採用 し(メタデータ) NativeModules JSON Bridge ◯ ◯( PWA も) ✕ ( React のみ) nativescript-vue vue-lynx ( コミュニティ OSS) react-native-web 自前で工夫が必要 Lynx for Web
  7. Capacitor とは WebView ベースのフレームワーク( Cordova の精神的後継) JS エンジンは WebView のもの(

    JavaScriptCore ・ V8 ) Vue.js ・ React ・ Svelte ・ Angular ・ Qwik などと組み合わせて使える Native ⇔ JS のデータ転送は JSON Bridge ← ここがポイント
  8. Capacitor を採 用 し た理由 Vue.js でネイティブアプリを書きたかった Swift ・ Kotlin

    に意識を向ける時間を減らしたかった プラグイン設計や接続部を作るときだけ集中する iOS ・ Android ・ Web でのアプリ配信が必要でも小さく始めやすい
  9. 空間解析アプリで 取 り 組 ん でいる こ と 収集 3D

    出力 図面出力 AR で空間情報を集める 高品質な 3D ( 3DGS ) 空間情報から図面へ (奥行・幅・高さ・カテゴ ライズ)
  10. 空間解析アプリで 取 り 組 ん でいる こ と 収集 AR

    で空間情報を集める (奥行・幅・高さ・カテゴ ライズ)
  11. そもそも LiDAR とは 光を当てて、返ってくるまでの時間で距離を 測る iPhone Pro は dToF (

    Direct Time-ofFlight ) 苦手:強い光・反射面・細かいエッジ 暗い場所も苦手( LiDAR は測れるが、 ARKit のトラッキングが落ちる) 取れるもの: 深度マップ 信頼度マップ RGB (カメラ映像として) 点群(深度の加工結果) iPhone 13 Pro の LiDAR 深度から作った点群
  12. 深度データの中 身 depthMap ( Float32 ):端末からその場所までの推定距離( m ) 行 列

    [ // 192 × 256 … [0.992, 0.995, 0.998, 1.000, 1.004, 1.008, 1.012, 1.016, …], [0.990, 0.994, 0.996, 0.999, 1.002, 1.006, 1.009, 1.013, …], [0.995, 0.998, 0.999, 1.002, 1.004, 1.011, 1.020, 1.021, …], [0.991, 0.992, 0.994, 0.997, 1.004, 1.007, 1.013, 1.021, …], [0.995, 1.000, 0.999, 1.004, 1.008, 1.009, 1.092, 1.057, …], [1.946, 2.695, 2.836, 2.922, 2.951, 2.885, 2.920, 2.967, …], … ] confidenceMap ( UInt8 ):その距離の確からしさ 拡大図の 6 × 8 マスが、右の配列に対応する。上 5 行は ドアの面(約 1m )、最後の行はスリットの奥(約 2.9m )で、境目に近づくほど信頼度が下がる。画像も深 度も、センサーの向き(横向き)のまま データ : iPhone 13 Pro で撮影 行 列。 [ // 192 × 256 0=low 1=medium 2=high … [2, 2, 2, 2, 2, 2, 2, 2, …], [2, 2, 2, 2, 2, 2, 2, 2, …], [2, 2, 2, 2, 2, 2, 2, 2, …], [1, 1, 1, 1, 1, 1, 1, 1, …], [1, 1, 1, 1, 1, 1, 1, 1, …], [0, 0, 0, 0, 0, 0, 0, 0, …], … ]
  13. 深度データの中 身 depthMap ( Float32 ):端末からその場所までの推定距離( m ) 行 列

    [ // 192 × 256 … [0.992, 0.995, 0.998, 1.000, 1.004, 1.008, 1.012, 1.016, …], [0.990, 0.994, 0.996, 0.999, 1.002, 1.006, 1.009, 1.013, …], [0.995, 0.998, 0.999, 1.002, 1.004, 1.011, 1.020, 1.021, …], [0.991, 0.992, 0.994, 0.997, 1.004, 1.007, 1.013, 1.021, …], [0.995, 1.000, 0.999, 1.004, 1.008, 1.009, 1.092, 1.057, …], [1.946, 2.695, 2.836, 2.922, 2.951, 2.885, 2.920, 2.967, …], … ] confidenceMap ( UInt8 ):その距離の確からしさ 拡大図の 6 × 8 マスが、右の配列に対応する。上 5 行は ドアの面(約 1m )、最後の行はスリットの奥(約 2.9m )で、境目に近づくほど信頼度が下がる。画像も深 度も、センサーの向き(横向き)のまま データ : iPhone 13 Pro で撮影 行 列。 [ // 192 × 256 0=low 1=medium 2=high … [2, 2, 2, 2, 2, 2, 2, 2, …], [2, 2, 2, 2, 2, 2, 2, 2, …], [2, 2, 2, 2, 2, 2, 2, 2, …], [1, 1, 1, 1, 1, 1, 1, 1, …], [1, 1, 1, 1, 1, 1, 1, 1, …], [0, 0, 0, 0, 0, 0, 0, 0, …], … ]
  14. 深度データの中 身 depthMap ( Float32 ):端末からその場所までの推定距離( m ) 行 列

    [ // 192 × 256 … [0.992, 0.995, 0.998, 1.000, 1.004, 1.008, 1.012, 1.016, …], [0.990, 0.994, 0.996, 0.999, 1.002, 1.006, 1.009, 1.013, …], [0.995, 0.998, 0.999, 1.002, 1.004, 1.011, 1.020, 1.021, …], [0.991, 0.992, 0.994, 0.997, 1.004, 1.007, 1.013, 1.021, …], [0.995, 1.000, 0.999, 1.004, 1.008, 1.009, 1.092, 1.057, …], [1.946, 2.695, 2.836, 2.922, 2.951, 2.885, 2.920, 2.967, …], … ] confidenceMap ( UInt8 ):その距離の確からしさ 拡大図の 6 × 8 マスが、右の配列に対応する。上 5 行は ドアの面(約 1m )、最後の行はスリットの奥(約 2.9m )で、境目に近づくほど信頼度が下がる。画像も深 度も、センサーの向き(横向き)のまま データ : iPhone 13 Pro で撮影 行 列。 [ // 192 × 256 0=low 1=medium 2=high … [2, 2, 2, 2, 2, 2, 2, 2, …], [2, 2, 2, 2, 2, 2, 2, 2, …], [2, 2, 2, 2, 2, 2, 2, 2, …], [1, 1, 1, 1, 1, 1, 1, 1, …], [1, 1, 1, 1, 1, 1, 1, 1, …], [0, 0, 0, 0, 0, 0, 0, 0, …], … ]
  15. Capacitor で ARKit を 扱 う 際 の 問 題

    点 公式のプラグインが存在しない Capacitor 公式の 38 プラグインに、 ARKit や LiDAR を扱うものはない 自作には Swift などネイティブ言語の知識が必要 カメラ映像を含む計測データが特大 JS 層での処理が現実的ではない 生データは 1 秒あたり約 260MB 。 Bridge は 100MiB を渡すだけで約 0.6 秒かかる 映像まで含めると、 Bridge では毎秒の量を送り切れない 出典 : Capacitor 「 Official Plugins 」・「 Custom Native iOS Code 」
  16. どれくらい重い ? iPhone 13 Pro ( LiDAR )でドアを 1 枚スキャン

    70 秒 → 約 420MB 動画 295MB ( HEVC )+ 深度 125MB ( DEFLATE )+ メタデータ 2MB ARKit の深度データは 60fps (毎フレーム取得) データ 1 フレームあたり 深度マップ sceneDepth.depthMap 256×192×4B ≒ 約 197KB 信頼度マップ sceneDepth.confidenceMap 256×192×1B ≒ 約 49KB RGB 映像 capturedImage 1920×1440 ・ YUV420 ≒ 約 4.1MB
  17. React Native : JSI で直 接 つ な ぐ 旧アーキテクチャは非同期ブリッジで呼び出

    しをシリアライズ → 大きいオブジェクトでボ トルネックに New Architecture では JSI で JS が C++ オブジェクトの参照を持ち、シリアライズな しで直接呼び出す e.g. VisionCamera は約 30MB のフレーム (約 2GB/s )を JSI で処理 図 : https://reactnative.dev/blog/2024/10/23/the-new-architecture-is-here 解説 : https://reactnative.dev/architecture/landing-page ( CC BY 4.0 )
  18. Flutter : Platform Channel と FFI Platform Channel :メッセージを StandardMessageCodec

    でバイナリにシリアライズ( Uint8List ⇔ Swift の FlutterStandardTypedData ) FFI ( dart:ffi ): C の API を直接呼ぶ。データの受け渡 しにシリアライズが不要で、 Platform Channel よりかなり速 くなりうる 図 : https://docs.flutter.dev/resources/architectural-overview 解説 : https://docs.flutter.dev/platform-integration/platform-channels ( CC BY 4.0 )
  19. NativeScript : JS か らネイ テ ィ ブ API を

    直 接 呼 ぶ JavaScript Application Code Plugins NPM Packages NativeScript Runtime Webpack Chrome DevTools Module Loader iOS ・ Android の API をそのまま呼べる JS Debugger V8 VM JS Workers LiveSync ビルド時に生成するメタデータで、 JS ( V8 )から NativeScript CLI 型の変換(マーシャリング)はランタイムが暗黙的 に行う 大きいデータは interop.bufferFromData() で Metadata / Type Marshaller @nativescript/ios FFI NSData を ArrayBuffer にラップして扱える Native iOS System Frameworks Obj-C runtime Native Deps 図 : https://docs.nativescript.org/dev-reference 解説 : NativeScript 「 Metadata 」「 Marshalling 」「 iOS Runtime Types 」
  20. Lynx :デュアルス レ ッドと NativeModules JS はメインスレッド( PrimJS )とバックグラウンドスレッド( iOS

    は JavaScriptCore )の 2 つで動く NativeModules は ArrayBuffer ⇔ NSData ( iOS )・ byte[] ( Android ) の型変換に対応 図 : https://lynxjs.org/react/main-thread-script.html 解説 : https://lynxjs.org/guide/scripting-runtime/index.html ・ https://lynxjs.org/guide/use-native-modules.html ( CC BY 4.0 )
  21. Capacitor : WebView と Native Bridge Web アプリは WebView 内で動き、

    Native Bridge がプラ グイン呼び出しをメッセージとして Native Runtime に渡す (呼び出しはすべて非同期) Web ⇔ Native のデータは JSON で表せる型にシリアライ ズ・デシリアライズされる → 大量のデータをやり取りするとシリアライズのオーバーヘ ッドが出る( Ionic 公式も言及) 図 : https://ionic.io/blog/how-capacitor-works-2 解説 : https://capacitorjs.com/docs/core-apis/data-types ・ https://ionic.io/blog/capacitor-everything-youve-ever-wanted-to-know
  22. まとめ:大きいデー タの通 り 道 JS ⇔ Native の通り道 大きいデータ ストリームで送れるか

    JSI ( C++ への直接バ 参照を持つのでシリア ライズなし △ イベント(バイト列は 0.87 から の ArrayBuffer 型か自作の JSI ) Flutter Platform Channel (バ イナリ)・ FFI FFI ならシリアライズ ◯ EventChannel (バイナリのま ま) NativeScript ランタイムがメタデータ で直接呼び出し NSData を ArrayBuffer にラップ NativeModules (デュ ArrayBuffer ⇔ Native Bridge ( JSON メッセージ) JSON にシリアライズ React Native Lynx Capacitor インディング) アルスレッド) なし NSData ・ byte[] → オーバーヘッド △ 専用の仕組みはない △ 専用の仕組みはない △ イベント・ readFileInChunks ( Bridge 経由) ストリームの出典 : React Native 「 Emitting Events in Native Modules 」・ Flutter 「 EventChannel 」 Lynx 「 Native Modules 」・ Capacitor 「 Filesystem 」
  23. まとめ:大きいデー タの通 り 道 React Native JS ⇔ Native の通り道

    大きいデータ ストリームで送れるか JSI ( C++ への直接バ 参照を持つのでシリア ライズなし △ イベント(バイト列は 0.87 から の ArrayBuffer 型か自作の JSI ) ◯ EventChannel (バイナリのま ま) インディング) Capacitor では 大きいデータを Bridge に流さない Flutter Platform Channel (バ イナリ)・ FFI FFI ならシリアライズ NativeScript ランタイムがメタデータ で直接呼び出し NSData を ArrayBuffer にラップ Lynx Capacitor 設計 が必要 ArrayBuffer NativeModules (デュ アルスレッド) なし Native Bridge ( JSON メッセージ) ⇔ NSData ・ byte[] JSON にシリアライズ → オーバーヘッド △ 専用の仕組みはない △ 専用の仕組みはない △ イベント・ readFileInChunks ( Bridge 経由) ストリームの出典 : React Native 「 Emitting Events in Native Modules 」・ Flutter 「 EventChannel 」 Lynx 「 Native Modules 」・ Capacitor 「 Filesystem 」
  24. Capacitor プラ グイン の 作 り 方 ① JS 側でインターフェースを定義

    import { registerPlugin } from '@capacitor/core'; export interface EchoPlugin { echo(options: { value: string }): Promise<{ value: string }>; } const Echo = registerPlugin<EchoPlugin>('Echo'); export default Echo; 出典 : https://capacitorjs.com/docs/ios/custom-code ( Apache-2.0 )
  25. Capacitor プラ グイン の 作 り 方 ① JS 側でインターフェースを定義

    import { registerPlugin } from '@capacitor/core'; export interface EchoPlugin { echo(options: { value: string }): Promise<{ value: string }>; } const Echo = registerPlugin<EchoPlugin>('Echo'); export default Echo; 出典 : https://capacitorjs.com/docs/ios/custom-code ( Apache-2.0 )
  26. Capacitor プラ グイン の 作 り 方 ② Swift 側でモジュールを宣言して実装

    import Capacitor @objc(EchoPlugin) public class EchoPlugin: CAPPlugin, CAPBridgedPlugin { public let identifier = "EchoPlugin" public let jsName = "Echo" public let pluginMethods: [CAPPluginMethod] = [ CAPPluginMethod(name: "echo", returnType: CAPPluginReturnPromise) ] @objc func echo(_ call: CAPPluginCall) { let value = call.getString("value") ?? "" call.resolve(["value": value]) } } 出典 : https://capacitorjs.com/docs/ios/custom-code ( Apache-2.0 )
  27. Capacitor プラ グイン の 作 り 方 ② Swift 側でモジュールを宣言して実装

    import Capacitor @objc(EchoPlugin) public class EchoPlugin: CAPPlugin, CAPBridgedPlugin { public let identifier = "EchoPlugin" public let jsName = "Echo" public let pluginMethods: [CAPPluginMethod] = [ CAPPluginMethod(name: "echo", returnType: CAPPluginReturnPromise) ] @objc func echo(_ call: CAPPluginCall) { let value = call.getString("value") ?? "" call.resolve(["value": value]) } } 出典 : https://capacitorjs.com/docs/ios/custom-code ( Apache-2.0 )
  28. Capacitor プラ グイン の 作 り 方 ② Swift 側でモジュールを宣言して実装

    import Capacitor @objc(EchoPlugin) public class EchoPlugin: CAPPlugin, CAPBridgedPlugin { public let identifier = "EchoPlugin" public let jsName = "Echo" public let pluginMethods: [CAPPluginMethod] = [ CAPPluginMethod(name: "echo", returnType: CAPPluginReturnPromise) ] @objc func echo(_ call: CAPPluginCall) { let value = call.getString("value") ?? "" call.resolve(["value": value]) } } 出典 : https://capacitorjs.com/docs/ios/custom-code ( Apache-2.0 )
  29. Control Plane ・ Data Plane と は もともとはネットワーク機器(ルーター・スイッチ)のアーキテクチャで使われる考え方 Control Plane

    パケットを「どう流すか」を決めて、 Data Data Plane ( For warding Plane ) Plane に指示する 指示に従って、パケットを実際に処理する e.g. 経路の選択・転送テーブルの調整 e.g. 転送・破棄・書き換え 出典 : RFC 7426 Section 2, 3.1, 3.3 https://datatracker.ietf.org/doc/html/rfc7426
  30. Control Plane ・ Data Plane と は もともとはネットワーク機器(ルーター・スイッチ)のアーキテクチャで使われる考え方 We b

    層 = C o n tData r o Plane l Plane ( For warding Plane ) Data Native 層 = Data Plane Control Plane パケットを「どう流すか」を決めて、 Plane に指示する 指示に従って、パケットを実際に処理する e.g. 経路の選択・転送テーブルの調整 e.g. 転送・破棄・書き換え と当てはめてみる 出典 : RFC 7426 Section 2, 3.1, 3.3 https://datatracker.ietf.org/doc/html/rfc7426
  31. 考え方 Web 層( Vue.js ) Control Plane Native 層( Swift

    ) ・ ① 指示だけ startSession updateCapture 進捗 captureProgress( 0.5 秒ごと) UI ・撮影ガイド Data Plane ARKit ・ LiDAR ② 逆投影・ボクセル化 ③ 点群データ dataplane://( 256KB ずつ) 点群ファイル
  32. ① 指示は Web 、 作 業は Native Swift の実装を都度書かず、プラグイン経由でネイティブ機能を操作する 進捗を

    0.5 秒ごとに受け取るよう登録し、撮影を始める handle = await SpatialCapture.addListener( 'captureProgress', (e) => { progress.value = e }, ) const r = await SpatialCapture.startSession(o) sessionId.value = r.sessionId 足りない視点を Web で判定し、撮り方だけを伝える const keyframe = next === 'sparse' ? { translation: base.translation * 3, rotationDeg: base.rotationDeg * 2.5 } : { translation: base.translation, rotationDeg: base.rotationDeg } await SpatialCapture.updateCapture({ keyframe })
  33. ① 指示は Web 、 作 業は Native Swift の実装を都度書かず、プラグイン経由でネイティブ機能を操作する 進捗を

    0.5 秒ごとに受け取るよう登録し、撮影を始める handle = await SpatialCapture.addListener( 'captureProgress', (e) => { progress.value = e }, ) const r = await SpatialCapture.startSession(o) sessionId.value = r.sessionId 足りない視点を Web で判定し、撮り方だけを伝える const keyframe = next === 'sparse' ? { translation: base.translation * 3, rotationDeg: base.rotationDeg * 2.5 } : { translation: base.translation, rotationDeg: base.rotationDeg } await SpatialCapture.updateCapture({ keyframe })
  34. ① 指示は Web 、 作 業は Native Swift の実装を都度書かず、プラグイン経由でネイティブ機能を操作する 進捗を

    0.5 秒ごとに受け取るよう登録し、撮影を始める handle = await SpatialCapture.addListener( 'captureProgress', (e) => { progress.value = e }, ) const r = await SpatialCapture.startSession(o) sessionId.value = r.sessionId 足りない視点を Web で判定し、撮り方だけを伝える const keyframe = next === 'sparse' ? { translation: base.translation * 3, rotationDeg: base.rotationDeg * 2.5 } : { translation: base.translation, rotationDeg: base.rotationDeg } await SpatialCapture.updateCapture({ keyframe })
  35. ② 重いデータは Native 内で 閉 じ る 重いデータの処理や負荷の高い計算は、できる限りネイティブ内で終わらせる Metal などを使う場合も同じ考え方を応用できる

    点群の生成は深度の逆投影からファイルの書き出しまで Swift で行い、 JS には点群ファイルの場所と点数などだけを返す @objc func buildPointCloud(_ call: CAPPluginCall) { // … work.async { [self] in do { // … let result = try DepthUnprojector.build(sessionDir: dir, frames: frames, condition: cond) { p in report(base + p * (1 - base), "points") } let lods = try store.writeLods(id, result: result, condition: cond) call.resolve(["lods": lods, "framesUsed": result.framesUsed, "samples": result.samples /* … */]) } catch { call.reject(error.localizedDescription) } } }
  36. ② 重いデータは Native 内で 閉 じ る 重いデータの処理や負荷の高い計算は、できる限りネイティブ内で終わらせる Metal などを使う場合も同じ考え方を応用できる

    点群の生成は深度の逆投影からファイルの書き出しまで Swift で行い、 JS には点群ファイルの場所と点数などだけを返す @objc func buildPointCloud(_ call: CAPPluginCall) { // … work.async { [self] in do { // … let result = try DepthUnprojector.build(sessionDir: dir, frames: frames, condition: cond) { p in report(base + p * (1 - base), "points") } let lods = try store.writeLods(id, result: result, condition: cond) call.resolve(["lods": lods, "framesUsed": result.framesUsed, "samples": result.samples /* … */]) } catch { call.reject(error.localizedDescription) } } }
  37. ② 重いデータは Native 内で 閉 じ る 重いデータの処理や負荷の高い計算は、できる限りネイティブ内で終わらせる Metal などを使う場合も同じ考え方を応用できる

    点群の生成は深度の逆投影からファイルの書き出しまで Swift で行い、 JS には点群ファイルの場所と点数などだけを返す @objc func buildPointCloud(_ call: CAPPluginCall) { // … work.async { [self] in do { // … let result = try DepthUnprojector.build(sessionDir: dir, frames: frames, condition: cond) { p in report(base + p * (1 - base), "points") } let lods = try store.writeLods(id, result: result, condition: cond) call.resolve(["lods": lods, "framesUsed": result.framesUsed, "samples": result.samples /* … */]) } catch { call.reject(error.localizedDescription) } } }
  38. ② 重いデータは Native 内で 閉 じ る 重いデータの処理や負荷の高い計算は、できる限りネイティブ内で終わらせる Metal などを使う場合も同じ考え方を応用できる

    点群の生成は深度の逆投影からファイルの書き出しまで Swift で行い、 JS には点群ファイルの場所と点数などだけを返す @objc func buildPointCloud(_ call: CAPPluginCall) { // … work.async { [self] in do { // … let result = try DepthUnprojector.build(sessionDir: dir, frames: frames, condition: cond) { p in report(base + p * (1 - base), "points") } let lods = try store.writeLods(id, result: result, condition: cond) call.resolve(["lods": lods, "framesUsed": result.framesUsed, "samples": result.samples /* … */]) } catch { call.reject(error.localizedDescription) } } }
  39. ③ JS に渡 すときは chunk す る 最後に JS へ送る巨大なデータも

    chunk して、描画への影響を抑える Native : dataplane:// で 256KB ずつ返す Web : ReadableStream で受け、 1 点 16B 単位で GPU へ 渡す while remaining > 0 { window.wait() let n = min(chunkSize, remaining) guard let data = try handle.read(upToCount: n), !data.isEmpty else { break } remaining -= data.count DispatchQueue.main.async { [weak self] in defer { window.signal() } // … task.didReceive(data) } } const res = await fetch(url, { signal: o.signal }) // … const reader = res.body.getReader() for (;;) { const { done, value } = await reader.read() if (done) break // … carry const usable = buf.byteLength - (buf.byteLength % 16) if (usable) o.onChunk(buf.subarray(0, usable)) if (usable < buf.byteLength) carry = buf.slice(usable) } (前回の端数 をつなぐ)
  40. ③ JS に渡 すときは chunk す る 最後に JS へ送る巨大なデータも

    chunk して、描画への影響を抑える Native : dataplane:// で 256KB ずつ返す Web : ReadableStream で受け、 1 点 16B 単位で GPU へ 渡す while remaining > 0 { window.wait() let n = min(chunkSize, remaining) guard let data = try handle.read(upToCount: n), !data.isEmpty else { break } remaining -= data.count DispatchQueue.main.async { [weak self] in defer { window.signal() } // … task.didReceive(data) } } const res = await fetch(url, { signal: o.signal }) // … const reader = res.body.getReader() for (;;) { const { done, value } = await reader.read() if (done) break // … carry const usable = buf.byteLength - (buf.byteLength % 16) if (usable) o.onChunk(buf.subarray(0, usable)) if (usable < buf.byteLength) carry = buf.slice(usable) } (前回の端数 をつなぐ)
  41. ③ JS に渡 すときは chunk す る 最後に JS へ送る巨大なデータも

    chunk して、描画への影響を抑える Native : dataplane:// で 256KB ずつ返す Web : ReadableStream で受け、 1 点 16B 単位で GPU へ 渡す while remaining > 0 { window.wait() let n = min(chunkSize, remaining) guard let data = try handle.read(upToCount: n), !data.isEmpty else { break } remaining -= data.count DispatchQueue.main.async { [weak self] in defer { window.signal() } // … task.didReceive(data) } } const res = await fetch(url, { signal: o.signal }) // … const reader = res.body.getReader() for (;;) { const { done, value } = await reader.read() if (done) break // … carry const usable = buf.byteLength - (buf.byteLength % 16) if (usable) o.onChunk(buf.subarray(0, usable)) if (usable < buf.byteLength) carry = buf.slice(usable) } (前回の端数 をつなぐ)
  42. Before ・ After 100MiB をネイティブから JS の Uint8Array へ渡す( Bridge

    は base64 、 Data Plane は Range 付きの fetch ) 時間 メモリ(アプリ+ WebView )
  43. 逆に転送方法を 変え ても速 く ならな か っ たも の サーバーからの取り込みは

    Data Plane の方が約 1 秒遅く、深度を毎秒 60 回送ったときは遅延も CPU も同じ だった サーバーから 100MiB を取り込む時間( iPhone 15 Plus ) LiDAR の深度( 1 フレーム約 246KB )を毎秒 60 回届けたときの遅延 p95 ( iPad Pro )
  44. 同じ発想が活きる 場 面 GPU ( WebGL ・ WebGPU ) ゲームの

    UI ( Coherent Gameface ) 指示: JS が描画の命令を出す 指示:画面は HTML ・ CSS ・ JS で作る データ:頂点やテクスチャは GPU に置いたまま データ:ゲームの状態は C++ 側が持つ CPU へ読み戻す readPixels などは、呼び出したスレッド C++ から出したモデルを、 data-bind-* 属性で画面に結び が止まるので本番では避ける( MDN ) つける XR ・ AR の 3D シーン( visionOS ) ネイティブの部品( Capacitor Google Maps ) 指示:アプリは UI と 3D の変更だけを送る 指示: JS の API で地図を作り、マーカーやカメラを動か データ:描画は別プロセスの render server が行う す Apple も、毎フレーム送る更新を減らすよう勧めている データ:地図はネイティブで描く( Android は WebView の下) このアプリの AR プレビューも同じ形 出典 : MDN 「 WebGL best practices 」・ Gameface 「 HTML Data Binding 」・ Capacitor 「 @capacitor/google-maps 」 Apple 「 Understanding the visionOS render pipeline 」「 Reducing the rendering cost of RealityKit content on visionOS 」
  45. Ask the Speaker ・懇 親 会 で 話 し た

    いこ と WebView で認証を受け渡す方法 空間解析・ 3D モデルの出力品質を上げ るためにやっていること 解析データの LLM ・ VLM 処理 3DGS