---
title: "WebGL 应用与微信小游戏运行时架构、内存组成和分析方法"
author: "Perrin Yong"
author_profile: https://www.pystone.net/profile/
published_by: "Perrin Yong"
canonical: https://www.pystone.net/notes/webgl-wechat-minigame-runtime-memory-analysis/
type: note
content_role: unspecified
visibility: public
id_stability: rename-stable
source_path: "10-计算机、信息技术与工程/05-游戏图形与运行时/WebGL/WebGL 应用与微信小游戏运行时架构、内存组成和分析方法.md"
content_hash: 885eb0ff13abf08da64f185b6af7f759bbd485935bdee853b7290cecd5a0d59c
knowledge_version: 224c990773de.5fa8af6e39fa
site_commit: 224c990773de166d23a886306577dd90379529ce
notes_commit: 5fa8af6e39fa3891d1b9b4832bfa6c4e0ecaaf0a
---
# WebGL 应用与微信小游戏运行时架构、内存组成和分析方法
只讨论 **浏览器 WebGL 应用、Unity WebGL、Emscripten/WebAssembly、微信小游戏运行时，以及它们在内存快照和 Android 真机内存分析中的表现**。其中部分示例来自本会话里的 heap snapshot 结构，例如 `HEAPU8 -> ArrayBuffer`、`FS -> nameTable -> contents -> Uint8Array -> ArrayBuffer` 这类 retainer path，用作通用解读样例。

---

## 1. WebGL 应用的整体分层架构

一个现代 WebGL 游戏或图形应用，通常不是“JavaScript 调用 GPU”这么简单，而是由多层运行时组成。理解内存时，必须先分清每一层。

### 1.1 从下到上的典型架构

```text
硬件层
  CPU / RAM / GPU / 显存 / 显示系统
    ↓
操作系统层
  Android / iOS / Windows / macOS
  进程管理、内存页、图形驱动、文件系统、网络
    ↓
浏览器或宿主运行时
  Chrome / Safari / Firefox / Edge
  或微信小游戏宿主、XWeb、小游戏 JS Runtime
    ↓
JavaScript 引擎
  V8 / JavaScriptCore / QuickJS-like runtime / 宿主自研封装
  负责 JS 对象、闭包、字符串、ArrayBuffer、GC、JIT/Bytecode
    ↓
WebAssembly 引擎
  加载 .wasm
  编译 Wasm code
  管理 WebAssembly.Memory、Table、Instance、Module
    ↓
WebGL 实现层
  WebGLRenderingContext / WebGL2RenderingContext
  浏览器或宿主把 WebGL API 转成 OpenGL ES / Metal / Vulkan / Direct3D / ANGLE / 原生图形 API
    ↓
Canvas 层
  HTMLCanvasElement 或小游戏 Canvas
  作为 WebGL 绘制目标
    ↓
Emscripten Runtime
  Module、HEAPU8、FS、dynCall、wasm exports、JS glue code
    ↓
游戏引擎运行时
  Unity WebGL / Cocos / Laya / PlayCanvas / Three.js / Babylon.js
    ↓
游戏业务层
  C# / TypeScript / JavaScript / Lua / C++ 逻辑
  场景、资源、UI、网络、插件、监控、截图、音频
```

每一层都可能分配内存，而且这些内存不一定都能在 JavaScript heap snapshot 里看到。

---

## 2. 浏览器 WebGL 应用的核心组成

### 2.1 JavaScript 应用层

普通 WebGL 应用的上层通常是 JavaScript 或 TypeScript。它负责：

```text
1. 创建 Canvas；
2. 获取 WebGL context；
3. 加载资源；
4. 管理渲染循环；
5. 创建 shader / texture / buffer / framebuffer；
6. 处理输入、网络、音频、UI；
7. 与 Wasm 或引擎 runtime 通信。
```

常见代码形态：

```js
const canvas = document.createElement("canvas");
const gl = canvas.getContext("webgl2") || canvas.getContext("webgl");

const tex = gl.createTexture();
const fbo = gl.createFramebuffer();
const buffer = gl.createBuffer();
```

这类 JS 对象本身通常很小，但它们对应的底层 native / GPU 资源可能很大。

---

### 2.2 WebGL API 层

WebGL 是暴露给 JavaScript 的图形 API。它的对象通常包括：

```text
WebGLTexture        纹理对象
WebGLBuffer         顶点 / 索引 / uniform buffer
WebGLFramebuffer    离屏渲染目标
WebGLRenderbuffer   深度 / 模板 / 多采样 renderbuffer
WebGLShader         顶点 / 片元 shader
WebGLProgram        shader program
WebGLQuery          GPU query
WebGLSync           GPU 同步对象
```

例如 `framebufferTexture2D()` 会把一个 texture 附加到 framebuffer 上，作为 framebuffer 的颜色、深度或模板附件。([MDN Web Docs][1])

内存理解重点：

```text
JS heap 里看到的 WebGLTexture 对象：
  只是一个 JS wrapper。

真正的大内存：
  可能在 GPU 显存、图形驱动、浏览器 renderer native memory、Android Graphics / GL mtrack 中。
```

所以，WebGL 纹理泄露、FBO 泄露、PBO 泄露，经常 **不会** 在 JS heap snapshot 中表现为大对象，但会在 Android `dumpsys meminfo` 的 `Graphics`、`GL mtrack`、`EGL mtrack`、`Private Other`、`RssAnon` 等指标里表现出来。

---

### 2.3 WebAssembly 层

WebAssembly 是浏览器中运行 C/C++/Rust/C# 转译代码的低层执行格式。Unity WebGL、部分 Cocos 模块、物理引擎、音视频编码器、图像编码器都可能使用 Wasm。

WebAssembly 应用通常包含：

```text
.wasm 文件
  编译后的二进制代码。

WebAssembly.Module
  解析后的 Wasm 模块。

WebAssembly.Instance
  实例化后的运行实例。

WebAssembly.Memory
  Wasm 线性内存。

WebAssembly.Table
  函数表，常用于间接调用、函数指针、dynCall。
```

在 heap snapshot 中，常见表现包括：

```text
Module
Table
WasmDispatchTable
WasmTrustedInstanceData
Managed<wasm::NativeModule>
WebAssembly.Memory.buffer
ArrayBuffer
system / JSArrayBufferData
```

`Managed<wasm::NativeModule>` 通常表示 V8 / JS 引擎保存的 Wasm 编译产物、元数据或 native module 结构。它不是业务 JS 对象，也不一定能由业务代码直接释放。

---

## 3. Unity WebGL 的架构

Unity WebGL 是理解小游戏 WebGL 内存的核心场景之一。

