Linear ArchiveArchived issues viewer
← Back to list
INS-864

ガチャメモリリーク 実機解析ツール検討

StatusDone
TeamInstansys
Assigneeasuki.uehata@instansys.co.jp
PriorityHigh
Created2026/06/17 09:48
Completed2026/06/18 05:02
Archived2026/06/26 01:08

Description

Audio メモリリークの解析では、実機解析時 Safari スニペットに 以下 コマンドを実行することで、個別ファイルレベルでのメモリリークを特定できた Object.keys(window.__pixiSound._sounds)

これの倣って、画像のメモリリークの特定をしやすいように同様の解析ツールを作り、ガチャの特定の演出論理の問題点の改修に努める

Comments (18)

asuki.uehata@instansys.co.jp2026/06/17 09:50

メモリリーク/GPU圧迫の容疑者と所見 をまとめます。

1. 最有力: useImageCachedata URL 100件キャッシュ

useImageCache.ts:6-32

  • グローバル Map<string, string>base64 data URL を最大100件保持。data URL は元ファイル比 ~1.3倍の文字列 → JS ヒープ常駐。
  • それを <img src="data:..."> で描画 → iOS WebKit が デコード済みビットマップ を別途内部キャッシュ。1枚 2MB の webp が 1024×1024 RGBA だと展開後 4MB+。100枚 = JSヒープ + decoded bitmap で合計数百MB級になりうる。
  • clearImageCache はあるが演出のタイミングでは呼ばれていない(ガチャ画面遷移時のフックなし)。
  • 改善案: data URL → Blob + URL.createObjectURL に変更し、cache eviction 時に revokeObjectURL。decoded bitmap の重複も解消できる。

2. 演出 DOM 要素数が異常に多い

CharacterObtainedOverlay.tsx:209,224,261

  • 1キャラ表示ごとに:
    • 回転光線 BURST_RAY_COUNT=55
    • 集中線 FOCUS_STREAK_COUNT=84 × 3層(back/front/前面)+ FRONT_FOCUS_STREAK_COUNT=26 = 計 278本
    • 粒系 sparkles 16+30 / suck 44
    • 合計 ~420 個の DOManimation: 付きで同時稼働
  • 各要素が filter: blur(...) / mix-blend-mode: screen / radial-gradient / mask-image を併用 → iOS WebKit でほぼ全てが GPU コンポジションレイヤーに昇格
  • GachaAnimation.tsx:68,85 key={revealIndex}完全再マウント → 10連は 420要素 × 10回の作成/破棄。古いレイヤーの GPU テクスチャ解放が間に合わないと WebGL context lost を誘発しうる。

3. 軽微な蓄積

  • useLayeredImagePreload.ts:15loadedUrls Set は上限なし(ただし文字列のみで軽い)。
  • CharacterObtainedOverlay.tsx:517-522 new Image() の cleanup が onload = null のみで src = "" していない — iOS で decoded bitmap が短時間残る。
asuki.uehata@instansys.co.jp2026/06/17 09:53

まず観測を入れて仮説を裏取りしたい

A. useImageCache の状態を公開

// useImageCache.ts 末尾に追加
if (typeof window !== "undefined") {
  (window as any).__imageCache = {
    map: imageCache,
    sizeBytes: () => [...imageCache.values()].reduce((s, v) => s + v.length, 0),
  };
}

これで window.__imageCache.sizeBytes() がガチャ前後でどれだけ増えるか / 100件上限に張り付いているかが見えると思われる

asuki.uehata@instansys.co.jp2026/06/18 02:51

ログ差し込み

