【性能优化】页面首屏加载角色和怪物模型过慢(> 5秒)原因调查及优化方案 #30

Closed
opened 2026-09-15 10:50:38 +00:00 by troytt · 3 comments
Owner

问题现象

进入游戏页面(如 Act 1 关卡场景)时,需要花费较长时间(> 5 秒甚至更久)才能将玩家角色(女法师)和怪物模型及动画完全加载渲染出来。

根本原因排查分析

  1. 多重海量 MPQ 文件的 HTTP Range 重复并发拉取与元数据开销:
    • 角色模型需要从 d2char.mpq(~270 MB)拉取 DCC/COF,怪物模型需要分别从 d2data.mpq(~267 MB)、d2exp.mpq(~160 MB)、Patch_D2.mpq 等多个 MPQ 卷拉取。
    • 每次挂载 MPQ 时,前端都需要通过多次 HTTP Range 往返请求拉取 MPQ Header、HashTable、BlockTable 及 (listfile)。
    • 当前 loadMonsterArtMap 与 loadCharacterArt 各自独立构建了 new MountedArchives(),没有在全局复用已经打开和解析过的 MPQ 句柄与元数据,导致网络链路并发往返开销倍增。
  2. 浏览器主线程实时解码多图层 DCC 并动态合成 Atlas 的计算瓶颈:
    • 当前进入场景后,动态遍历怪物的各个部件(TR, HD, RA 等多达 16 部位)的 DCC 数据并在前端主线程解码,再通过 Canvas 2D / Uint8Array 逐帧 blit 合成为一张 2048x1024 的 WebGL 纹理图集。
    • 当多个怪物模型在首屏同时进行解码与纹理上传时,阻塞主线程并与场景瓦片(tileset)加载争抢带宽与 CPU。

建议解决方案

  1. 单例化与复用 MountedArchives 实例:
    • 在前端全局缓存并共享同一组 MPQ 挂载句柄与 block table,避免为不同系统重复发起相同的 HTTP Range 元数据请求。
  2. 离线烘焙预合成怪物精灵图集(Pack 预打包):
    • 参考关卡地形与 NPC 瓦片方案,在离线烘焙阶段(pack-act-assets.ts)将该关卡出没怪物的 8 向动画直接预先渲染成 monsters.png + monsters.json 精灵图集打入关卡包中,运行时直接作为单张图片拉取,将加载时间从 5 秒以上降低至 200ms 以内。
  3. 按需异步占位加载(Fallback Sprite):
    • 在怪物资源尚未加载完成前,渲染轻量占位阴影/线框,同时支持 Web Worker 解码 DCC,避免阻塞主线程动画与操作。
## 问题现象 进入游戏页面(如 Act 1 关卡场景)时,需要花费较长时间(> 5 秒甚至更久)才能将玩家角色(女法师)和怪物模型及动画完全加载渲染出来。 ## 根本原因排查分析 1. **多重海量 MPQ 文件的 HTTP Range 重复并发拉取与元数据开销**: - 角色模型需要从 `d2char.mpq`(~270 MB)拉取 DCC/COF,怪物模型需要分别从 `d2data.mpq`(~267 MB)、`d2exp.mpq`(~160 MB)、`Patch_D2.mpq` 等多个 MPQ 卷拉取。 - 每次挂载 MPQ 时,前端都需要通过多次 HTTP Range 往返请求拉取 MPQ Header、HashTable、BlockTable 及 (listfile)。 - 当前 `loadMonsterArtMap` 与 `loadCharacterArt` 各自独立构建了 `new MountedArchives()`,没有在全局复用已经打开和解析过的 MPQ 句柄与元数据,导致网络链路并发往返开销倍增。 2. **浏览器主线程实时解码多图层 DCC 并动态合成 Atlas 的计算瓶颈**: - 当前进入场景后,动态遍历怪物的各个部件(TR, HD, RA 等多达 16 部位)的 DCC 数据并在前端主线程解码,再通过 Canvas 2D / Uint8Array 逐帧 blit 合成为一张 2048x1024 的 WebGL 纹理图集。 - 当多个怪物模型在首屏同时进行解码与纹理上传时,阻塞主线程并与场景瓦片(tileset)加载争抢带宽与 CPU。 ## 建议解决方案 1. **单例化与复用 MountedArchives 实例**: - 在前端全局缓存并共享同一组 MPQ 挂载句柄与 block table,避免为不同系统重复发起相同的 HTTP Range 元数据请求。 2. **离线烘焙预合成怪物精灵图集(Pack 预打包)**: - 参考关卡地形与 NPC 瓦片方案,在离线烘焙阶段(`pack-act-assets.ts`)将该关卡出没怪物的 8 向动画直接预先渲染成 `monsters.png` + `monsters.json` 精灵图集打入关卡包中,运行时直接作为单张图片拉取,将加载时间从 5 秒以上降低至 200ms 以内。 3. **按需异步占位加载(Fallback Sprite)**: - 在怪物资源尚未加载完成前,渲染轻量占位阴影/线框,同时支持 Web Worker 解码 DCC,避免阻塞主线程动画与操作。
Author
Owner