### 3.1 Unity WebGL 的编译链路

Unity WebGL 的大致编译链路是：

```text
C# 游戏代码
  ↓ IL2CPP
C++ 代码
  ↓ Emscripten
WebAssembly

Unity 引擎 C/C++ runtime
  ↓ Emscripten
WebAssembly

Unity WebGL loader / framework JS
  ↓
浏览器或小游戏宿主
```

Unity 文档说明，Unity WebGL 使用 Emscripten 将 Unity runtime 的 C/C++ 代码交叉编译成 WebAssembly；C# 脚本则通过 IL2CPP 转成 C++，再由 Emscripten 编译成 Wasm。([Unity Documentation][2])

---

### 3.2 Unity WebGL 构建产物

Unity WebGL 构建后通常会产生：

```text
index.html
  浏览器入口页面。

.loader.js
  加载器脚本。

.framework.js
  Unity WebGL JavaScript framework / glue code。

.wasm
  Unity runtime + IL2CPP 代码编译后的 Wasm。

.data / .data.unityweb / data.unity3d
  资源数据包，包含场景、资源、序列化数据等。

StreamingAssets / AssetBundles / Addressables
  运行时资源。
```

Unity 官方文档也说明 Web Build folder 会包含运行 Web 应用所需的文件。([Unity Documentation][3])

---

### 3.3 Unity Heap / Wasm Heap

Unity WebGL 有一个非常重要的内存概念：**Unity Heap**。

Unity 文档说明，Unity heap 是一个连续内存块，用于保存 Unity runtime objects，包括 managed/native objects、assets、scenes、shaders 等；这个 heap 会以 WebAssembly Memory 的形式存在，其 `buffer` 是一个可调整大小的 `ArrayBuffer`。([Unity Documentation][4])

在 heap snapshot 中，它通常表现为：

```text
Module
  -> HEAPU8
    -> Uint8Array
      -> ArrayBuffer
        -> system / JSArrayBufferData
```

示例 retainer path：

```text
ArrayBuffer retained=256MB
  <- Uint8Array
  <- HEAPU8
  <- Module
```

本会话的快照示例中也出现了 `ArrayBuffer 256MB -> HEAPU8` 这种典型结构。

专业解释：

```text
HEAPU8 不是一块新内存。
HEAPU8 是 WebAssembly.Memory.buffer 的 Uint8Array 视图。
真正的大内存是 backing store，也就是 system / JSArrayBufferData。
```

---

## 4. Emscripten Runtime 的核心概念

Emscripten 是把 C/C++ 编译到 WebAssembly 的工具链。Unity WebGL、很多 C++ 游戏引擎、图像编码器、物理库都会间接使用它。

### 4.1 Module

`Module` 是 Emscripten JavaScript glue code 的核心对象。它通常保存：

```text
Module.HEAP8
Module.HEAPU8
Module.HEAP16
Module.HEAPU16
Module.HEAP32
Module.HEAPU32
Module.HEAPF32
Module.HEAPF64

Module._malloc
Module._free
Module.ccall
Module.cwrap
Module.FS
Module.canvas
Module.asm
Module.wasmMemory
```

`Module` 本身不一定很大，但它能引用很多大对象。例如：

```text
Module.HEAPU8.buffer
Module.FS.nameTable
Module.canvas
Module.asm.__indirect_function_table
```

所以 heap snapshot 中看到 `Module` 作为 retainer 时，不能直接说“Module 分配了这些内存”，更准确的话术是：

> `Module` 是 Emscripten runtime 的根对象，它让 Wasm heap、FS、函数表等结构保持可达。

---

### 4.2 HEAPU8 / HEAP32 / HEAPF32

这些是对同一个 `WebAssembly.Memory.buffer` 的不同 typed array 视图。

```text
HEAPU8   Uint8Array，按字节访问 Wasm 内存
HEAP8    Int8Array
HEAPU16  Uint16Array
HEAP32   Int32Array
HEAPF32  Float32Array
HEAPF64  Float64Array
```

它们都不是独立的大内存。真正大的是：

```text
WebAssembly.Memory.buffer
  -> ArrayBuffer
    -> native: system / JSArrayBufferData
```

### 4.3 Wasm 线性内存

Wasm 线性内存可以理解为 C/C++ 程序看到的一整块连续地址空间。C++ 里的：

```cpp
malloc()
new
std::vector
std::string
Unity native objects
IL2CPP managed heap
decoded asset data
temporary buffers
```

最终很多都会落在这块线性内存里。

关键特征：

```text
1. Wasm heap 增长后通常不会自动缩回。
2. C++ 对象释放后，只是 allocator 内部可复用，不代表 WebAssembly.Memory.buffer 变小。
3. JavaScript heap snapshot 能看到 Wasm Memory 的 ArrayBuffer 容量，但不一定能知道里面哪些 C++ 对象占用。
4. 如果 heap snapshot 前后 HEAPU8.buffer.byteLength 没变，不代表 C++ 内部没有分配变化；需要看 sbrk、malloc 统计或引擎内部内存统计。
```

---

### 4.4 Emscripten FS：MEMFS / IDBFS / WasmFS

Emscripten 提供一个虚拟文件系统，使 C/C++ 代码可以使用类似 POSIX 的文件 API。

常见文件系统：

```text
MEMFS
  内存文件系统。
  文件内容保存在 JS/Wasm 内存中。
  默认经常挂载在 /。

IDBFS
  IndexedDB-backed 文件系统。
  通常先在内存中维护文件，再与 IndexedDB 同步。

NODEFS
  Node.js 环境下映射本地文件系统。

WasmFS
  新的 Wasm-based 文件系统层。
```

Emscripten 文档说明，MEMFS 会在 runtime 初始化时挂载到 `/`；预加载文件可以在启动时放进 MEMFS。([Emscripten][5]) Emscripten 也有专门的 File System API 文档，并说明 WasmFS 是新的高性能、多线程文件系统层。([Emscripten][6])

在 heap snapshot 中，MEMFS 文件常见 retainer path 是：

```text
FS
  -> nameTable
    -> FSNode
      -> contents
        -> Uint8Array
          -> ArrayBuffer
            -> system / JSArrayBufferData
```

本会话的快照示例中就出现了 `FS -> nameTable -> contents -> Uint8Array -> ArrayBuffer 43MB` 的结构。

专业解释：

> 这条链说明“大内存是一个 Emscripten FS 文件的内容”。
> 它不能单独证明这个文件是谁写入的，需要结合文件路径、内容头、日志或 FS dump 才能归因。

---

## 5. 微信小游戏与浏览器 WebGL 应用的区别

微信小游戏可以运行 WebGL 和 Wasm，但它不是标准浏览器页面。