// 画像メモリリーク調査用に imageCache を公開する。INS-862 で導入。
// data URL を文字列として保持しているので、件数だけでなく累計バイト数を見たい。
// 使用例:
//   window.__imageCache.size()                                   // 件数(最大100)
//   window.__imageCache.sizeBytes()                              // data URL 文字列の累計バイト数
//   (window.__imageCache.sizeBytes() / 1024 / 1024).toFixed(2)   // MB 表記
//   window.__imageCache.keys()                                   // キャッシュ中のパス一覧
//   window.__imageCache.top(5)                                   // サイズ上位 N 件 [{key, bytes}]
//   // ガチャ前後の差分:
//   const before = window.__imageCache.sizeBytes();
//   // …10連ガチャ…
//   window.__imageCache.sizeBytes() - before;                    // 増分(byte)
if (typeof window !== "undefined") {
  (
    window as unknown as {
      __imageCache?: {
        size: () => number;
        sizeBytes: () => number;
        keys: () => string[];
        top: (n?: number) => { key: string; bytes: number }[];
        clear: () => void;
      };
    }
  ).__imageCache = {
    size: () => imageCache.size,
    sizeBytes: () =>
      [...imageCache.values()].reduce((sum, v) => sum + v.length, 0),
    keys: () => [...imageCache.keys()],
    top: (n = 10) =>
      [...imageCache.entries()]
        .map(([key, v]) => ({ key, bytes: v.length }))
        .sort((a, b) => b.bytes - a.bytes)
        .slice(0, n),
    clear: clearImageCache,
  };
}
asuki.uehata@instansys.co.jp2026/06/18 02:52

データ取得内容

// 1) 起動直後(ログイン後ホーム)
window.__imageCache.size()
window.__imageCache.sizeBytes()
window.__imageCache.top(10)

// 2) ガチャ画面に遷移直後
window.__imageCache.size()
window.__imageCache.sizeBytes()

// 3) 10連ガチャの直前にスナップショット
const snap1 = {
  count: window.__imageCache.size(),
  bytes: window.__imageCache.sizeBytes(),
  keys: new Set(window.__imageCache.keys()),
};

// 4) 10連ガチャ演出を最後まで再生し終わった直後
const snap2 = {
  count: window.__imageCache.size(),
  bytes: window.__imageCache.sizeBytes(),
  keys: window.__imageCache.keys(),
};
const delta = {
  countDelta: snap2.count - snap1.count,
  bytesDelta: snap2.bytes - snap1.bytes,
  bytesMBDelta: ((snap2.bytes - snap1.bytes) / 1024 / 1024).toFixed(2) + " MB",
  newKeys: snap2.keys.filter(k => !snap1.keys.has(k)),
};
console.table(delta);
console.table(window.__imageCache.top(10));

// 5) 10連ガチャを 3回繰り返した直後(蓄積を見る)
window.__imageCache.size()      // 100張り付きならLRU evict 発生 = 容量超過の証拠
window.__imageCache.sizeBytes()

ホーム画面

window.__imageCache.size()

< 0

window.__imageCache.sizeBytes()

< 0

window.__imageCache.top(10)

< [] (0)

[preload] unloadBundle done: "home" (released 0 images, 9 sounds)

[Log] [cache-metrics] preload "gacha": 7件 1514ms (cumulative cacheHit=0 network=0 0KB) (index-P4Wb1vOZ.js, line 16)

ガチャ画面に移動後

window.__imageCache.size()

< 0

window.__imageCache.sizeBytes()

< 0

"想戦の結晶" がでてしばらくして

document.querySelectorAll('[style*="animation"]').length

< 190

window.__pixiApp?.renderer?.texture.managedTextures.length

< undefined

window.__pixiAssets.cache._cache.size

< 6

セリナの勲章

document.querySelectorAll('[style*="animation"]').length

< 191

青ヘパイス

document.querySelectorAll('[style*="animation"]').length

< 240

WebGL: context lost.

ここでBGM停止

App タップがきかない

skip がなんとかできた。

console.table(delta);

[Log] (インデックス)

0

1

2

3

4

5

6

7

newKeys

"items/battle_memory_fragment.webp"

"items/peridot.webp"

"characters/selina/icon.2a427cd9d2.webp"

"equipments/accessory/accessory-powerring.webp"

"items/adamantite.webp"

"items/blue_hephice.webp"

"equipments/accessory/accessory-ring-of-rain.webp"

"items/magic_ore.webp"

console.table(window.__imageCache.top(10));

0

"items/blue_hephice.webp"

147459

1

"items/magic_ore.webp"

73879

2

"items/peridot.webp"

68207

3

"items/adamantite.webp"

58231

4

"characters/selina/icon.2a427cd9d2.webp"

20671

5

"equipments/accessory/accessory-ring-of-rain.webp"

10031

6

"equipments/accessory/accessory-powerring.webp"

8807

7

"items/battle_memory_fragment.webp"

3415

10連ガチャ終了後サマリー表示時点

window.__imageCache.size()

< 8

window.__imageCache.sizeBytes()

< 390700

document.querySelectorAll('[style*="animation"]').length

< 195

document.querySelectorAll('[style*="animation"]').length