优化决策落地方案确认

根据 Master 确认的优化方案,性能问题(加载 > 5s)按以下两步落地:

1. 前端全局单例化复用 MountedArchives 句柄(运行时优化)

  • 问题:原先 loadMonsterArtMap 与 loadCharacterArt 各自独立执行 new MountedArchives() 并通过 httpRangeSource 重复打开 d2data.mpq、d2exp.mpq、Patch_D2.mpq 和 d2char.mpq,各自拉取 block/hash table,产生成倍的 HTTP Range 往返与内存浪费。
  • 改动:
    • 在前端维护全局单例 getSharedArchives(bases) 或在场景初始化时统一装配好统一的 MountedArchives(一次性挂载 DATA + CHAR MPQ)。
    • 角色模型与所有怪物模型共用该单例,彻底消除重复的 MPQ Header / Table 请求。

2. 离线预打包怪物图集到关卡包中(根治方案:>5s 降至 <200ms)

  • 问题:在浏览器端实时解码多部位 DCC 并在线拼合 Atlas,占用大量 CPU 与主线程时间。
  • 改动:
    • 在 scripts/pack-act-assets.ts 烘焙关卡包时,检测关卡所需的 monsters.types。
    • 调用现有的 compositeMonsterAnimation 与 buildAtlas 离线解码该关卡怪物 8 向 Walk / Stand 动画,导出为 monsters-0.png(带 Act Palette 的 Indexed PNG)。
    • 在 scene.json 中记录怪物的图集布局(monsterAtlases)。
    • 前端加载场景包时,优先加载 monsters-0.png(单次 HTTP 请求,直接 GPU 上传),仅在缺失时降级回 MPQ 实时解码。
## 优化决策落地方案确认 根据 Master 确认的优化方案,性能问题(加载 > 5s)按以下两步落地: ### 1. 前端全局单例化复用 MountedArchives 句柄(运行时优化) - **问题**:原先 `loadMonsterArtMap` 与 `loadCharacterArt` 各自独立执行 `new MountedArchives()` 并通过 `httpRangeSource` 重复打开 `d2data.mpq`、`d2exp.mpq`、`Patch_D2.mpq` 和 `d2char.mpq`,各自拉取 block/hash table,产生成倍的 HTTP Range 往返与内存浪费。 - **改动**: - 在前端维护全局单例 `getSharedArchives(bases)` 或在场景初始化时统一装配好统一的 `MountedArchives`(一次性挂载 DATA + CHAR MPQ)。 - 角色模型与所有怪物模型共用该单例,彻底消除重复的 MPQ Header / Table 请求。 ### 2. 离线预打包怪物图集到关卡包中(根治方案:>5s 降至 <200ms) - **问题**:在浏览器端实时解码多部位 DCC 并在线拼合 Atlas,占用大量 CPU 与主线程时间。 - **改动**: - 在 `scripts/pack-act-assets.ts` 烘焙关卡包时,检测关卡所需的 `monsters.types`。 - 调用现有的 `compositeMonsterAnimation` 与 `buildAtlas` 离线解码该关卡怪物 8 向 Walk / Stand 动画,导出为 `monsters-0.png`(带 Act Palette 的 Indexed PNG)。 - 在 `scene.json` 中记录怪物的图集布局(`monsterAtlases`)。 - 前端加载场景包时,优先加载 `monsters-0.png`(单次 HTTP 请求,直接 GPU 上传),仅在缺失时降级回 MPQ 实时解码。
Author
Owner

修复完成说明

