[性能/资源] 优化首屏加载耗时与资源体积(.r8 实体图集无损压缩、首帧关键路径解耦与按需懒加载) #505

Open
opened 2026-09-28 01:55:44 +00:00 by troytt · 0 comments
Owner

背景与问题描述 (Background & Problem Statement)

随着全 5 幕关卡、7 大职业、全量怪物、技能飞弹、光环覆盖层与 UI 面板资源的持续完善,网页端首次打开进入游戏(boot() -> runScene() -> loop.start())需要加载的资源体积越来越大,首屏等待时间明显变长。

经全链路实测诊断,当前打开网页加载缓慢主要由以下 5 个核心瓶颈 叠加导致:

瓶颈维度 现状实测数据 根因定位
1. .r8 实体图集未压缩裸传 samples/d2-packs/entities/ 共 648.98 MB(单个角色 char-so.r8 7.9 MB、char-dz.r8 12.3 MB;单个怪物如 monster-0e-hth.r8 高达 23.2 MB) scripts/pack-entity-assets.ts 将 8-bit 调色板索引直存为未压缩 .r8 裸字节数组(含大量透明像素 0),Vite 开发服与普通静态托管均不对 .r8 后缀启用流式压缩。实测仅做无损 Gzip 即可从 648.98 MB 降至 83.44 MB(缩减 87.1%,立省 565.5 MB)。
2. 首帧渲染被长串行链路阻塞 loop.start() 必须等待 10+ 个串行步骤 全部完成才画出第一帧 在 src/scene/act-scene.ts(第 7886–9499 行)中,主循环启动前串行阻塞等待:同步解析全量掉落表 TSV -> 地图全部 objectPages -> 小地图图集 -> 全部 40 张 UI PNG(含未打开的背包/角色/储物箱/赫拉迪克方块/任务/传送站/商店底图) -> 当前关卡所有怪物种类的巨型 .r8 图集 -> 角色 .r8 图集。
3. 启动瞬间百级并发请求风暴(带宽挤兑) 首屏一进入就无节制并发拉取 38 MB+ 非首屏纹理(上百个 HTTP 请求) act-scene.ts(第 7352–7362 行)在 runScene() 开头立即全量拉取所有飞弹(public/missiles 19 MB)和所有光环/诅咒覆盖层(public/overlays 19 MB);src/ui/hud-manager.ts(第 431–433 行)立即拉取全部 30 个技能图标与技能树背景图(public/skills 3.5 MB),严重挤占 HTTP 连接池与地图/主角的关键带宽。
4. 重复拉取与无效 404 探测 每次启动浪费 ~1.6 MB 重复下载 + 8 次必然 404 请求 ① BAKED_UI_MANIFEST(684 KB)已打包进 JS(src/ui/baked-ui-meta.ts),但 HudManager.init() 仍通过网络请求 /ui/manifest.json(703 KB);
② /ui/items-atlas.png(702 KB)在 HudManager.init() 与 loadItemsAtlas() 中被重复请求并上传 GPU 两次;
③ index.json(204 KB)在 populatePackSelectors()(act-scene.ts:9594)中未走 getPackIndex() 缓存,被拉取两次;
④ minimap.loadAtlases(renderer, "/automap")(src/ui/minimap.ts:228-246)对 8 张小地图先请求不存在的 /automap/*.png 触发 8 次 404 后才回退到 /ui/automap-*.png。
5. 首屏主线程同步解析与巨型静态 JS 包 首屏 JS 体积 ~2.8 MB(acts.js 1.3 MB + embedded-drop-tables.js 1.2 MB + skill-calc-engine.js 306 KB) act-scene.ts:9985 在发起任何网络请求前,先在主线程同步调用 getEmbeddedDropTables() 将 1.1 MB 的原始 TSV 字符串用 TextEncoder 编码并解析成掉落表与商店表,阻塞首屏网络请求发起。

核心需求与设计方案 (Requirements & Proposed Design)

阶段 1:消除重复请求、404 探测���主线程同步阻塞

  1. 复用已内置的 BAKED_UI_MANIFEST,消除 /ui/manifest.json(703 KB)网络请求:
    • HudManager.init()(src/ui/hud-manager.ts:422-429)直接使用已导入的 BAKED_UI_MANIFEST,不再发起 fetch("/ui/manifest.json")。
  2. 合并 items-atlas.png(702 KB),消除重复下载与双份 GPU 纹理:
    • 统一 HudManager 与 act-scene.ts(loadItemsAtlas)的物品图集加载入口,复用单份 GPU 纹理与 ImageBitmap。
  3. 修复 index.json(204 KB)二次拉取:
    • 将 populatePackSelectors()(src/scene/act-scene.ts:9594)中的 fetchJson 改为调用带内存缓存的 getPackIndex(packBase)。
  4. 修复小地图图集 8 次 404 请求:
    • 将 minimap.loadAtlases(renderer, "/ui") 首选路径对齐为实际存放目录 /ui/automap-*.png,消除每次进图 8 个 /automap/automap-*.png 的 404 报错。
  5. 将 getEmbeddedDropTables() 移出首屏网络请求前的同步阻塞点并动态分包:
    • 先立即发起 loadPackRuntime() 网络请求;将 embedded-drop-tables.ts(1.2 MB)改为异步并行加载或在首帧渲染后初始化,使首包 JS 体积减少 1.2 MB。

阶段 2:.r8 实体图集 Gzip 无损压缩存储与流式解压(缩减 87.1% 实体体积)

  • 保持 1.13c 地面真值契约不变:.r8 存储 8-bit 原始调色板索引(gl.R8),用于片元着色器精确执行 Act 调色板映射与 invColormap(Boss 染色、冰冻/中毒/石化色调偏移)。
  • 离线打包压缩:在 scripts/pack-entity-assets.ts 写出 .r8 时采用 zlib.gzipSync(packed.r8, { level: 6 }) 无损压缩:
    • char-so.r8:7.90 MB -> 1.55 MB(缩小 80.4%)
    • monster-0e-hth.r8:23.24 MB -> 1.98 MB(缩小 91.5%)
    • samples/d2-packs/entities/ 总体积:648.98 MB -> 83.44 MB(节省 565.53 MB)
  • 运行时自适应解压:在 src/scene/act-scene.ts 的 fetchR8Sheet() 中检测 Gzip 魔数(0x1f 0x8b),通过浏览器原生 DecompressionStream("gzip") 解压为 Uint8Array(同时向下兼容未压缩 .r8),并严格校验解压后 byteLength === width * height。
  • 验证脚本同步支持:scripts/verify-entity-packs.ts 支持检测 Gzip 魔数并解压校验尺寸与像素契约。

阶段 3:首屏渲染关键路径解耦(毫秒级进入游戏画面)

  1. 地图与物件图集优先级重排(视口优先):
    • runtime.loadPages() 优先加载 priorityPages(出生点首屏视口所需的地板与墙壁图集页),不再让全图所有 objectPages 阻塞首屏视口地板页;其余图集页在首帧后后台补齐。
  2. 角色提前并行拉取 & 怪物图集后台流式就绪:
    • 在 runScene() 开头即并行发起当前主角的 loadCharacterArt() 请求;
    • 怪物图集 loadMonsterArtMap() 改为后台流式加载(每加载完一种怪物立即渲染上屏),不阻塞主角与地图的首帧呈现(同时保证无头浏览器验证脚本的确定性等待契约)。
  3. UI 贴图拆分为「首屏常驻 HUD」与「二级面板按需/后台加载」:
    • 首屏仅等待底部控制栏(ctrl-panel.png)、生命/法力球(orbs.png)、字体与状态图标;
    • 默认关闭的二级面板(char-sheet.png、inv-sheet.png、stash-bg.png、cube-bg.png、quest-bg.png、quests-atlas.png、waypoint-bg.png、vendor-bg.png)移至首帧渲染后后台加载或首次打开面板时等待。

阶段 4:消除启动请求风暴——飞弹(19 MB)与覆盖层(19 MB)按需懒加载

  1. 飞弹图集(missiles/ 19 MB)按需加载:
    • 移除 runScene() 启动时全量拉取 MISSILE_METAS 的逻辑;封装 ensureMissileArtLoaded()(带请求去重 Promise 缓存),在释放/装备对应技能时按需加载,首帧后仅预热当前绑定的初始技能飞弹。
  2. 光环/诅咒/状态覆盖层(overlays/ 19 MB)按需加载:
    • 移除 runScene() 启动时全量拉取 OVERLAY_METAS 的逻辑;封装 ensureOverlayArtLoaded(),在触发 triggerOverlay() 或光环状态时按需加载。
  3. 技能图标(skills/icon_*.png)按需加载:
    • 首屏仅加载底栏当前绑定的技能图标,完整技能树图标与背景图延后至首帧后或打开技能树面板时加载。

预期收益与验收标准 (Acceptance Criteria)

  • samples/d2-packs/entities/ 磁盘与传输体积从 ~649 MB 降至 ~83 MB(缩减 >85%),且 npm run verify:entity-packs 校验 100% 通过。
  • 首屏进入 Act 1 罗格营地的关键路径网络下载量从 ~65–90 MB 降至 ~5 MB,首屏并发请求数减少 85%+。
  • 消除 /ui/manifest.json、/ui/items-atlas.png、index.json 的重复网络请求,消除小地图 /automap/*.png 的 8 次 404 请求。
  • 飞弹(Missiles)、覆盖层(Overlays)、二级 UI 面板实现按需/延后加载,不阻塞首帧 firstFrameMs 且不影响战斗与面板交互。
  • 全量单元测试与无头浏览器验证(npm test、npm run verify:acts、npm run verify:animation、npm run verify:ground-browser)100% 通过,严格保持 1.13c 视觉与逻辑对齐。
## 背景与问题描述 (Background & Problem Statement) 随着全 5 幕关卡、7 大职业、全量怪物、技能飞弹、光环覆盖层与 UI 面板资源的持续完善,网页端首次打开进入游戏(`boot()` -> `runScene()` -> `loop.start()`)需要加载的资源体积越来越大,首屏等待时间明显变长。 经全链路实测诊断,当前打开网页加载缓慢主要由以下 **5 个核心瓶颈** 叠加导致: | 瓶颈维度 | 现状实测数据 | 根因定位 | | :--- | :--- | :--- | | **1. `.r8` 实体图集未压缩裸传** | `samples/d2-packs/entities/` 共 **648.98 MB**(单个角色 `char-so.r8` **7.9 MB**、`char-dz.r8` **12.3 MB**;单个怪物如 `monster-0e-hth.r8` 高达 **23.2 MB**) | `scripts/pack-entity-assets.ts` 将 8-bit 调色板索引直存为未压缩 `.r8` 裸字节数组(含大量透明像素 `0`),Vite 开发服与普通静态托管均不对 `.r8` 后缀启用流式压缩。实测仅做无损 Gzip 即可从 **648.98 MB 降至 83.44 MB(缩减 87.1%,立省 565.5 MB)**。 | | **2. 首帧渲染被长串行链路阻塞** | `loop.start()` 必须等待 **10+ 个串行步骤** 全部完成才画出第一帧 | 在 `src/scene/act-scene.ts`(第 7886–9499 行)中,主循环启动前串行阻塞等待:同步解析全量掉落表 TSV -> 地图全部 `objectPages` -> 小地图图集 -> 全部 **40 张 UI PNG**(含未打开的背包/角色/储物箱/赫拉迪克方块/任务/传送站/商店底图) -> 当前关卡**所有怪物种类**的巨型 `.r8` 图集 -> 角色 `.r8` 图集。 | | **3. 启动瞬间百级并发请求风暴(带宽挤兑)** | 首屏一进入就无节制并发拉取 **38 MB+** 非首屏纹理(上百个 HTTP 请求) | `act-scene.ts`(第 7352–7362 行)在 `runScene()` 开头立即全量拉取所有飞弹(`public/missiles` **19 MB**)和所有光环/诅咒覆盖层(`public/overlays` **19 MB**);`src/ui/hud-manager.ts`(第 431–433 行)立即拉取全部 30 个技能图标与技能树背景图(`public/skills` **3.5 MB**),严重挤占 HTTP 连接池与地图/主角的关键带宽。 | | **4. 重复拉取与无效 404 探测** | 每次启动浪费 **~1.6 MB** 重复下载 + **8 次必然 404 请求** | ① `BAKED_UI_MANIFEST`(684 KB)已打包进 JS(`src/ui/baked-ui-meta.ts`),但 `HudManager.init()` 仍通过网络请求 `/ui/manifest.json`(**703 KB**);<br>② `/ui/items-atlas.png`(**702 KB**)在 `HudManager.init()` 与 `loadItemsAtlas()` 中被**重复请求并上传 GPU 两次**;<br>③ `index.json`(**204 KB**)在 `populatePackSelectors()`(`act-scene.ts:9594`)中未走 `getPackIndex()` 缓存,被拉取两次;<br>④ `minimap.loadAtlases(renderer, "/automap")`(`src/ui/minimap.ts:228-246`)对 8 张小地图先请求不存在的 `/automap/*.png` 触发 8 次 404 后才回退到 `/ui/automap-*.png`。 | | **5. 首屏主线程同步解析与巨型静态 JS 包** | 首屏 JS 体积 **~2.8 MB**(`acts.js` 1.3 MB + `embedded-drop-tables.js` 1.2 MB + `skill-calc-engine.js` 306 KB) | `act-scene.ts:9985` 在发起任何网络请求前,先在主线程同步调用 `getEmbeddedDropTables()` 将 1.1 MB 的原始 TSV 字符串用 `TextEncoder` 编码并解析成掉落表与商店表,阻塞首屏网络请求发起。 | --- ## 核心需求与设计方案 (Requirements & Proposed Design) ### 阶段 1:消除重复请求、404 探测���主线程同步阻塞 1. **复用已内置的 `BAKED_UI_MANIFEST`,消除 `/ui/manifest.json`(703 KB)网络请求**: - `HudManager.init()`(`src/ui/hud-manager.ts:422-429`)直接使用已导入的 `BAKED_UI_MANIFEST`,不再发起 `fetch("/ui/manifest.json")`。 2. **合并 `items-atlas.png`(702 KB),消除重复下载与双份 GPU 纹理**: - 统一 `HudManager` 与 `act-scene.ts`(`loadItemsAtlas`)的物品图集加载入口,复用单份 GPU 纹理与 `ImageBitmap`。 3. **修复 `index.json`(204 KB)二次拉取**: - 将 `populatePackSelectors()`(`src/scene/act-scene.ts:9594`)中的 `fetchJson` 改为调用带内存缓存的 `getPackIndex(packBase)`。 4. **修复小地图图集 8 次 404 请求**: - 将 `minimap.loadAtlases(renderer, "/ui")` 首选路径对齐为实际存放目录 `/ui/automap-*.png`,消除每次进图 8 个 `/automap/automap-*.png` 的 404 报错。 5. **将 `getEmbeddedDropTables()` 移出首屏网络请求前的同步阻塞点并动态分包**: - 先立即发起 `loadPackRuntime()` 网络请求;将 `embedded-drop-tables.ts`(1.2 MB)改为异步并行加载或在首帧渲染后初始化,使首包 JS 体积减少 1.2 MB。 ### 阶段 2:`.r8` 实体图集 Gzip 无损压缩存储与流式解压(缩减 87.1% 实体体积) - **保持 1.13c 地面真值契约不变**:`.r8` 存储 8-bit 原始调色板索引(`gl.R8`),用于片元着色器精确执行 Act 调色板映射与 `invColormap`(Boss 染色、冰冻/中毒/石化色调偏移)。 - **离线打包压缩**:在 `scripts/pack-entity-assets.ts` 写出 `.r8` 时采用 `zlib.gzipSync(packed.r8, { level: 6 })` 无损压缩: - `char-so.r8`:**7.90 MB -> 1.55 MB**(缩小 80.4%) - `monster-0e-hth.r8`:**23.24 MB -> 1.98 MB**(缩小 91.5%) - `samples/d2-packs/entities/` 总体积:**648.98 MB -> 83.44 MB**(节省 **565.53 MB**) - **运行时自适应解压**:在 `src/scene/act-scene.ts` 的 `fetchR8Sheet()` 中检测 Gzip 魔数(`0x1f 0x8b`),通过浏览器原生 `DecompressionStream("gzip")` 解压为 `Uint8Array`(同时向下兼容未压缩 `.r8`),并严格校验解压后 `byteLength === width * height`。 - **验证脚本同步支持**:`scripts/verify-entity-packs.ts` 支持检测 Gzip 魔数并解压校验尺寸与像素契约。 ### 阶段 3:首屏渲染关键路径解耦(毫秒级进入游戏画面) 1. **地图与物件图集优先级重排(视口优先)**: - `runtime.loadPages()` 优先加载 `priorityPages`(出生点首屏视口所需的地板与墙壁图集页),不再让全图所有 `objectPages` 阻塞首屏视口地板页;其余图集页在首帧后后台补齐。 2. **角色提前并行拉取 & 怪物图集后台流式就绪**: - 在 `runScene()` 开头即并行发起当前主角的 `loadCharacterArt()` 请求; - 怪物图集 `loadMonsterArtMap()` 改为后台流式加载(每加载完一种怪物立即渲染上屏),不阻塞主角与地图的首帧呈现(同时保证无头浏览器验证脚本的确定性等待契约)。 3. **UI 贴图拆分为「首屏常驻 HUD」与「二级面板按需/后台加载」**: - 首屏仅等待底部控制栏(`ctrl-panel.png`)、生命/法力球(`orbs.png`)、字体与状态图标; - 默认关闭的二级面板(`char-sheet.png`、`inv-sheet.png`、`stash-bg.png`、`cube-bg.png`、`quest-bg.png`、`quests-atlas.png`、`waypoint-bg.png`、`vendor-bg.png`)移至首帧渲染后后台加载或首次打开面板时等待。 ### 阶段 4:消除启动请求风暴——飞弹(19 MB)与覆盖层(19 MB)按需懒加载 1. **飞弹图集(`missiles/` 19 MB)按需加载**: - 移除 `runScene()` 启动时全量拉取 `MISSILE_METAS` 的逻辑;封装 `ensureMissileArtLoaded()`(带请求去重 Promise 缓存),在释放/装备对应技能时按需加载,首帧后仅预热当前绑定的初始技能飞弹。 2. **光环/诅咒/状态覆盖层(`overlays/` 19 MB)按需加载**: - 移除 `runScene()` 启动时全量拉取 `OVERLAY_METAS` 的逻辑;封装 `ensureOverlayArtLoaded()`,在触发 `triggerOverlay()` 或光环状态时按需加载。 3. **技能图标(`skills/icon_*.png`)按需加载**: - 首屏仅加载底栏当前绑定的技能图标,完整技能树图标与背景图延后至首帧后或打开技能树面板时加载。 --- ## 预期收益与验收标准 (Acceptance Criteria) - [ ] `samples/d2-packs/entities/` 磁盘与传输体积从 **~649 MB 降至 ~83 MB**(缩减 >85%),且 `npm run verify:entity-packs` 校验 100% 通过。 - [ ] 首屏进入 Act 1 罗格营地的关键路径网络下载量从 **~65–90 MB 降至 ~5 MB**,首屏并发请求数减少 **85%+**。 - [ ] 消除 `/ui/manifest.json`、`/ui/items-atlas.png`、`index.json` 的重复网络请求,消除小地图 `/automap/*.png` 的 8 次 404 请求。 - [ ] 飞弹(Missiles)、覆盖层(Overlays)、二级 UI 面板实现按需/延后加载,不阻塞首帧 `firstFrameMs` 且不影响战斗与面板交互。 - [ ] 全量单元测试与无头浏览器验证(`npm test`、`npm run verify:acts`、`npm run verify:animation`、`npm run verify:ground-browser`)100% 通过,严格保持 1.13c 视觉与逻辑对齐。
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#505
No description provided.