< 195

window.__pixiApp?.renderer?.texture.managedTextures.length

< undefined

window.__pixiAssets.cache._cache.size

< 6

つづけて20連目開始時点 女神の3選択肢 で以下増加が確認できた。

document.querySelectorAll('[style*="animation"]').length

< 350

window.__pixiApp?.renderer?.texture.managedTextures.length

< undefined

window.__pixiAssets.cache._cache.size

< 6

ジャジャム 新キャラ登場

document.querySelectorAll('[style*="animation"]').length

< 843

window.__pixiApp?.renderer?.texture.managedTextures.length

< undefined

window.__pixiAssets.cache._cache.size

< 6

asuki.uehata@instansys.co.jp2026/06/18 02:52
(1) body 直下の要素ごとに、アニメ要素数をカウント — portal root 累積を見る

console.table(
  [...document.body.children].map(c => ({
    tag: c.tagName,
    cls: (c.className || "").toString().slice(0, 80),
    children: c.children.length,
    anim: c.querySelectorAll('[style*="animation"]').length,
  }))
);

0
"DIV"
""
3
195
1
"DIV"
"fixed inset-0 z-[99999] pointer-events-auto"
14
648

// (2) アニメ要素のクラス別件数 — どの演出パーツが累積してるか
const counts = {};
document.querySelectorAll('[style*="animation"]').forEach(e => {
  const k = (e.className || "(no-class)").toString().slice(0, 60);
  counts[k] = (counts[k] || 0) + 1;
});
console.table(
  Object.entries(counts).sort((a,b) => b[1]-a[1]).slice(0, 20)
    .map(([cls, n]) => ({ cls, n }))
);

0
"absolute"
490
1
"absolute rounded-full"
157
2
"absolute left-1/2 top-1/2"
120
3
"w-full h-full"
30
4
"absolute left-1/2 top-1/2 rounded-full"
20
5
"relative aspect-square h-[calc(20.37_*_var(--gvh))]"
10
6
"absolute rounded-full pointer-events-none z-[15]"
2
7
"h-full w-auto object-contain drop-shadow-[calc(0.8*var(--gvh"
2
8
"absolute inset-0 overflow-hidden pointer-events-none"
1
9
"absolute inset-0"
1
10
"h-[160%] w-[65%] absolute left-[0%] top-[0%] z-0"
1
11
"absolute inset-0 flex items-start justify-center drop-shadow"
1
12
"absolute z-[50] rounded-full pointer-events-none"
1
13
"absolute inset-0 z-[50] bg-white"
1
14
"absolute inset-x-0 bottom-0 pointer-events-none z-[47]"
1
15
"absolute inset-x-0 top-0 pointer-events-none z-[47]"
1
16
"absolute z-30 pointer-events-none flex flex-col items-center"
1
17
"w-[13.125%] h-full flex flex-col items-center justify-center"
1
18
"flex-1 h-full flex items-center justify-start whitespace-now"
1
19
"w-full h-[7.95%] flex items-center justify-center font-mplus"
1

// (3) ガチャ系コンテナの生存数
({
  gachaAnimation: document.querySelectorAll('[data-testid="gacha-animation"]').length,
  fixedFull: document.querySelectorAll('.fixed.inset-0').length,
  highZ: [...document.querySelectorAll('*')].filter(e => {
    const z = parseInt(getComputedStyle(e).zIndex, 10);
    return !isNaN(z) && z >= 1000;
  }).length,
})
{gachaAnimation: 1, fixedFull: 10, highZ: 8} = $12


// (4) DOM 総数と body 直下の数(基準値)
({
  totalDom: document.querySelectorAll('*').length,
  bodyChildren: document.body.children.length,
  imgTags: document.querySelectorAll('img').length,
})
{totalDom: 1392, bodyChildren: 2, imgTags: 75} = $13


// (5) 念のため imageCache 現況
({
  imageCacheCount: window.__imageCache.size(),
  imageCacheMB: (window.__imageCache.sizeBytes()/1024/1024).toFixed(2),
})
< {imageCacheCount: 8, imageCacheMB: "0.37"}
asuki.uehata@instansys.co.jp2026/06/18 02:52

上記により、、

body 直下に <div class="fixed inset-0 z-[99999] pointer-events-auto"> が 1つあり、その 直接子が 14個 蓄積、内部にアニメ要素 648個。これが演出ごとに unmount されず積み上がっている portal/overlay container となっている模様