### 5.1 运行环境差异

标准浏览器 WebGL 应用通常运行在：

```text
HTML 页面
DOM
Canvas
浏览器网络 API
浏览器存储 API
标准 DevTools
```

微信小游戏运行在：

```text
微信宿主 App
小游戏 JS runtime
微信封装的 Canvas / WebGL API
wx API
小游戏文件系统
小游戏网络 API
小游戏包体 / 分包 / 资源缓存机制
微信开发者工具 / 真机远程调试
```

Cocos Creator 文档对微信小游戏的描述很适合作为通用理解：微信小游戏运行环境是微信小程序环境的扩展，在小程序环境基础上提供了 WebGL 接口封装，但这些接口由微信团队通过原生实现封装，不能等同于浏览器环境。([Cocos Creator][7])

因此，同样一段 WebGL 或 Unity WebGL 代码：

```text
在 Chrome 浏览器里：
  内存可能表现为 renderer process + GPU process。

在微信小游戏里：
  内存可能表现为 com.tencent.mm:appbrandX、tools、xweb、GPU/native mtrack、Private Other 等多个进程或类别。
```

---

### 5.2 Canvas / WebGL 差异

浏览器中：

```js
const canvas = document.querySelector("canvas");
const gl = canvas.getContext("webgl");
```

小游戏中通常是：

```js
const canvas = wx.createCanvas();
const gl = canvas.getContext("webgl");
```

差异包括：

```text
1. 没有完整 DOM。
2. Canvas 由小游戏宿主创建和管理。
3. WebGL API 由宿主封装，底层可能走微信/XWeb/平台图形实现。
4. 部分扩展、像素读取、canvas 导出、文件转换行为可能和浏览器不同。
5. 真机内存归因通常更依赖 Android/iOS 系统工具，而不是单靠 JS heap snapshot。
```

---

### 5.3 文件系统差异

标准浏览器 WebGL：

```text
Emscripten MEMFS / IDBFS
Browser Cache
IndexedDB
Fetch / XHR response cache
```

微信小游戏：

```text
小游戏包体
分包
远程资源
微信本地缓存目录
小游戏文件系统 API
Unity / Emscripten 虚拟 FS
宿主内部资源缓存
```

在小游戏里，Emscripten FS、Unity datapackage、小游戏宿主缓存、网络下载缓存可能同时存在。它们在内存中可能表现为：

```text
JS ArrayBuffer
Uint8Array
FSNode.contents
Native file buffer
Network response buffer
Decompression buffer
小游戏宿主内部 cache
```

---

### 5.4 Unity WebGL 转小游戏的额外成本

Unity / 团结引擎的小游戏 WebGL 平台文档指出，WebGL 运行机制和文件系统让内存结构与原生 App 很不一样，通常会多出 Wasm 加载与编译、Wasm Heap 未使用部分、JS File System 等内存；文档还特别提示，Emscripten 使用 JS + IndexedDB 模拟文件系统时，JS 中可能始终存有文件 copy，因此在微信小游戏中应尽量避免使用 Emscripten 文件系统。([Unity User Manual][8])

这对内存分析很重要：

```text
小游戏 WebGL 内存高，不一定是业务泄露。
它可能是：
  Wasm 文件加载和编译成本；
  Unity Heap 预留容量；
  data package / FS 文件内容；
  浏览器/宿主 JIT 和 code cache；
  GPU/Canvas/WebGL native 资源；
  资源下载、解压、缓存；
  多进程 PSS 统计口径导致的波动。
```

---

## 6. WebGL / 小游戏应用的内存组成

从内存分析角度，可以把 WebGL 应用内存分成以下几类。

---

### 6.1 JavaScript Heap

包括：

```text
普通 Object
Array
Map / Set / WeakMap
Closure
Function
Promise
字符串
TypedArray wrapper
ArrayBuffer wrapper
事件回调
定时器回调
模块对象
小游戏 runtime JS 对象
引擎 JS glue code
```

在 heap snapshot 中常见类型：

```text
object
array
string
concatenated string
closure
code
object shape / system Map
system / Context
```

注意：

```text
ArrayBuffer wrapper 很小。
真正的二进制 backing store 通常体现在 native: system / JSArrayBufferData。
```

---

### 6.2 ArrayBuffer / TypedArray backing store

典型对象链：

```text
Uint8Array
  -> buffer
    -> ArrayBuffer
      -> backing_store
        -> system / JSArrayBufferData
```

常见来源：

```text
1. Wasm Memory buffer；
2. 网络 response ArrayBuffer；
3. 文件读取结果；
4. 图片/音频/压缩包二进制；
5. WebGL readPixels 结果；
6. Emscripten FS 文件 contents；
7. 上传 request body；
8. 解压缩中间 buffer；
9. 编码器输入 / 输出 buffer。
```

在 heap snapshot 中，如果看到：

```text
native | system / JSArrayBufferData | 43MB
```

不要停在这个节点，要往 retainer path 往上看：

```text
是 HEAPU8 持有？
是 FSNode.contents 持有？
是 Response / Promise 持有？
是 Upload queue 持有？
是 WebGL readback buffer 持有？
```

本会话快照示例中 `system / JSArrayBufferData` 排名前两项是 256MB 和 43MB，说明大头是 ArrayBuffer backing store，而不是普通 JS 对象。

---

### 6.3 Wasm Linear Memory / Unity Heap

表现：

```text
Module.HEAPU8.buffer
ArrayBuffer
system / JSArrayBufferData
```

常见大小：

```text
64MB
128MB
192MB
256MB
512MB
更高
```

取决于：

```text
INITIAL_MEMORY
ALLOW_MEMORY_GROWTH
MAXIMUM_MEMORY
Unity Player Settings
运行时峰值分配
资源规模
C++ allocator 行为
```

分析重点：

```text
HEAPU8.buffer.byteLength：
  表示 Wasm heap 容量。

C++ 已使用内存：
  需要看 malloc/sbrk/Unity profiler/引擎统计。

释放 C++ 对象：
  不一定让 HEAPU8.buffer 变小。

Wasm heap 增长：
  一旦扩容，通常成为新的常驻高水位。
```

---

### 6.4 Wasm 编译产物和引擎内部结构

heap snapshot 中可能看到：

```text
Managed<wasm::NativeModule>
WasmTrustedInstanceData
WasmDispatchTable
WasmImportData
Table
Code
BytecodeArray
FeedbackCell
FeedbackVector
SharedFunctionInfo
```

专业解释：

