修复在线游玩加载体验:加载期黑屏/空白与发行资源传输开销(#636) #639
Reference in New Issue
Block a user
Delete Branch "fix/game-play-loading"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
关联 Issue:#636
现象与实测对照
用户反馈「进入游玩…加载有点慢,一直黑屏体验不好」。用真实栈(真实 SpacetimeDB + 真实私有 OSS + 真实发行包 + 真实 Chromium)复现,链路按 4 Mbps / 100 ms 模拟(按真实线字节数延迟后投递)。夹具是真实 AGC 发行包形状:根
index.html+ 真实 Phaser 4 压缩包assets/phaser.min.js(1,375,976 B)+ 脚本 + 素材。游戏正在启动…opacity:1,区域全白空白,页面全程无加载文案assets/phaser.min.js传输Content-Encoding: none,模拟 2724 ms)If-None-Match返回 304改动
前端:两端共用同一个加载面
packages/shared/src/components/PlatformGameLoadingSurface.tsx:封面/标题 + 动效进度条 + 按时长推进的阶段文案(准备 → 加载游戏资源 → 启动游戏引擎)+ 慢加载提示;role="status"/aria-busy;样式写进packages/shared/src/components/styles.css(后台没有 Tailwind,共享组件不能只靠工具类撑外观)。GamePlayPage:一行小字换成加载面,加载上限改为共享常量PLATFORM_GAME_LOADING_TIMEOUT_MS(20 秒,GAME_PLAY_STARTUP_TIMEOUT_MS仍是同一口径的再导出);onLoad后淡入画面、超时面板与重试/返回不变。AdminGameDistributionReviewPage:试玩区包一层加载面,onLoad后才让位给画面;补 20 秒超时说明与「重新创建试玩会话」入口;新建会话重置加载态,不会沿用上一个会话的 ready。后端:发行网关的传输与条件请求
gzip时下发Content-Encoding: gzip+Vary: Accept-Encoding;图片/音频/视频等已是压缩格式的资源不压,小文件不压。versionId + 资源路径摘要的强ETag;命中If-None-Match返回 304(无正文,保留Cache-Control与ETag)。max-age=60, must-revalidate与撤销窗口都不变,差别只是 60 秒之后重复游玩不再重下整包。no-store且不给 ETag;*校验器对包内不存在的路径仍返回 404。文档与门禁
docs/project-memory/shared-memory/pitfalls.md、docs/【开发运维】本地开发验证与生产运维-2026-05-15.md、deploy/nginx/README.md与两份里程碑记录同步现行口径。scripts/check-game-distribution-ops-rollback-e2e.mjs补四条断言:压缩 +Vary、强 ETag、If-None-Match→ 304(无正文)、不接受 gzip 时不压缩。验证
cargo test -p api-server --bin api-server -- game_distributionIf-None-Match列表/弱校验/gzip;q=0、文本压缩与二进制/小文件不压、304 无正文与*对缺失路径 404)npx vitest run packages/shared/src/components/PlatformGameLoadingSurface.test.tsx src/components/game-distribution/GameDistributionPages.test.tsx apps/admin-web/src/pages/AdminGameDistributionReviewPage.test.tsxnpm run check:game-distribution-ops-rollback-e2enpm run typecheck/ admin-web typecheck /npm run check:encoding/git diff --check自查(Review 轮次)
本轮 review 发现 7 处问题(2 处会让 CI 变红)并全部修复:Rust 未过
cargo fmt、ESLint 4 处(含共享组件文件同时导出常量触发react-refresh/only-export-components)、主规范未同步发行缓存契约、nginx README 与 Pingora 口径自相矛盾、后台超时态仍转圈且aria-busy为 true、Q=0大小写、pitfalls 措辞;并按实测把大文件压缩从 level 6 降到 level 1。逐条证据与「复核过无问题」的结论见下方评论。边界与不做项
publicationRevision、审核 CAS、/games/<gameId>/路由、Cookie 拒绝与sandbox="allow-scripts"口径。Content-Encoding就跳过」收口);Pingora 网关在自身压缩关闭时会移除accept-encoding,那种部署形态下源站压缩不生效,属于网关侧口径,本次不改。.tmp/为 gitignored)。截图(真实 Chromium,4 Mbps 模拟链路)
用户端加载面(改后):
用户端画面就绪:
后台试玩加载面(改前这里是全白空白、没有任何文案):
后台试玩画面就绪:
自查结论(Review 轮次,commit
d7c4f82d7)方向与两端口径正确;本轮找出 7 处问题,全部已修并复验(其中 2 处会让 CI 直接变红),另有 4 项判定为「记录不修」。以下每条都给证据与修复。
已修
cargo fmt --check(6 处,全在新增测试)npm run check:rustfmt报 6 个 diff(源文件 4933–5145)cargo fmt后check:rustfmt绿simple-import-sort/imports×2、react-refresh/only-export-components×2npx eslint报 4 error;npm run lint:eslint(--max-warnings 0)是 CI 门禁platformGameLoadingSurfaceModel.ts(对齐platformPillBadgeModel等);删除GAME_PLAY_STARTUP_TIMEOUT_MS别名(常量别名不是字面量,正是触发那条规则的原因),测试直接用共享常量;eslint --fix修排序;全仓lint:eslint复跑绿docs/【玩法创作】平台入口与玩法链路-2026-05-15.md「下架、缓存与运维」,本次给发行网关加了压缩与条件请求,属于该契约no-store+ 浏览器/CDN 有效期 ≤60 秒的约束Vary、强 ETag 与 304,并写明max-age=60, must-revalidate与撤销窗口都不放宽;check:doc-index仍通过deploy/nginx/README.md自相矛盾:「关闭边缘压缩的 Pingora 模式」下用户字节与生产一致——实际那种模式会移除Accept-Encoding,源站也不会压pingora-gateway的normalize_accept_encoding_for_gateway_compression(压缩关闭时remove_header);pitfalls 里写对了、README 写反了aria-busy=truearia-busy没变stalled:停掉扫动动画、aria-busy=false、压暗填充;后台超时时传入;补用例Accept-Encoding权重参数名大小写不敏感(RFC 9110 §12.5.3),GZIP;Q=0被当成「接受」q=gzip;Q=0、GZIP;Q=0.000已复核、判定无问题(附证据)
(versionId, 资源路径)摘要,不是字节摘要,前提是「冻结后同版本同路径字节不变」。核对:reset_package_upload只允许未确认版本重置(注释与实现一致);上传对已存在且长度不同的对象返回 409PACKAGE_OBJECT_MISMATCH,长度相同则跳过;发行网关只服务published。→ 不会出现「同 ETag 不同字节」。If-None-Match: *对包内不存在的路径仍 404(有用例);换版 → ETag 变 → 200;下架 → 404。max-age/must-revalidate未动。should_allow_gateway_compression_for_response只在 200 且响应无Content-Encoding时压;nginx 同理;两者的 304/206 都不压。no-store且无 ETag;sandbox、Cookie 403、CSP/CORP/CORS 不变(check:game-distribution-ops-rollback-e2e47 PASS)。check:encoding、check:doc-index、check:production-ops、check:nginx-spa-routes、check:pingora-route-parity、check:game-distribution-dto-parity、check:preview-deployer、check:maintenance-page、check:npm-workspaces、check:spacetime-schema、check:rustfmt、typecheck(含 admin-web 口径)、lint:eslint全绿。记录不修(含理由)
≥2 MiB用 level 1(zlib 端实测约 1/3 耗时、压缩比只差约 3%),小文件仍 level 6;并把「优先缓存压缩结果而不是继续放宽级别」写进 pitfalls。identity;q=0(客户端明确拒绝 identity)时对 <1 KiB 文本仍发 identity:严格说该压或 406。现实客户端不发这种组合,未改。check:git-hooks(Windows 临时目录 EBUSY)与check:generated-bindings(worktree 缺未入库的claude-agent随包资源)——在 master 上同样红,与本 PR 无关;CI 以 PR 上的 Project CI 为准。lgtm