asuki.uehata@instansys.co.jp2026/06/18 02:53

createPortal の宛先と、z-[99999] を作っている場所を特定を試みる

asuki.uehata@instansys.co.jp2026/06/18 02:56

fixed inset-0 z-[99999] の div は Character/ItemObtainedOverlay の portal 出力で確定か(CharacterObtainedOverlay.tsx:728, ItemObtainedOverlay.tsx:567)。

asuki.uehata@instansys.co.jp2026/06/18 02:56

body直下に1個だけ=portal自体の累積は無し。GachaMainAnimation は z-[10000] なので別物。

asuki.uehata@instansys.co.jp2026/06/18 02:57

ただ 1オーバーレイの理論アニメ要素数(~340個)に対して 648個 あるので、2つの Overlay が同時に mount されている疑い が残ります。あと #root 側の 195 anim も誰かが裏で動いている疑い。

ジャジャム 新キャラ召喚時 の 詳細データをとる

asuki.uehata@instansys.co.jp2026/06/18 03:02
// (A) 同テストID オーバーレイの生存数 — 1個より多ければ leak 確定
({
  characterObtained: document.querySelectorAll('[data-testid="character-obtained-overlay"]').length,
  itemObtained: document.querySelectorAll('[data-testid="item-obtained-overlay"]').length,
  gachaMainAnim: document.querySelectorAll('[data-testid="gacha-main-animation"]').length,
})

{characterObtained: 1, itemObtained: 0, gachaMainAnim: 0}

// (B) portal div(z-[99999]) 直下の14個の中身を出す
{
  const portal = [...document.body.children]
    .find(c => c.className.includes("z-[99999]"));
  console.table([...portal.children].map((c, i) => ({
    i,
    tag: c.tagName,
    cls: (c.className || "").toString().slice(0, 70),
    anim: c.querySelectorAll('[style*="animation"]').length,
    children: c.children.length,
  })));
}
SyntaxError: Unexpected identifier 'portal'. Expected a ':' following the property name 'const'.


// (C) #root の 195 anim はどこから? — 直下クラス別
{
  const root = document.body.children[0];
  console.table([...root.querySelectorAll('[style*="animation"]')]
    .reduce((acc, e) => {
      // 直近の data-testid 付き祖先で集計
      let p = e;
      while (p && !p.dataset?.testid && p !== root) p = p.parentElement;
      const k = p?.dataset?.testid || "(no-testid-ancestor)";
      acc[k] = (acc[k] || 0) + 1;
      return acc;
    }, {}));
}

SyntaxError: Unexpected identifier 'root'. Expected a ':' following the property name 'const'.


// (D) 念のため: 全 fixed inset-0 を列挙してどれが何か特定
console.table([...document.querySelectorAll('.fixed.inset-0')].map((e, i) => ({
  i,
  cls: (e.className || "").toString().slice(0, 70),
  testid: e.dataset?.testid || "",
  parentTag: e.parentElement?.tagName,
  parentIsBody: e.parentElement === document.body,
  anim: e.querySelectorAll('[style*="animation"]').length,
})));

(インデックス)
i
cls
testid
parentTag
parentIsBody
anim
9
9
"fixed inset-0 z-[99999] pointer-events-auto"
"character-obtained-overlay"
"BODY"
true
648
2
2
"fixed inset-0 transition-opacity duration-200 ease-in-out"
""
"DIV"
false
195
0
0
"fixed inset-0"
""
"DIV"
false
195
5
5
"fixed inset-0 z-9999 w-full h-full items-center justify-center flex po"
""
"DIV"
false
180
4
4
"fixed inset-0 z-[9998] pointer-events-none"
""
"DIV"
false
15
8
8
"fixed inset-0 w-full h-full pointer-events-none z-[2000000]"
""
"DIV"
false
0
7
7
"fixed inset-0 z-[10000] overflow-hidden"
"gacha-animation"
"DIV"
false
0
6
6
"bg-cover bg-center bg-no-repeat fixed inset-0 pointer-events-none"
""
"DIV"
false
0
3
3
"fixed inset-0 z-[9998] bg-cover bg-center bg-no-repeat"
""
"DIV"
false
0
asuki.uehata@instansys.co.jp2026/06/18 03:03

「1演出の単体重量が極大」

数値で確定したこと