```text
Managed<wasm::NativeModule>
  Wasm 编译后的 native module / 引擎内部结构。

WasmDispatchTable / Table
  Wasm 函数表、间接调用表、函数指针相关结构。

BytecodeArray / FeedbackVector / Code
  JavaScript 引擎为 JS 函数生成的字节码、反馈向量、优化信息。

WAGame.js / framework.js / plugin.js string
  运行时代码或源码字符串，不一定是泄露。
```

本会话快照示例中存在 `Managed<wasm::NativeModule> 35.23MB`、`WasmTrustedInstanceData`、`WasmDispatchTable`、`Table` 等，这类对象通常属于 Wasm/JS 引擎运行时成本。

---

### 6.5 Emscripten FS / Unity datapackage / 资源包

典型 retainer path：

```text
FS
  -> nameTable
    -> Array[index]
      -> FSNode
        -> contents
          -> Uint8Array
            -> ArrayBuffer
              -> JSArrayBufferData
```

可能来源：

```text
1. Unity data package；
2. 首包资源；
3. 预加载文件；
4. 运行时写入的临时文件；
5. 下载缓存；
6. 截图临时文件；
7. 日志文件；
8. AssetBundle / Addressables cache。
```

重要判断：

```text
FS -> nameTable -> contents 只能证明“这是 FS 文件内容”。
不能直接证明“这是哪个模块写入的”。
必须进一步看：
  文件路径；
  文件头；
  FS dump；
  写入调用栈；
  运行日志；
  阶段差分。
```

如果文件头是 `UnityWebData1.0`，通常更像 Unity datapackage，而不是业务临时缓存。

---

### 6.6 WebGL / GPU / Graphics 资源内存

常见资源：

```text
Texture
Renderbuffer
Framebuffer
Vertex Buffer
Index Buffer
Uniform Buffer
Shader
Program
PBO / Pixel Buffer Object
VAO
Transform feedback
```

这些资源大多不在 JS heap 中以真实大小体现。

典型内存估算：

```text
RGBA8 texture:
  width × height × 4 bytes

RGBA8 texture with mipmaps:
  width × height × 4 × 1.33 左右

Depth24/Stencil8:
  width × height × 4 bytes

双缓冲 / 多缓冲:
  单份大小 × buffer count

PBO ring:
  requiredBytes × ringSize
```

例如：

```text
1280 × 720 × 4 ≈ 3.52 MB
1920 × 1080 × 4 ≈ 7.91 MB
2560 × 1440 × 4 ≈ 14.06 MB
三份 2560×1440 RGBA ≈ 42.19 MB
```

所以“截图、离屏渲染、readPixels、PBO ring、canvas 导出”很容易产生 20–50MB 级别的波动，但不一定在 JS heap snapshot 中出现。

---

### 6.7 Canvas backing store

Canvas 本身也有 backing store。实际像素尺寸通常是：

```text
cssWidth × devicePixelRatio
cssHeight × devicePixelRatio
```

例如：

```text
CSS 1280×720
devicePixelRatio = 2
实际 backing store = 2560×1440
RGBA = 2560×1440×4 ≈ 14.06 MB
```

如果同时有：

```text
主 Canvas
离屏 Canvas
截图 Canvas
临时 Canvas
WebGL framebuffer
导出图片 buffer
```

内存会快速增加。

---

### 6.8 网络、解压和上传内存

常见链路：

```text
fetch / wx.request
  -> response ArrayBuffer
  -> 解压 buffer
  -> 解析 buffer
  -> 写入 FS 或 Wasm heap
  -> 上传 body copy
```

常见重复副本：

```text
网络 response buffer
解压前 compressed buffer
解压后 uncompressed buffer
Wasm heap copy
FS contents copy
业务 cache copy
上传 request body copy
宿主内部 native copy
```

优化目标不是“完全没有 buffer”，而是减少同一份数据在同一时间窗口内出现多份副本。

---

### 6.9 图片、音频、字体和视频

图片资源可能经历：

```text
compressed image file
decoded RGBA pixels
GPU texture
mipmap chain
CPU-side cache
```

音频资源可能经历：

```text
compressed audio file
decoded PCM
audio engine buffer
streaming buffer
native audio cache
```

字体资源可能经历：

```text
font file
glyph cache
texture atlas
layout cache
```

这些通常也不完全体现在 JS heap snapshot 中。

---

## 7. Chrome / 微信开发者工具 Heap Snapshot 术语

Chrome DevTools 文档把 heap snapshot 用于分析对象分布和内存泄露；Memory 面板里常见 `Shallow Size` 和 `Retained Size` 两个概念。([Chrome for Developers][9])

### 7.1 Shallow Size / Self Size

含义：

```text
对象自身占用的内存。
```

例如：

```text
ArrayBuffer wrapper self_size = 52 B
Uint8Array wrapper self_size = 60 B
```

这不代表 buffer 只有几十字节。真正数据可能在 backing store。

---

### 7.2 Retained Size

含义：

```text
如果这个对象被释放，并且它支配的对象也变为不可达，总共可以释放的内存。
```

Retained Size 适合找 dominator，但要小心：

```text
某个 Object retained 很大，可能只是因为它是一个根容器。
它不一定是分配源。
```

例如：

```text
FS object retained 43MB
```

专业解释不是“FS 对象自己有 43MB”，而是：

> FS 通过 nameTable 支配了某个 FSNode.contents，而 contents 持有 43MB ArrayBuffer。

---

### 7.3 Distance

含义：

```text
对象到 GC Root 的最短引用距离。
```

距离越小，越接近根对象。常见根：

```text
window / globalThis
GameGlobal
Module
GC roots
Global handles
Native contexts
Internalized strings
```

距离小不代表泄露，只说明它容易长期存活。

---

### 7.4 Retainer Path

Retainer path 是：

```text
谁持有了这个对象？
这个对象为什么还活着？
```

它不是 allocation stack。

正确表述：

```text
Retainer path 证明“当前由谁保活”。
Allocation stack 才证明“当初由谁分配”。
```

例如：

```text
FS -> nameTable -> contents -> Uint8Array -> ArrayBuffer
```

只能证明：

```text
这个 ArrayBuffer 当前作为一个 FS 文件内容存在。
```

不能单凭这条链判断：

```text
是谁写了这个文件？
这个文件是什么？
是否是泄露？
```

---

### 7.5 常见 node type 解读

| type        | 含义                | 常见来源                                    |
| ----------- | ----------------- | --------------------------------------- |
| `object`    | JS 对象             | Object、ArrayBuffer、Uint8Array、Module、FS |
| `array`     | JS 数组或内部数组        | object elements、nameTable、属性数组          |
| `string`    | 字符串               | 源码字符串、日志、JSON、base64                    |
| `code`      | JS 引擎代码对象         | BytecodeArray、Code、SharedFunctionInfo   |
| `closure`   | 函数闭包              | 回调、模块函数、事件监听                            |
| `native`    | JS 引擎外部/native 对象 | JSArrayBufferData                       |
| `hidden`    | 引擎内部对象            | WasmNativeModule、Map、Context            |
| `synthetic` | 快照合成根节点           | GC roots、Global handles                 |