已在提交 d6e6248 中完成优化:

  1. 多 MPQ 句柄与索引共享缓存:引入全局 archivePromiseCache,复用已打开的 MPQ 卷句柄与 HashTable / BlockTable / (listfile) 解析,消除切换关卡与并发展开时的重复开档请求。
  2. 并发 Range 请求加速:
    • compositeMonsterAnimation 和 loadCharacterSheet 将所有层级的 DCC 数据通过 Promise.all 进行并发 HTTP Range 读取与解码。
    • loadMonsterAtlas 与 loadCharacterArt 将 walk 与 stand 动画并发合成。
    • loadMonsterArtMap 针对不同 token 实行并发加载。
  3. 安全区空载:在城镇(Town)或无怪物场景,跳过怪兽模型的 Range 请求与解码流程。
### 修复完成说明 已在提交 `d6e6248` 中完成优化: 1. **多 MPQ 句柄与索引共享缓存**:引入全局 `archivePromiseCache`,复用已打开的 MPQ 卷句柄与 HashTable / BlockTable / (listfile) 解析,消除切换关卡与并发展开时的重复开档请求。 2. **并发 Range 请求加速**: - `compositeMonsterAnimation` 和 `loadCharacterSheet` 将所有层级的 DCC 数据通过 `Promise.all` 进行并发 HTTP Range 读取与解码。 - `loadMonsterAtlas` 与 `loadCharacterArt` 将 walk 与 stand 动画并发合成。 - `loadMonsterArtMap` 针对不同 token 实行并发加载。 3. **安全区空载**:在城镇(Town)或无怪物场景,跳过怪兽模型的 Range 请求与解码流程。
Author
Owner

根治方案完成:离线预打包人物与怪物图集至 Act 资源包 (270c743)

  1. 离线打包工具 (scripts/pack-entity-assets.ts + 集成至 scripts/pack-act-assets.ts):
    • 离线合成女法师 (char-so 行走 wl + 站立 nu) 与全部 11 种怪物模型规格 (FA, FS, ZM, SI, YE, SK, CR-1hs, CR-bow, CR-2ht, GM, CW) 的 8/16 朝向 DCC+COF 图层。
    • 结合各幕调色板 (act1..5) 生成带透明索引 (tRNS chunk index 0) 的 Indexed PNG 图集 + JSON 帧坐标表,保存在 samples/d2-packs/act<N>/entities/。
  2. 运行时零解码加载 (src/scene/act-scene.ts):
    • loadCharacterArt 与 loadMonsterArtMap 在 pack 模式下直接 fetch 预烘焙 PNG + JSON,交由浏览器硬件加速 createImageBitmap 上传 WebGL 纹理,彻底绕过运行时 MPQ HTTP Range 拉取与 JS DCC/COF 逐像素解压(加载耗时 < 200ms),仅当预烘焙文件缺失时回退至 MPQ 缓存加载。
  3. 测试覆盖 (tests/packed-entity-art.test.ts):
    • 验证 Acts 1..5 全部预烘焙人物与怪物 PNG 头、IHDR 尺寸与 JSON 帧组一致性。
## 根治方案完成:离线预打包人物与怪物图集至 Act 资源包 (`270c743`) 1. **离线打包工具 (`scripts/pack-entity-assets.ts` + 集成至 `scripts/pack-act-assets.ts`)**: - 离线合成女法师 (`char-so` 行走 `wl` + 站立 `nu`) 与全部 11 种怪物模型规格 (`FA`, `FS`, `ZM`, `SI`, `YE`, `SK`, `CR-1hs`, `CR-bow`, `CR-2ht`, `GM`, `CW`) 的 8/16 朝向 DCC+COF 图层。 - 结合各幕调色板 (`act1..5`) 生成带透明索引 (`tRNS` chunk index 0) 的 Indexed PNG 图集 + JSON 帧坐标表,保存在 `samples/d2-packs/act<N>/entities/`。 2. **运行时零解码加载 (`src/scene/act-scene.ts`)**: - `loadCharacterArt` 与 `loadMonsterArtMap` 在 `pack` 模式下直接 `fetch` 预烘焙 PNG + JSON,交由浏览器硬件加速 `createImageBitmap` 上传 WebGL 纹理,**彻底绕过运行时 MPQ HTTP Range 拉取与 JS DCC/COF 逐像素解压(加载耗时 < 200ms)**,仅当预烘焙文件缺失时回退至 MPQ 缓存加载。 3. **测试覆盖 (`tests/packed-entity-art.test.ts`)**: - 验证 Acts 1..5 全部预烘焙人物与怪物 PNG 头、IHDR 尺寸与 JSON 帧组一致性。
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#30
No description provided.