観測数値意味
characterObtained1Overlay 多重 mount なし
imageCache8件 / 0.37MBdata URL 累積リークなし
body 直下2要素portal 累積なし
1 CharacterObtainedOverlay 内648 animこれが GPU 主犯
baseline (gacha scene + OverlayUI)195 anim常駐分
ピーク同時稼働843 anim(新キャラ登場時)
asuki.uehata@instansys.co.jp2026/06/18 03:03

メモリリークがあってもなくても、いずれにしても、1演出が単発でこれだけ重い → iOS WebKit の GPU レイヤープールが枯渇 → WebGL context lost、という構造はある模様

asuki.uehata@instansys.co.jp2026/06/18 03:04

10連ガチャするたびに error ”WebGL context lost" は、出てBGMが停止したりするので、少なくとも、10連ガチャのうち単発ごとの演出の重さは軽減する必要あり。

asuki.uehata@instansys.co.jp2026/06/18 03:06

推定

CharacterObtainedOverlay 単体 648 アニメ要素の内訳推定

CharacterObtainedOverlay.tsx の定数から:

  • focusStreaks × 3層 = 84+84+26 = 194
  • BURST_RAY = 55
  • suckParticles = 44, crossSparkles = 30, risingSparkles = 16
  • 計 ~340個の generator 由来 + 各要素が複数の animation/blur レイヤーを持つので、実 DOM で 648 になる
  • 大半が filter: blur() / mix-blend-mode: screen / mask-image 併用 → 全てが iOS で GPU レイヤー昇格

WebGL context lost が「青ヘパイス 240要素」で起きた理由

その直前の重たい演出(セリナ勲章=191、想戦結晶=190)の GPU テクスチャ解放が WebKit 側で間に合わず、累積した GPU メモリ圧 + 新しいオーバーレイ生成 = WebKit が context drop で防衛、という挙動。JS ヒープではなく GPU side のリーク的滞留を疑う

asuki.uehata@instansys.co.jp2026/06/18 03:09

一旦、ガチャ演出修正前のデータ

【昨日26/6/17 リモート develop ソース時点(before)】
・1キャラ獲得演出: DOM 648 要素 (filter:blur / mix-blend / mask 多用)
・1ガチャ画面 baseline: 195 要素
・ピーク同時稼働: 843 要素 (GPU レイヤー化)
・imageCache (JSヒープ): 0.37MB (リークなし、シロ)
・Pixi WebGL textures: 6 (シロ)
・portal/DOM累積: なし (リークではない)
・症状: 10連 2回目 ~ ジャジャム登場で WebGL context lost / BGM停止 / 操作不能
・原因: 1演出あたりの GPU レイヤー数が iOS WKWebView の許容を越える

ただ、体感上 実機での挙動をみると、上記意外にも負荷はあり、差し込んだログでは、検知できていないメモリリークは疑う余地あり。

asuki.uehata@instansys.co.jp2026/06/18 03:28

ここまでのまとめです

asuki.uehata@instansys.co.jp2026/06/18 03:28

1. 調査背景

  • ブランチ: INS-862_Gacha_memory_leak_
  • 症状(ユーザー報告):
    • 10連ガチャを繰り返すとWebGL: context lost が発生
    • 同時に BGM 停止、操作不能
    • 上長が部分的に対策した版でも ガチャとホーム5往復でApp停止 という蓄積系の障害報告あり
  • 仮説: 「演出単発の高負荷」と「累積するメモリリーク」が両方起きている

2. 今回セッションで追加した観測ポイント

window 上に以下のフックを追加(INS-862 タグ):

グローバルファイル観測対象
window.__pixiSoundHome.tsx:84(既存・INS-817) @pixi/sound_sounds Map
window.__pixiAssetsHome.tsxPIXI Assets.cache._cache
window.__pixiAppPixiStage.tsx現 mount 中の Application(renderer / managedTextures)
window.__imageCacheuseImageCache.tssize() / sizeBytes() / keys() / top(n) / clear()

3. 実測データ(10連 × 2 ガチャ中)

3-1. 観測値の推移

タイミング__imageCache size / bytes[style*="animation"]__pixiAssets.cache__pixiApp
ホーム画面0 / 0B0undefined
ガチャ画面遷移直後0 / 0B6undefined
1連目「想戦の結晶」1906undefined
1連目「セリナ勲章」1916undefined
1連目「青ヘパイス」2406undefined
ここで WebGL: context lost / BGM停止 / タップ不能
10連サマリー8 / 390KB1956undefined
2回目10連「女神3選択肢」8 / 390KB3506undefined
2回目10連「ジャジャム新キャラ登場」8 / 390KB8436undefined