---

## 8. 典型快照结构的专业解读

### 8.1 `system / JSArrayBufferData`

看到：

```text
native: system / JSArrayBufferData 256MB
```

专业解读：

```text
这是某个 ArrayBuffer 的外部 backing store。
它是字节数据真正占用的地方。
需要往上找 backing_store 的 owner ArrayBuffer。
```

继续看 retainer：

```text
JSArrayBufferData
  <- backing_store
ArrayBuffer
  <- buffer
Uint8Array
  <- HEAPU8
Module
```

结论：

```text
这是 Wasm linear memory / Unity Heap。
```

如果 retainer 是：

```text
JSArrayBufferData
  <- ArrayBuffer
  <- Uint8Array
  <- contents
FSNode
  <- nameTable
FS
```

结论：

```text
这是 Emscripten FS 文件内容。
```

---

### 8.2 `Module -> HEAPU8 -> ArrayBuffer`

专业含义：

```text
Module 是 Emscripten runtime 全局对象。
HEAPU8 是 Wasm memory 的 Uint8Array 视图。
ArrayBuffer 是 WebAssembly.Memory.buffer。
JSArrayBufferData 是 backing store。
```

不要说：

```text
Module 分配了 256MB。
```

应该说：

```text
Module 持有 Wasm linear memory 的视图，使这块 Wasm heap 在 retainer path 中可见。
```

---

### 8.3 `FS -> nameTable -> contents -> Uint8Array`

专业含义：

```text
Emscripten FS 中某个文件节点持有文件内容。
```

可能是：

```text
Unity data package
预加载资源
临时文件
缓存文件
日志文件
截图文件
下载文件
```

排查方式：

```text
1. dump FS 文件路径；
2. 打印文件 size；
3. 读取文件头；
4. hook FS.writeFile / FS.write；
5. 结合日志和时间点。
```

---

### 8.4 `Managed<wasm::NativeModule>`

专业含义：

```text
JS 引擎中与 Wasm module 编译结果相关的 native 内部结构。
```

常见归属：

```text
Wasm 代码编译产物
Wasm 函数元数据
JIT / baseline / optimized code
引擎内部缓存
```

通常不能通过业务 JS 直接释放。减少它的方式通常是：

```text
减小 Wasm 文件体积；
裁剪未用代码；
按需加载模块；
Wasm 分包；
避免一次性加载全部功能。
```

---

### 8.5 `BytecodeArray / FeedbackVector / SharedFunctionInfo`

专业含义：

```text
JavaScript 引擎为 JS 代码生成的执行数据。
```

常见来源：

```text
Unity framework.js
小游戏 runtime JS
引擎 glue code
业务 bundle
插件 bundle
Babel runtime
polyfill
```

这类对象在 heap snapshot 里可能很多，但一般不是 20–50MB 驻留增长的主因，除非出现大量动态生成代码、eval、new Function 或反复加载 bundle。

---

### 8.6 `string` 大对象

常见来源：

```text
源码字符串
内联 source map
base64 图片
base64 音频
JSON payload
日志缓存
错误堆栈
压缩包文本
```

专业判断：

```text
源码字符串：
  多数是运行时或打包成本，不一定是泄露。

base64：
  高风险，因为体积比二进制更大，并且字符串在 JS heap 里很明显。

JSON：
  如果长期缓存大型上报包或配置包，可能泄露。

日志：
  如果数组无上限，会持续增长。
```

---

## 9. Android 真机内存指标

在微信小游戏真机排查中，JS heap snapshot 只覆盖一部分内存。Android 进程内存更适合用：

```text
adb shell dumpsys meminfo <pid>
/proc/<pid>/smaps_rollup
/proc/<pid>/status
showmap
Perfetto
heapprofd
GPU/mtrack 指标
```

Android 官方文档说明 `dumpsys meminfo` 可用于查看进程内存，且通常关注 `Pss Total` 和 `Private Dirty`；`Private Dirty` 很重要，因为它是进程私有且不能轻易回收的脏页。([Android Developers][10]) Perfetto 文档也强调共享内存会映射到多个进程，PSS 会按比例分摊，因此分析时经常要关注 Private Dirty 来避免共享页干扰。([Android Git Repositories][11])

### 9.1 RSS

```text
Resident Set Size
当前实际驻留在物理内存中的页总量。
```

特点：

```text
包含共享页。
容易受系统缓存、共享库、其他进程影响。
不能直接等同于“这个进程独占了多少内存”。
```

---

### 9.2 PSS

```text
Proportional Set Size
按比例分摊共享页后的内存。
```

例如某个 100MB 共享库被 4 个进程共享，每个进程 PSS 可能只算 25MB。

优点：

```text
适合估算进程对系统总内存的贡献。
```

缺点：

```text
共享页归属变化会造成波动。
跨运行比较时容易不稳定。
```

---

### 9.3 Private Dirty

```text
进程私有、已修改、不能直接丢弃的内存页。
```

这是判断真实驻留增长的重要指标。

如果某阶段：

```text
Private Dirty +30MB
RssAnon +30MB
持续 60 秒不回落
```

这比单纯 `PSS +30MB` 更能证明有真实私有内存增长。

---

### 9.4 RssAnon / RssFile / RssShmem

来自 `/proc/<pid>/status`。

```text
RssAnon
  匿名内存，通常对应 native heap、Wasm memory、malloc、匿名 mmap。

RssFile
  file-backed mapping，例如 so、dex、wasm 文件、资源文件映射。

RssShmem
  共享内存，例如图形 buffer、ashmem、共享映射。
```

判断：

```text
RssAnon 增长：
  更像 malloc / Wasm heap / native heap。

RssFile 增长：
  更像文件映射、代码、资源、cache。

RssShmem / Graphics 增长：
  更像图形缓冲、Canvas、GPU、Surface。
```

---

### 9.5 dumpsys meminfo 常见分类

| 分类                     | 通常含义                                           |
| ---------------------- | ---------------------------------------------- |
| Java Heap              | Java/Kotlin 对象，微信宿主或插件 Java 层                  |
| Native Heap            | C/C++ malloc/new                               |
| Code                   | so、dex、oat、jit code、wasm/code mapping          |
| Stack                  | 线程栈                                            |
| Graphics               | 图形相关内存                                         |
| GL mtrack / EGL mtrack | OpenGL/EGL 资源                                  |
| Private Other          | 不容易归类的 native/private memory，WebGL/XWeb/宿主内部常见 |
| System                 | 系统归属或分摊内存                                      |

