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

Flutter Webを「SDK」として配る — 50以上のゲームを支える埋め込みアーキテクチャ

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest. →

Flutter Webを「SDK」として配る — 50以上のゲームを支える埋め込みアーキテクチャ

FlutterKaigi 2026の登壇資料

Avatar for Masayuki Suda

Masayuki Suda

October 28, 2026

More Decks by Masayuki Suda

Other Decks in Technology

Transcript

  1. をををを SDK ををを ホストページ(Next.js ) • • • • 以以以以以以以以以以以

    以以以以 SDK 以以以以 以以以以以以以以以以以以 以以以以以以以以以 以以以 / 以以以以 をををを Flutter を mount() … dispose() 以以以以 / 以以以 3
  2. ををををを 1. 脳タイムとゲームの作り 2. 課題 — Flutter Web は「部品」になれない 3.

    解決1 — iframe 隔離ライフサイクル 4. 解決2 — Dart 本編 JS ブリッジの「Interface 」 5. 解決3 — npm 配布・配信最適化・描画コスト 6. 量産を支える開発体制 7. まとめと、Flutter チームへの要望 4
  3. Flutter をををををを • 以以以 JS 以以 Canvas 以以以以以 Flutter 以以以以以以以

    JS 以以以以 1 以以以 →以以以以以以以以以以以以以以以 • 以以 Dart 以以以以以以以以以以以以以 7
  4. をををををををを • Next.js ををををを 以以以以以以以 SDK 以以以以 Flutter をををを Flutter

    + Riverpod 22 以以 • SEO 以以以以 • 以以以 DB 以 Web 以以以以以以 Flutter Web 以 npm 以以以 8
  5. ををををををををををををををを • 以以以 • 以以以以以以 • 以以以以以以以以 以以以以以以 Flutter ををを

    Unity WebGL / Phaser / 以以 + UI + 以以以以以 Canvas 以 Widget 以以以以以 CustomPainter 以以以以 9
  6. ををををををを — をををを をををををを 3を Widget が描き、Notifier が状態を持ち、純粋な Dart が計算する

    presentation application domain _Board CustomPainter FruitSoroeController Notifier<FruitSoroeState> ををを 1 ををををを FruitSoroeBoard をををを FruitSoroeResolver 以 phase 以 board 以以以以以 → → 1 以以以以以以以以以以以 ゲームらしいのは真ん中だけ — 一気に計算した結果を Notifier が 1 段ずつ再生 11
  7. domain を 1ををををををををををををををを fruit_soroe_resolver.dart(抜粋) 1 sealed class FruitSoroeAct {} 2

    3 4 5 6 7 class FruitSoroeClearAct extends FruitSoroeAct { final FruitSoroeBoard afterCreate; // 弾ける瞬間の盤面 final FruitSoroeBoard afterCollapse; // 詰めた後の盤面 final int cascade; // 何連鎖目か } 8 class FruitSoroeShuffleAct extends FruitSoroeAct { 10 final FruitSoroeBoard board; // 並べ替えた盤面 11 } 9 絵のことは何も知らない。ただのデータ 12
  8. domain ををををををををををををををを fruit_soroe_resolver.dart(抜粋) 1 2 3 4 5 6 7

    8 while (!plan.isEmpty) { cascade++; final afterCreate = current.applyCreated(plan.created); final afterCollapse = afterCreate.collapse(plan.cleared, rng); acts.add(FruitSoroeClearAct(afterCreate, afterCollapse, cascade)); current = afterCollapse; plan = current.planMatches(); // 次のそろいを探す } 連鎖の核はこの while ループだけ 13
  9. domain ををををををををををををををを fruit_soroe_resolver.dart(抜粋) 1 2 3 4 5 6 7

    8 while (!plan.isEmpty) { cascade++; final afterCreate = current.applyCreated(plan.created); final afterCollapse = afterCreate.collapse(plan.cleared, rng); acts.add(FruitSoroeClearAct(afterCreate, afterCollapse, cascade)); current = afterCollapse; plan = current.planMatches(); // 次のそろいを探す } 1 手の連鎖が丸ごと「演出の台本」になる 14
  10. application をををを 1 ををををををを fruit_soroe_controller.dart(抜粋) for (final act in resolution.acts)

    { 2 switch (act) { 3 case FruitSoroeClearAct(): 4 ... // 弾ける → 降ってくる 5 case FruitSoroeShuffleAct(): 6 ... // 手詰まりの並べ替え 7 } 8 } 1 sealed なので case の書き漏れはコンパイルエラー 15
  11. application を state ををををををををを fruit_soroe_controller.dart(抜粋) 1 2 3 4 5

    6 7 8 9 10 11 case FruitSoroeClearAct(): state = state.copyWith( board: act.afterCreate, phase: FruitSoroePhase.clearing, // 弾ける ); await _delay(Timing.scaled(Timing.clear, act.cascade)); state = state.copyWith( board: act.afterCollapse, phase: FruitSoroePhase.falling, // 降ってくる ); await _delay(Timing.scaled(Timing.fall, act.cascade)); Widget は phase と盤面を描くだけ。順序を知らない 16
  12. application ををををををををををををを fruit_soroe_state.dart static Duration scaled(Duration base, int cascade) {

    2 if (cascade <= 1) return base; 3 final factor = 1 - 0.15 * (cascade - 1); 4 return base * (factor < 0.55 ? 0.55 : factor); 5 } 1 1 段ごとに 15% 短く、下限は 55% (消える演出 300ms → 165ms ) 17
  13. をををを React をををををををををををををを ホストページ(Next.js ) • mount → 以以 以以以

    → dispose 以以以 / 以以以以 npm 以以以以以以以以 • SPA 以以以 A→B • React 以以以以以以以以以 をををを Flutter を mount() … dispose() 以以以以 / 以以以 19
  14. をを① Flutter Web をををををををををををををををを • 以以以以以以以 1以以以 API 以以以 1以以以

    • 以以以以以以以 • 以以以 • → 以以以以以以以以以以 LateInitializationError DOM(flt -glass -pane) 以以以以以以以以以以以以 20
  15. をを② Flutter をををををををををををををををををを • Flutter 以 window.history • 以以以以 •

    → 以以以以以以以以以以以以 以以以以以 Next.js 以以以以以以以以以 21
  16. 1をををを 1をを iframe をををををををををををを hostElement • iframe 以以以以以以 (ホストのdiv )

    <iframe> same -origin same -origin • mount = 以以 以以以 • dispose = 以以 • 以以以以以以以 Flutter engine DOM(flt -glass -pane) / listeners / timers WebGL context dispose() = この箱ごと消す 24
  17. mount をををををををををををををををを ブリッジ注入がロードより先、が生命線 mount.ts(抜粋) 1 2 3 4 5 //

    配列は realm を跨げない (#496) bridge.config = win.structuredClone(config); win[bridgeGlobal] = bridge; // ← ロード「前」に注入 win._flutter = { buildConfig: {...} }; script.src = `${gameBaseUrl}flutter.js`; 25
  18. をををををを ready Promise ををををを mount.ts(抜粋) const engine = await init.initializeEngine({

    2 hostElement: doc.body, assetBase: gameBaseUrl }); 3 await engine.runApp(); 4 resolveReady(); // settled フラグで冪等化 1 27
  19. iframe ををををを Android をを GPU ををを • Android Chrome 以

    GPU 以以以以以以以以 • → CanvasKit 以以以以以以以以以以以以 • getContext 以 wrap 以以以以以以 • 以 WebGL 以以以以以以 = GPU 以以以以以以以以以以以以以以以以以以 28
  20. iframe ををををを Android をを GPU ををを mount.ts(抜粋) 1 2 3

    4 5 function dispose() { if (disposed) return; // 冪等 releaseWebglContexts(); // ★ Android Chrome 対策 iframe.remove(); } 以 WebGL コンテキスト = GPU で描く窓口。ブラウザ内の数に上限あり 29
  21. をををを iOS を React ををを • iOS 以以以以以以以 • StrictMode

    以以以以以以以 1をををををを → 3 以以以以 → 以以以以以以 32
  22. をををををををををををををををををををを • 以以以 = 以以以以以以以以以以 • SDK 以以以以以以以以以以以 • 以以以

    • SDK 以以以以以以以以以以以以 以 Callback 以以以以以以以 source of truth 以以以以 以以以以以以以以以以以以以以以以以以以 33
  23. をををを = window ををを Interface をををを • 以以以 → 以以以

    Next.js を config を渡す config • 以以以 → 以以以 window[bridgeGlobal] = Interface を TS ををを callback • TypeScript 以以以以以 callback を返す をををを Flutter / Dart を 35
  24. をををををををををををををを Dart をををを ホストの例外で落ちないように呼ぶ _host_bridge.dart(抜粋) 1 final raw = _bridge?.getProperty<JSAny?>(name.toJS);

    2 3 if (raw == null || !raw.typeofEquals('function')) return; 4 5 6 7 try { (raw as JSFunction).callAsFunction(null, arg); } catch (e, st) { SdkLog.error(...); } 36
  25. をををををををををををを Callback ををををを • SDK 以以以以以以以以以以以以以以以以以以以以 • 以以以以以以以以以以以以以以以以以以以以 • 以以

    onExit 以以以以以以以以以以以以以以以以以 以以以以以以以以以以以以以以以以以以以以以以以以以以 37
  26. 1をををを 1をを main をををををををををををををを • 1以以以 = 1 main =

    1 以以以以以以以以 • 以以以以以以以以以以以以以以以以以以以以 1以 38
  27. 1をををを 1をを main をををををををををををををを build_sdk.sh(抜粋) fvm flutter build web --release

    --wasm \ 2 -t lib/sdk_entrypoints/${game}_web.dart \ 3 --base-href "/${game}/" 1 39
  28. ををを npm をををを • @theoria -tech/brainlingo -sdk -web • 以以以

    • 以以以以 mount 以 TS + Flutter 以以以以 npm install 以以以以 41
  29. をををををををををを • Next.js 以 public/ 以以以以 以以以以以以 • immutable をををを

    max -age=0 304 以以以以以以以以以以 → immutable 以以以以以 以以以以以以以以以以以以以以以以以以以以以以以 Version 以以以以以 2を 404 S3 以以以以以以以以以以以以以以以 CloudFront の振り分け + S3 のメタデータ(抜粋) 1 2 3 // バージョン付きパスだけ S3 から、1年 + immutable path_pattern = "/games/*.*.*/*" "CacheControl": "public, max-age=31536000, immutable" 42
  30. CanvasKit を skwasm を CDN をををを • canvaskit/ 以以以以 gstatic

    以 CDN 以以以 • 以以 CDN 以以以以 • 以以以以以以以 skwasm 以以以 1以以以以以以以以以 43
  31. prefetch をををををををををををををををを 流す順番(共有資産が先) • prefetch 以 1以以以以以以以以以 • Safari 以以以

    以以以以以以以以 10 以以以以以 • 1以以以以以以以以以 → canvaskit.js → canvaskit.wasm → main.dart.js CanvasKit 以以以以以以以以以以 以以以以 • prefetch 以以以 2MB 以以以以以以 ready 以 250ms 以以以以以以以 44
  32. wasm ををををををを Chrome ををを 既定の allowList (Flutter 3.47.4 • blink

    Chrome / Edge / Android dart2wasm + skwasm 1.17M B webkit Safari / iOS 以以 dart2js + CanvasKit 2.17M B gecko Firefox dart2js + CanvasKit 2.17M B wasm 以 Chrome / Edge 以以 Safari 以 iOS 以 Flutter 以以以以以以 • ・転送 br ) 以以以以 以以 0.3MB 以以 (br) 以 1.48 → 1.17 MiB Safari / iOS は Flutter 公式が wasm 未対応 45
  33. ををををををを COOP/COEP ををををををを fruit_soroe 以以以以 をを 1以以 SwiftShader 以以以 100ms

    をををををを (Flutter 3.47.4) ををををををををを dart2js + CanvasKit 4以 400 –520 ms skwasm 以以以以以 2以 以 380 ms skwasm + COOP/COEP 0を — をををををををををををををををををををををををを COOP / COEP = Cross -Origin -Opener -Policy / Cross -Origin -Embedder -Policy 47
  34. をををををををををををを Flutter Web • 1以以以 = 1以以以以以以以以 = 1PR •

    以以以以 npm 以以以以以以 1以以以以 • 以以以以以以以以以 • 以以以以 22以以以以以以 main 以以 mount 以 847 以 50
  35. をををををををををををををををををををを をををををを ををををををををををを iframe 以以以以以 getContext 以 wrap 以以 以以以以以以以以以以以以以以以

    GPU 以以以以以 → 以以以以以 → dispose 以以 → Web 以以 view 以以以以以以以 API WebGL 以以以以以以以以以以以 Impeller 以以以以以以以 以以以以以以以以以以以以以以以以以以以以以以 52
  36. ををを ① ををををを をををををを iframe 以以以以以 + WebGL 以以以以 ②

    ををををををを ③ をををををををを Interface 以以以以 以 + 以以 + 以以以以以 npm 以以以 以以以以以以以以以以 immutable 公式 API が揃うまでは、この回避策で Flutter Web 以 を「部品」として使える 53