3-2. ピーク時点(ジャジャム登場)の DOM 構成

  • document.body.children.length = 2
    • <div> (= #root) … 195 anim 要素
    • <div class="fixed inset-0 z-[99999] pointer-events-auto" data-testid="character-obtained-overlay">648 anim 要素
  • document.querySelectorAll('[data-testid="character-obtained-overlay"]').length = 1
  • document.querySelectorAll('[data-testid="item-obtained-overlay"]').length = 0
  • document.querySelectorAll('[data-testid="gacha-main-animation"]').length = 0
  • document.querySelectorAll('img').length = 75
  • document.querySelectorAll('*').length = 1392

3-3. アニメ要素のクラス分布(ピーク時)

クラス件数
absolute490
absolute rounded-full157
absolute left-1/2 top-1/2120
w-full h-full30
absolute left-1/2 top-1/2 rounded-full20
(以下少数)

4. 観測できた範囲で確定した事実

  1. useImageCache の data URL キャッシュは 8件 / 0.37MB に留まり、本セッション計測範囲では肥大していない。
  2. <img> タグは ピーク時 75 個。
  3. body 直下の DOM 要素数は 常時2個createPortal の portal node 累積は本セッション計測範囲では発生していない)。
  4. [data-testid="character-obtained-overlay"]同時に1個 しか存在していない(CharacterObtainedOverlay の多重 mount は本セッション計測範囲では発生していない)。
  5. WebGL: context lost は「青ヘパイス出現時 (anim 要素 240)」という比較的軽い場面で発生しており、その後の「ジャジャム登場 (anim 843)」のような重い場面まで持たずに既に GPU リソースが枯渇していた。
  6. CharacterObtainedOverlay 単体で anim 要素 648 個(コード上の generator 数 ≒ 340 + 1要素あたり複数アニメ層)。大半が filter: blur() / mix-blend-mode: screen / mask-image を併用。
  7. ガチャ画面の常駐 baseline(OverlayUI 下の scene UI)は 195 anim 要素
  8. ガチャ演出は PixiStage を使っていないGachaAnimation.tsx / CharacterObtainedOverlay.tsx / ItemObtainedOverlay.tsx は DOM <img> + CSS animation 構成)。__pixiApp が undefined なのはこのため。

5. 本セッションの観測フックでは まだ検知できていない 領域

以下は「リークが無い」ことを示せていない。引き続き要調査:

  • iOS WKWebView 内部のデコード済み bitmap キャッシュ(JS から観測不可)
  • ガチャ演出中の GPU テクスチャ滞留(ガチャ画面に PixiStage が無いため __pixiApp.renderer.texture.managedTextures で観測できない)
  • useLayeredImagePreload.ts:15loadedUrls Set(上限なし、未公開)
  • preload-image.tspreloadedUrls / preloadedCdnPaths Set(上限なし、未公開)
  • useEffect 内の setTimeout / setInterval / requestAnimationFrame の cleanup 漏れ
  • addEventListener の remove 漏れ
  • @pixi/sound の AudioBuffer 滞留(window.__pixiSound._sounds は公開済みだが今回未計測)
  • React 内部のクロージャ参照(Jotai atom / React Query キャッシュなど)
  • new Image() 後に img.src = "" していない箇所(CharacterObtainedOverlay.tsx:517 等)
  • ホームとガチャの行き来を5往復くらいで停止という蓄積系障害は 本セッションのスナップショット計測では再現も計測もしていない。20連目の 3つめあたりでジャジャムが登場してきた時点でのわかっている問題のまとめ

6. 現時点の仮説

  • 仮説A: 1演出の DOM 重量(CharacterObtainedOverlay 単体 648 anim + blur/blend/mask 多用)が iOS WKWebView の GPU レイヤープール上限を圧迫し、複数演出を跨いだ GPU テクスチャ解放遅延と相まって WebGL: context lost を誘発している。
  • 仮説B: 上記とは独立に、JS ヒープまたは WebKit 内部キャッシュに累積するリークが存在し、ガチャとホームの5往復の蓄積で破綻する。今回の観測フックでは捕捉できていない