微信小游戏中，`Private Other` 增长并不罕见，因为 WebGL、XWeb、Canvas、宿主桥接、资源缓存都可能落在这里。

---

## 10. 为什么“启动前 700MB，另一次启动前 800MB”很常见

WebGL 小游戏内存基线波动大，常见原因：

```text
1. 微信多进程状态不同；
2. appbrand 进程复用；
3. 上一次小游戏残留缓存；
4. XWeb / JS 引擎 code cache；
5. Wasm 编译缓存；
6. 资源缓存；
7. GPU / Canvas / Surface 状态不同；
8. Android 系统内存压力不同；
9. PSS 共享页分摊变化；
10. 是否已加载 Unity datapackage；
11. 是否已触发音频、字体、图片、网络、WebGL context。
```

因此，专业测试口径应是：

```text
同一进程、同一次运行、同一阶段、稳定后采样。
```

不要比较：

```text
Run A 初始 700MB
Run B 初始 800MB
```

而要比较：

```text
同一个 PID：
  T0: WebGL/Unity 加载完成，目标模块未启动
  T1: 目标模块启动后
  T2: 首次功能触发后
  T3: 清理后
```

---

## 11. 内存增长、驻留、泄露的专业定义

### 11.1 峰值内存

```text
某个操作期间短时间增加，随后回落。
```

例如：

```text
下载资源时 response buffer + 解压 buffer 同时存在。
截图时 readPixels buffer + encode buffer 同时存在。
上传时 body copy + native copy 同时存在。
```

峰值问题会导致 OOM，但不是泄露。

---

### 11.2 驻留内存

```text
操作完成后仍然保留，并且长期稳定存在。
```

例如：

```text
初始化后的 WebGL texture cache
Wasm heap 扩容后的容量
引擎单例缓存
常驻资源
全局 buffer pool
```

驻留不一定是 bug，但需要判断是否必要、是否过早、是否过大。

---

### 11.3 泄露

```text
重复执行同一操作后，稳定基线持续上升，且没有释放路径。
```

例如：

```text
每次截图后多一个未 delete 的 texture；
每次请求后 pending map 少删一个 entry；
每次 start 后多注册一组事件监听；
每次写 FS 临时文件后未 unlink；
每次 push 日志无上限。
```

证明泄露要看趋势，不看单次。

---

## 12. WebGL 应用内存分析方法

### 12.1 分阶段基线法

推荐阶段：

```text
T0: 宿主进程已稳定，游戏未加载
T1: WebGL/Unity runtime 加载完成
T2: data package / 首包资源加载完成
T3: 主场景进入，静置 60 秒
T4: 目标功能初始化前
T5: 目标功能初始化后
T6: 首次功能调用后
T7: 功能完成后静置 60 秒
T8: stop / destroy / cleanup 后
T9: 重复 N 次后
```

每个阶段采集：

```text
JS heap snapshot
HEAPU8.buffer.byteLength
FS 大文件列表
WebGL 资源统计
dumpsys meminfo
smaps_rollup
log marker
```

---

### 12.2 JS Heap Snapshot 分析步骤

步骤：

```text
1. 按 retained size 排序；
2. 找大对象；
3. 区分 wrapper 和 backing store；
4. 展开 retainer path；
5. 判断对象类别：
   Wasm heap？
   FS file？
   network buffer？
   WebGL readback？
   string/base64？
   closure queue？
6. 做前后 snapshot diff；
7. 对无法归因的大 buffer 加分配栈或 tag。
```

重点：

```text
Retainer path 回答“为什么还活着”。
Allocation instrumentation 回答“哪里分配的”。
Snapshot diff 回答“阶段之间新增了什么”。
```

---

### 12.3 Android 真机分析步骤

推荐命令：

```bash
adb shell "ps -A | grep -i com.tencent.mm"
```

找到小游戏进程，例如：

```text
com.tencent.mm:appbrand0
com.tencent.mm:appbrand1
com.tencent.mm:tools
com.tencent.mm:toolsmp
```

然后采样：

```bash
adb shell "PID=<pid>; while true; do \
echo '===== MEM '$(date +%s)' pid='$PID' ====='; \
dumpsys meminfo $PID | sed -n '1,120p'; \
echo '----- smaps_rollup -----'; \
cat /proc/$PID/smaps_rollup 2>/dev/null | grep -E 'Rss:|Pss:|Private_Clean:|Private_Dirty:|SwapPss:'; \
echo '----- status -----'; \
cat /proc/$PID/status 2>/dev/null | grep -E 'VmRSS|VmHWM|RssAnon|RssFile|RssShmem|VmSwap'; \
sleep 2; \
done"
```

分析顺序：

```text
1. 先看同 PID；
2. 再看所有微信相关进程合计；
3. 优先看 Private Dirty、RssAnon；
4. 再看 PSS、Graphics、Private Other；
5. 结合 log marker 对齐操作阶段。
```

---

### 12.4 Perfetto / heapprofd

Perfetto 的 native heap profiling 可以按 malloc/free 调用栈聚合内存，但它只能看到 profiling 开始后的 native 分配，并且目标进程需要满足设备和权限条件。([Android Git Repositories][11])

在微信小游戏场景下限制较多：

```text
1. 目标进程是微信，不是自己的 APK；
2. 普通 user 设备可能不能 profile 微信进程；
3. userdebug/root 设备更适合；
4. 仍需结合业务 marker 才能知道哪个阶段触发。
```

可用时，Perfetto 适合回答：

```text
Native heap 增长到底来自哪个 native 调用栈？
是 malloc？
是 mmap？
是图形资源？
是线程栈？
是 JIT/code？
```

---

## 13. 常见高风险内存路径

### 13.1 截图 / readPixels

典型路径：

```text
WebGL framebuffer
  -> readPixels
  -> JS Uint8Array
  -> Wasm encoder input
  -> JPEG/PNG output
  -> upload body
```

高风险点：

```text
1. 每次 new Uint8Array(width × height × 4)；
2. 多个 readback ring slot；
3. raw RGBA 同时存在 JS + Wasm + FS 三份；
4. 使用 canvas.toTempFilePath / readFile 产生宿主内部 copy；
5. base64 导出；
6. 上传回调闭包持有大 buffer；
7. GL texture/FBO/PBO 没有 delete。
```

优化：

```text
1. readback buffer pool；
2. 限制 ring slot 数量；
3. 直接写入 Wasm HEAPU8.subarray；
4. 避免 raw RGBA 落 FS；
5. 上传完成后清理引用；
6. 显式 delete WebGL resources。
```

