perf(scene): 解决首屏阻塞式 166 次 MPQ Range 请求导致的地图载入卡顿 #117

Closed
opened 2026-09-19 02:58:47 +00:00 by troytt · 1 comment
Owner

缺陷背景与症状

用户反馈访问地图页面(如 http://localhost:8080/acts.html?act=1&level=2-act-1-wilderness-1-var1 鲜血荒地)时载入十分缓慢,页面出现数秒的挂起卡顿。

根因排查与真实指标

  1. 排除内存问题 (Non-OOM):
    • 宿主机系统可用内存 72 GB / 96 GB,进程无任何内存压力。
    • 浏览器 JS Heap 实际使用量仅 24.55 MB / 4,192 MB (0.5%),渲染管线与内存完全健康。
  2. 瓶颈定位:首屏 166 次 MPQ Range 分块请求瀑布流阻塞:
    • 在 Issue #107 引入真实掉落管线时,act-scene.ts:1739 在首屏临界路径写死了阻塞调用:
      const dropArchives = await getMountedDataArchives(...)
      const dropTables = await loadDropTables(dropArchives)
      
    • 预烘焙资源包模式(Pack 模式)原本无需触碰原始 MPQ,但此处强行挂起,向 d2data.mpq / d2exp.mpq 发起了 166 次 206 Partial Content 分块网络请求,现场解压 16 张 TXT 数据表,在开发态和远程网络代理下耗时高达 5~8 秒。
    • 且 dropTables 没有做全局会话缓存。
  3. 时机不合理:
    • GameEngine 在第 0 帧(地图渲染与玩家出生巡游)根本不需要掉落表,dropTables 仅在怪物死亡(onMonsterSlain)时按需结算。

修复方案

  1. 全局单例记忆化缓存:建立 getSharedDropTables(bases) 全局 Promise 单例缓存,整个会话期间只挂载并解析一次。
  2. 异步后台解耦首屏:从 runScene 首屏阻塞链中移出 await loadDropTables,地图与瓦片立即出画面;后台并行拉取并注入 engine.opts.dropTables。
  3. 在 boot() 早期自动预热:在页面刚进入加载 index.json 时即在后台静默发起预热,使玩家接触怪物前掉落表早已加载就绪。
### 缺陷背景与症状 用户反馈访问地图页面(如 `http://localhost:8080/acts.html?act=1&level=2-act-1-wilderness-1-var1` 鲜血荒地)时载入十分缓慢,页面出现数秒的挂起卡顿。 ### 根因排查与真实指标 1. **排除内存问题 (Non-OOM)**: - 宿主机系统可用内存 72 GB / 96 GB,进程无任何内存压力。 - 浏览器 JS Heap 实际使用量仅 **24.55 MB** / 4,192 MB (0.5%),渲染管线与内存完全健康。 2. **瓶颈定位:首屏 166 次 MPQ Range 分块请求瀑布流阻塞**: - 在 Issue #107 引入真实掉落管线时,`act-scene.ts:1739` 在首屏临界路径写死了阻塞调用: ```typescript const dropArchives = await getMountedDataArchives(...) const dropTables = await loadDropTables(dropArchives) ``` - 预烘焙资源包模式(Pack 模式)原本无需触碰原始 MPQ,但此处强行挂起,向 `d2data.mpq` / `d2exp.mpq` 发起了 **166 次 206 Partial Content 分块网络请求**,现场解压 16 张 TXT 数据表,在开发态和远程网络代理下耗时高达 5~8 秒。 - 且 `dropTables` 没有做全局会话缓存。 3. **时机不合理**: - `GameEngine` 在第 0 帧(地图渲染与玩家出生巡游)根本不需要掉落表,`dropTables` 仅在怪物死亡(`onMonsterSlain`)时按需结算。 ### 修复方案 1. **全局单例记忆化缓存**:建立 `getSharedDropTables(bases)` 全局 Promise 单例缓存,整个会话期间只挂载并解析一次。 2. **异步后台解耦首屏**:从 `runScene` 首屏阻塞链中移出 `await loadDropTables`,地图与瓦片立即出画面;后台并行拉取并注入 `engine.opts.dropTables`。 3. **在 `boot()` 早期自动预热**:在页面刚进入加载 `index.json` 时即在后台静默发起预热,使玩家接触怪物前掉落表早已加载就绪。
Author
Owner

修复完成并已合并至 main

  • Commit: 79bf8f4
  • 变更摘要:
    1. 首屏临界路径解耦:将 loadDropTables 移出 runScene 首屏同步阻塞链,使得第 0 帧地图瓦片渲染和玩家出生无需等待 166 次 MPQ Range 分块请求,出画面速度直接缩减至 400ms 左右。
    2. 全局单例记忆化缓存:实现 getSharedDropTables(bases),全会话期间仅在首次挂载并解析一次,切关卡与重新进图时开销降至 0ms。
    3. 早期非阻塞预热与动态注入:在 boot() 最前端发起后台静默预热,并在 GameEngine 增加 setDropTables 支持后台 Promise 敲定后的无感注入。
    4. 回归防护单测:新增 tests/scene-drop-preload.test.ts,验证单例缓存防重载与延迟安全注入。
  • 验证结果:
    • npm run typecheck: 0 errors
    • npx vitest run: 102 passed, 1 skipped (1598 passed, 2 skipped, 0 failed)
    • 实测鲜血荒地出画面耗时由数秒降至 413 ms。
### 修复完成并已合并至 main - **Commit**: `79bf8f4` - **变更摘要**: 1. **首屏临界路径解耦**:将 `loadDropTables` 移出 `runScene` 首屏同步阻塞链,使得第 0 帧地图瓦片渲染和玩家出生无需等待 166 次 MPQ Range 分块请求,出画面速度直接缩减至 **400ms 左右**。 2. **全局单例记忆化缓存**:实现 `getSharedDropTables(bases)`,全会话期间仅在首次挂载并解析一次,切关卡与重新进图时开销降至 0ms。 3. **早期非阻塞预热与动态注入**:在 `boot()` 最前端发起后台静默预热,并在 `GameEngine` 增加 `setDropTables` 支持后台 Promise 敲定后的无感注入。 4. **回归防护单测**:新增 `tests/scene-drop-preload.test.ts`,验证单例缓存防重载与延迟安全注入。 - **验证结果**: - `npm run typecheck`: 0 errors - `npx vitest run`: 102 passed, 1 skipped (1598 passed, 2 skipped, 0 failed) - 实测鲜血荒地出画面耗时由数秒降至 **413 ms**。
Sign in to join this conversation.
No Label
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: troytt/diablo2-web#117
No description provided.