---

### 13.2 WebGL texture / framebuffer

危险模式：

```js
const tex = gl.createTexture();
const fbo = gl.createFramebuffer();
// 使用后没有 gl.deleteTexture(tex)
// 使用后没有 gl.deleteFramebuffer(fbo)
```

专业建议：

```text
所有 create* 都应有对应 delete*。
纹理缓存要有容量上限。
场景切换、功能关闭、Canvas 重建时要释放 GPU 资源。
```

---

### 13.3 FS 文件缓存

危险模式：

```js
FS.writeFile("/tmp/large.raw", rawBytes);
// 忘记 FS.unlink
```

结果：

```text
FS.nameTable
  -> FSNode.contents
    -> Uint8Array
      -> ArrayBuffer
```

优化：

```text
1. 临时文件用完 unlink；
2. 不把 raw 大文件写入 FS；
3. 对 FS 文件做 size dump；
4. 对 FS.writeFile / FS.write 加 hook；
5. 避免在小游戏中滥用 Emscripten FS cache。
```

---

### 13.4 网络 response / upload body

危险模式：

```text
response ArrayBuffer
  -> Uint8Array copy
  -> Wasm heap copy
  -> FS copy
  -> request body copy
```

优化：

```text
1. 使用 view / subarray，避免 slice 复制；
2. 限制同时 inflight 请求；
3. 上传完成后释放 body 引用；
4. 不缓存完整历史包；
5. 大文件流式处理或分块处理。
```

---

### 13.5 Base64

危险模式：

```text
binary image
  -> base64 string
  -> JSON
  -> upload
```

问题：

```text
1. base64 体积膨胀；
2. 字符串在 JS heap 中明显变大；
3. JSON stringify 可能再产生副本；
4. 上传时可能再复制。
```

优化：

```text
优先使用 ArrayBuffer / Uint8Array / Blob-like binary API。
```

---

### 13.6 事件监听和定时器

危险模式：

```js
setInterval(...)
requestAnimationFrame(loop)
wx.onShow(callback)
wx.onHide(callback)
canvas.addEventListener(...)
```

重复注册但不注销，会导致：

```text
closure retained
context retained
large state retained
module retained
buffer retained
```

优化：

```text
1. start/stop 成对；
2. on/off 成对；
3. timer id 统一管理；
4. 多次 start 前先检查是否已经注册；
5. stop 后断开大对象引用。
```

---

## 14. 内存快照快速判断表

| 快照表现                                        | 专业含义                            | 下一步                            |
| ------------------------------------------- | ------------------------------- | ------------------------------ |
| `HEAPU8 -> ArrayBuffer 256MB`               | Wasm linear memory / Unity Heap | 看是否启动前已存在，是否 grow              |
| `FS -> nameTable -> contents -> Uint8Array` | Emscripten FS 文件内容              | dump 路径、文件头、写入栈                |
| `system / JSArrayBufferData` 很大             | ArrayBuffer backing store       | 找 owner ArrayBuffer 和 retainer |
| `Managed<wasm::NativeModule>` 很大            | Wasm 编译产物                       | 减小 wasm、分包、懒加载                 |
| `WasmDispatchTable / Table`                 | Wasm 函数表                        | 通常不是泄露                         |
| 大 `string`                                  | 源码、base64、JSON、日志               | 看内容和 retainer                  |
| `system / Context` 很多                       | 闭包上下文                           | 查事件、timer、Promise、模块缓存         |
| `Object retained 很大`                        | 容器支配子图                          | 展开子节点，不要误判                     |
| `WeakMap table`                             | 弱映射内部结构                         | 注意 key/value 是否仍被强引用           |
| JS heap 无增长但 Android 内存涨                    | native/GPU/mmap/宿主缓存            | 看 meminfo、smaps、Perfetto       |

---

## 15. 优化原则

### 15.1 延迟初始化

不要在启动阶段初始化所有重资源。

```text
启动阶段只初始化核心逻辑。
截图、编码、上传、音频、复杂 WebGL FBO、离屏 Canvas 应按需初始化。
```

---

### 15.2 复用 buffer

适合复用：

```text
readPixels buffer
YUV buffer
encode output buffer
network body buffer
decompression scratch
tile buffer
temporary typed arrays
```

注意：

```text
复用不是无限缓存。
buffer pool 必须有最大容量和释放时机。
```

---

### 15.3 避免重复副本

优先：

```js
view = HEAPU8.subarray(ptr, ptr + len);
```

谨慎：

```js
array.slice()
buffer.slice()
Array.from()
JSON.stringify(largeObject)
base64 encode
FS.readFile()
FS.writeFile()
```

---

### 15.4 显式释放 WebGL 资源

```js
gl.deleteTexture(tex);
gl.deleteBuffer(buf);
gl.deleteFramebuffer(fbo);
gl.deleteRenderbuffer(rbo);
gl.deleteProgram(program);
gl.deleteShader(shader);
```

同时清空 JS 引用：

```js
tex = null;
fbo = null;
```

---

### 15.5 控制 Emscripten FS

```text
临时文件：
  用完 unlink。

大 raw 数据：
  尽量不进 FS。

缓存文件：
  有容量限制和淘汰策略。

持久化：
  明确 sync 时机和内存副本成本。
```

---

### 15.6 控制 Wasm heap 峰值

```text
1. 避免同一时间保留多个大 vector/string/buffer；
2. 分块处理大资源；
3. 降低启动阶段 malloc 峰值；
4. 调整 INITIAL_MEMORY / MAXIMUM_MEMORY；
5. 避免一次操作把 heap grow 到高水位；
6. 使用引擎 profiler 或 allocator 统计确认真实使用量。
```

---

### 15.7 用阶段差分而不是单点比较

专业测试报告应该写：

```text
同一 PID 下，T0 到 T1：
Private Dirty 中位数 +X MB；
RssAnon 中位数 +Y MB；
JS heap retained +Z MB；
HEAPU8.buffer 未变化；
FS 大文件未新增；
Graphics +N MB；
持续 60 秒未回落。
```

不要写：

```text
这次启动前 700MB，另一次 800MB，所以 SDK/模块涨了 100MB。
```

---

## 16. 推荐的分析流程模板

### 16.1 证明是否增长

```text
1. 选定同一设备、同一微信版本、同一小游戏包；
2. 冷启动微信或清理 appbrand 进程；
3. 找到目标 PID；
4. 进入游戏并等待稳定；
5. 打 T0 marker；
6. 执行目标操作；
7. 打 T1/T2/T3 marker；
8. 每阶段等待 60 秒；
9. 采集 meminfo + smaps + heap snapshot；
10. 至少重复 5 次，取中位数。
```

---

### 16.2 证明是否泄露

```text
for i in 1..10:
  start feature
  wait stable
  stop feature
  wait stable
  collect memory
```

判断：

```text
每轮结束后的稳定基线持续上升：
  泄露或未释放缓存。

第一轮上升后稳定：
  驻留成本。

操作期间上升，结束后回落：
  峰值成本。
```

---

### 16.3 归因路径

```text
JS heap 增长：
  heap snapshot / retainer / allocation stack。

Wasm heap capacity 增长：
  HEAPU8.buffer.byteLength / sbrk / malloc stats。

FS 增长：
  FS dump / file path / file head / FS.write hook。

GPU/Graphics 增长：
  WebGL resource counter / delete audit / dumpsys Graphics / GL mtrack。

Native heap 增长：
  dumpsys Native Heap / smaps RssAnon / Perfetto heapprofd。

Private Other 增长：
  XWeb/宿主/图形/匿名 mmap，需要 marker + Perfetto + 功能开关实验。
```

---

## 17. 标准术语表

| 术语                       | 专业解释                                     |
| ------------------------ | ---------------------------------------- |
| JS Heap                  | JavaScript 引擎管理的对象堆                      |
| GC Root                  | 垃圾回收根对象，从它可达的对象不会被回收                     |
| Shallow Size / Self Size | 对象自身大小                                   |
| Retained Size            | 对象释放后可能连带释放的大小                           |
| Retainer Path            | 当前是谁持有该对象                                |
| Allocation Stack         | 对象创建时的调用栈                                |
| ArrayBuffer              | JS 二进制缓冲区对象                              |
| JSArrayBufferData        | ArrayBuffer 的 native backing store       |
| TypedArray               | Uint8Array、Float32Array 等 ArrayBuffer 视图 |
| WebAssembly.Memory       | Wasm 线性内存对象                              |
| HEAPU8                   | Emscripten 对 Wasm memory 的 Uint8Array 视图 |
| Module                   | Emscripten runtime 全局对象                  |
| MEMFS                    | Emscripten 内存文件系统                        |
| IDBFS                    | IndexedDB-backed Emscripten 文件系统         |
| FSNode.contents          | Emscripten FS 文件内容                       |
| Wasm NativeModule        | JS 引擎中的 Wasm 编译产物                        |
| WebGLTexture             | WebGL 纹理 wrapper                         |
| Framebuffer              | 离屏或目标渲染缓冲组合                              |
| Renderbuffer             | 深度/模板/多采样渲染缓冲                            |
| PBO                      | Pixel Buffer Object，用于像素传输               |
| PSS                      | 共享页按比例分摊后的进程内存                           |
| RSS                      | 进程驻留物理内存                                 |
| Private Dirty            | 进程私有且不可直接丢弃的脏页                           |
| RssAnon                  | 匿名驻留内存，常对应 malloc/Wasm/native            |
| RssFile                  | 文件映射驻留内存                                 |
| RssShmem                 | 共享内存驻留                                   |
| Graphics / GL mtrack     | 图形/GPU/EGL/OpenGL 相关内存                   |
| Private Other            | Android meminfo 中不易归类的私有内存               |

---

## 18. 最重要的心智模型

分析 WebGL / 微信小游戏内存时，应该始终把内存分成四个层面：

```text
1. JS 可见对象
   Object / Array / Closure / String / ArrayBuffer wrapper。

2. JS 外部 backing store
   system / JSArrayBufferData，大型二进制数据。

3. Wasm / Emscripten / Unity runtime
   HEAPU8、Unity Heap、FS、Wasm compiled module、Table。

4. 宿主和系统层
   WebGL/GPU、Canvas、XWeb、微信小游戏 runtime、Android native heap、Graphics、Private Other。
```

一个 solid 的内存结论必须回答：

```text
1. 增长发生在哪个阶段？
2. 增长属于 JS heap、Wasm heap、FS、GPU、native，还是宿主缓存？
3. 是峰值、驻留，还是泄露？
4. 是同一进程内稳定复现，还是跨运行波动？
5. 快照中的 retainer path 证明了什么？
6. allocation stack 或打点是否证明了分配来源？
7. 这块内存对功能是否必要？
8. 是否过早分配？
9. 是否可以延迟、复用、缩容或释放？
```

最终，专业表述应从：

```text
“启动后内存涨了 50MB”
```

升级为：

```text
“在同一 appbrand 进程内，目标功能启动后 60 秒稳定区间，Private Dirty 中位数增加 28MB，RssAnon 增加 31MB；JS heap snapshot 未新增大型 retained object，HEAPU8.buffer 未增长，FS 大文件未新增；因此该增长更可能位于 native / graphics / 宿主 runtime 层。下一步应按 WebGL resource、Canvas readback、网络 body、native heap 四类路径继续分解。”
```

这就是 WebGL / 微信小游戏内存分析中最重要的专业化思考方式。

[1]: https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/framebufferTexture2D?utm_source=chatgpt.com "WebGLRenderingContext: framebufferTexture2D() method"
[2]: https://docs.unity3d.com/cn/2020.1/Manual/webgl-gettingstarted.html?utm_source=chatgpt.com "WebGL 开发入门"
[3]: https://docs.unity3d.com/Manual/webgl-building.html?utm_source=chatgpt.com "Web Build folder"
[4]: https://docs.unity3d.com/6000.3/Documentation/Manual/webgl-memory.html?utm_source=chatgpt.com "Memory in Unity Web"
[5]: https://emscripten.org/docs/porting/files/file_systems_overview.html?utm_source=chatgpt.com "File System Overview"
[6]: https://emscripten.org/docs/api_reference/Filesystem-API.html?utm_source=chatgpt.com "File System API"
[7]: https://docs.cocos.com/creator/3.6/manual/zh/editor/publish/publish-wechatgame.html?utm_source=chatgpt.com "发布到微信小游戏"
[8]: https://docs.unity.cn/cn/tuanjiemanual/Manual/WebGLPlatform.html?utm_source=chatgpt.com "团结引擎- 手册: 平台特征简介"
[9]: https://developer.chrome.com/docs/devtools/memory-problems/heap-snapshots?utm_source=chatgpt.com "Record heap snapshots | Chrome DevTools"
[10]: https://developer.android.com/tools/dumpsys?utm_source=chatgpt.com "dumpsys | Android Studio"
[11]: https://android.googlesource.com/platform/external/perfetto/%2B/refs/heads/main/docs/case-studies/memory.md?utm_source=chatgpt.com "Debugging memory usage on Android"
