docs(perf): archive initial-load diagnosis and remediation plan

Capture audit evidence, performance baseline, and U1–U7 remediation plan
without business code changes so the work can land on dev before U2.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
weichen
2026-09-17 08:08:20 +00:00
co-authored by Cursor
parent d0ef6961b4
commit 3445d386ea
15 changed files with 6951 additions and 0 deletions
@@ -0,0 +1,41 @@
---
title: "首次加载性能诊断与改进方案"
status: done
created: 2026-09-17
updated: 2026-09-17
owner: Codex
branch: feature/initial-load-performance-plan
worktree: .worktrees/feature-initial-load-performance-plan
mutex: loading-performance-audit
---
# 首次加载性能诊断与改进方案
## 目标
以源码、现有容器静态资源和只读浏览器测量定位首次加载瓶颈,输出可审核的分期改进方案。
## 范围
登录前首屏、登录后初始化、项目/文档首次打开、静态资源交付;区分事实与待验证假设。
## 非目标
不修改业务代码,不重启/重建共享服务,不触发真实对话或文档写任务,不提交或推送其他人的工作。
## 步骤
1. [x] 阅读仓库启动链;独立 dev 基线 worktree,主仓保留其他 Agent 分支。
2. [x] 采集数值基线和源码证据。
3. [x] 写 audit 和 remediation,同步 CURRENT,归档任务。
## 验收标准
- [x] 原因有文件位置或运行时测量支持,未覆盖环境明确标注。
- [x] 方案包含优先级、改动边界、性能预算、回归门禁和回滚。
## 证据 / 备注
仅诊断与方案,不代表优化已实施。fetch 后 origin/dev 落后本地 dev;不拉回或覆盖本地工作。worktree 从创建瞬间的 dev 建立。
## 交付
- 诊断:`.doc/audit/2026-09-17-initial-load-performance-audit.md`
- 方案:`.doc/remediation/2026-09-17-initial-load-performance-plan.md`
- 报告:`.doc/test-reports/2026-09-17-initial-load-performance-baseline.md`
- 本轮仅方案;没有业务代码、没有部署、未提交、未合dev、未推送。主仓仅同步本任务认领。
- 临时3199 Vite已停止;既有3100/8080/8081未重启。未占集成槽。
@@ -0,0 +1,117 @@
---
title: "首次加载慢:静态资源、字体与初始化请求诊断"
status: done
created: 2026-09-17
updated: 2026-09-17
severity: high
---
# 首次加载性能诊断
## 结论与适用范围
当前已复现的主要问题在浏览器加载入口:登录页也下载约 2.82 MB 未压缩 JavaScript;外部 Google Fonts CSS 阻塞页面加载;若用户访问 Vite 3100,首访进一步扩大为 207 个资源请求、12.24 MB。冷缓存与热缓存差异很大,与“尤其第一次慢”一致。
登录后还有已确认的请求放大和后端重复工作,但没有登录态浏览器瀑布图或 SQL 耗时,不能声称已量化其贡献。用户已确认 3100/8080/8081,系统重启后第一次访问约十数秒;设备、网络与“系统重启”包含宿主机还是仅应用尚未确认;本次未复现用户原始环境的完整等待时间。
- 主仓读取时 `feature/rdev-core@52f0fbc`;其他 Agent 正在工作,未切换其分支。
- 文档独立 worktree 基线为 `dev@d0ef696`。创建前已 fetch;本地 dev 有未推送提交,未 reset/覆盖。
- 实测现有 8080/8081/8000 构建入口与 3100 Vite;以资源 hash 为运行产物标识,**没有证据证明容器恰好等于上述源码 commit**。
- “首次”定义为新浏览器 context、空 HTTP 缓存;没有重启服务、清空 DB 缓存或重启 Vite,故不是服务/依赖预构建冷启动。
- 未登录、没有调用对话/文档写接口;浏览器探针未使用真实用户会话。
## 实测基线
工具:隔离 Playwright v1.63.0 Docker 浏览器,host 网络访问既有服务。宿主浏览器缺 libatk,改用本机已有浏览器镜像;未修改应用容器。每个场景一个样本,仅诊断,不是 p95 或交付验收。
| 场景 | 登录输入框可见 | FCP | 资源数 | transferSize 合计 |
|---|---:|---:|---:|---:|
| 8080 本机冷缓存 | 662 ms | 664 ms | 4 | 2,857,761 B |
| 同 context 再次导航 | 62 ms | 60 ms | 5 | 353 B |
| 8080 CDP 10 Mbps / 80 ms 冷缓存 | 2,636 ms | 2,636 ms | 4 | 2,857,761 B |
| 同 context 再次导航 | 60 ms | 60 ms | 5 | 353 B |
| 8080 CDP 2 Mbps / 80 ms 冷缓存 | 11,776 ms | 11,776 ms | 4 | 2,857,761 B |
| 3100 本机冷缓存 | 659 ms | 652 ms | 207 | 12,238,086 B |
| 3100 同 context 再次导航 | 162 ms | 160 ms | 208 | 52,253 B |
transferSize 是浏览器 Resource Timing 可观测资源求和,不含顶层 HTML,不等于全链路抓包流量。输入框可见按导航命令开始至 selector 可见计时,FCP 来自页面 Performance API,两者采样时钟和帧提交不同。CDP 限速为模拟设置,不能代替真实公网 RTT;本机 document TTFB 仍约 1–3 ms。首次/再次均保持已有服务运行,未运行 Vite 冷预构建基准。
### F1. 入口包过大且未按需加载(高置信)
- `web/src/main.tsx:2` 静态导入 App;`web/src/App.tsx:98,103,108,145-146` 静态导入导出、系统管理、文件预览等模块;未登录判断直到 `App.tsx:3461`,无法避免入口依赖下载和求值。
- `web/src/features/replyExport/download.ts:1` → docx;`web/src/engine/docxRenderer.ts:19-22` → docx、KaTeX、Mermaid;`web/src/engine/xlsxRenderer.ts:1` → XLSX。
- `web/src/components/doc/preview/FilePreviewPanel.tsx:19-24` 全部静态导入预览器;DocxViewer、ExcelViewer 各自静态导入重依赖。
- 浏览器访问登录页即可观测大 JS。3100 瀑布中含 katex.js 485,944 B、xlsx.js 870,702 B(decodedBodySize)。并非仅仅源码中出现依赖但构建已经排除。
- 8080 主 JS `/assets/index-BLnmtk_3.js` = **2,820,055 B**;8081 = 2,805,141 B;8000 = 2,798,137 B。
因果链:空缓存 → 登录视图与全部工具共入口 → 下载/解析无关依赖 → 网络较慢时登录表单迟迟出现。10 Mbps 下主 JS 下载约 2,373 ms;2 Mbps 首屏约 11.78 秒。没有采集 CPU trace,不能进一步把时间分解成具体库的执行成本。
### F2. 静态响应无压缩、无明确长缓存策略(高置信)
- 三个构建入口均携带 `Accept-Encoding: gzip, br` 请求主 JS,响应没有 `Content-Encoding`,`Content-Length` 为原始字节数。
- `internal/server/server.go:344-384` 使用 `http.ServeFile`,没有静态压缩协商或显式 Cache-Control。
- 同一 8080 JS 离线 gzip 后 **796,146 B**,相对原始包减少约 **71.8%**。这是体积估算,不是已部署优化结果,也不是端到端提速承诺。
- 资源有 Last-Modified,没有显式 Cache-Control/ETag。本次再次导航已命中浏览器缓存;**不能写成“完全没有缓存”**。问题是版本化资源缺少可预测的长期缓存契约。
源站是上述行为,未知用户实际入口是否另有反向代理压缩。优先在真正提供构建资源的层验证和修复。
### F3. 外部字体 CSS 可放大首次白屏(高置信机制,用户网络贡献待测)
- `web/index.html:9-15` 的 Google Fonts 是 head 中普通 stylesheet。`display=swap` 控制字体显示,不会让外链 CSS 本身变为非阻塞。
- 本机自然请求字体 CSS 耗时 **508 ms**,DOMContentLoaded 609 ms,登录可见 662 ms。
- 浏览器拦截并立即失败字体 CSS:登录可见 **210 ms**。Playwright route 会禁用 HTTP cache,因此这个场景不能用于热缓存收益对比,也不能据单样本得出精确净收益。
- 控制实验仅让字体 CSS 等待 5 秒再失败,其余请求正常:DOMContentLoaded **5,111 ms**、登录可见 **5,161 ms**。证明该依赖可以把外部等待传到登录首屏,**不表示自然字体请求真的用了 5 秒**。
### F4. 登录后项目成员扇出并放大会话同步(源码已证实,耗时待测)
`web/src/App.tsx:524-540` 获取项目后,对每个项目调用成员接口;`:383-387` 每次成员结果用 map 生成新的 projects 数组;`:560-590` 的会话列表 effect 依赖 projects,因此成员返回可重复触发会话列表请求。取消标志仅忽略部分返回,没有 AbortController 终止已发请求。React 批处理可能合并更新,**不能写成固定 N 次会话请求**。
项目数 N 增大时,初始成员请求为 N,叠加会话重取、setProjects 及本地持久化。`web/src/data/storage.ts:63` JSON.stringify + localStorage.setItem,`App.tsx:343` 随 projects 更新保存。持久化是否成为长任务还须 profile,不能预设为主要根因。
### F5. 认证有重复查询和同 session 写放大(源码已证实,耗时待测)
`internal/server/auth_middleware.go:48,62` 已认证并保存 principal;`internal/server/auth_handlers.go:126-139` 的 me 再认证。每次经 `internal/auth/service.go:197-203`、`internal/auth/pg_store.go:335-367` 执行 SELECT session、SELECT status、UPDATE session、GetUserByID。正常 PG me 路径共 8 次认证 DB 操作,加一次权限查询。
其他鉴权请求也无条件 UPDATE 同一 auth_sessions 行;首屏并发可能放大行锁/连接等待。未采集 pg_stat_activity/锁等待或接口 p95,锁竞争是待验证风险。
### F6. 文档列表的隐含写修复与轮询叠加(源码已证实,耗时待测)
`internal/docgov/project_docs.go:59-74` 列表先调用 `HealProjectDocuments`;`internal/docduty/store.go:173-205` 开事务并取得项目 advisory lock(`internal/docduty/revoke.go:111-126`),重复读取创建者/成员,扫描文档,按需逐条 UPDATE,commit 后才正式 SELECT。
前端 `App.tsx:1507-1545` 每 5 秒为租约同步调用这个完整列表。请求时长超过间隔时可能重叠;相同项目多个标签放大成本。**不能直接删除 heal**:须先补齐创建/成员变更/转交/撤销写路径与历史数据修复闭环。本轮没有用该 GET 做只读探针。
### F7. 会话列表先聚合消息再分页(源码已证实,耗时待测)
`internal/ctxmgr/chat_history.go:110-125` 候选会话 JOIN 消息、COUNT、GROUP BY/ORDER BY 后 LIMIT,另取最新消息内容。仅返回少量会话不代表少量消息扫描。`internal/projectworkflow/store.go:171-240` 项目列表也无分页。
已有 `chat_messages(session_id, created_at)` 索引(`deploy/postgres/goose/00001_baseline.sql:119`);不笼统认定缺索引。先 EXPLAIN/数据量和可见性分析,再决定先分页后统计、索引或摘要。
### F8. 重启场景补查(用户补充后)
用户明确 3100、8080、8081 在系统重启后首次进入约十数秒。三种冷态须分开:浏览器 HTTP 缓存、Vite 转换/依赖缓存、Go/DB/代理启动就绪。重启若不改变构建 hash,并不会自动清掉浏览器持久 HTTP 缓存;不能直接用新 context 测量解释全部重启现象。
现存容器启动记录:dev StartedAt `2026-09-17T07:00:38.127811581Z`,打印“监听地址”于 `07:00:38.293728694Z`(约 **166 ms**);test StartedAt `06:20:05.243037366Z`,打印监听于 `06:20:05.384607542Z`(约 **142 ms**)。这是已有 DB 就绪条件下的一次启动、且日志先于真正 ListenAndServe,**不是 host reboot→ready 时间**;但未支持“Go 每次初始化必然十几秒”。
`cmd/server/main.go:41-50` 的初始化在监听前,`internal/bizdev/service.go:69-114` 包括 DB Ping/迁移/成员补齐/seed;DB连接段 timeout 5 秒,后续 seed 使用 Background,缺少分阶段耗时。`internal/app/app.go:355` 的 Runtime.Start 幂等,入口重复调用不会重复启动 worker,不能据此误判双倍启动。
Compose app healthcheck 每 10 秒、start_period 20 秒;start_period 是失败宽限窗口,**不代表强制等待20秒**。若代理/运维脚本等健康状态才放流,则探测周期可能影响可见就绪,需要对齐宿主启动、DB healthy、app listen、ready、首次请求时间。3100 的无缓存 Vite 预构建/按请求转换为独立候选,8080/8081 构建产物不存在 Vite 首次编译。
隔离 Vite 在 3199 使用独立空 cacheDir,同源码、未重启宿主/DB:监听准备 43 ms,首访登录可见 **1,580 ms**、207 资源/14,445,631 B,再次 171 ms。服务先启动、浏览器随后连接,部分预构建可能已在导航前完成;该值不是“启动到首屏”总时间。支持开发冷态有额外成本,但本机未复现自然网络十数秒;不得仅靠 warmup 宣称已解决 8080/8081。证据见 `../test-reports/assets/2026-09-17-initial-load-performance/vite-cold.json`。
本轮不重启正在使用的共享服务;U1/U7 纳入计划中的隔离重启矩阵和阶段计时。没有把服务冷启动排除在用户需求之外。
## 已排除与未证明
- 本机静态 HTML TTFB 约 1–3 ms;这组未登录测试并非后端生成首页耗时数秒。
- 没有请求产品 PI Runner,因此本次首屏慢不能归因于大模型冷启动。
- 服务监听前迁移/种子初始化(`cmd/server/main.go:41-50,84-85`;`internal/bizdev/service.go:69-114`)只解释重启就绪,不等于每个新浏览器首访。
- 没有用户重启时的 FRP/公网瀑布、登录后基线、慢 CPU profile、数据库 EXPLAIN;不对这些项宣称已复现。
- 基线现有测试以功能为主;本轮未发现首屏资源预算/冷缓存性能门禁。未运行业务测试,因为没有业务代码变更。
## 关联整改与证据
- [改进方案](../remediation/2026-09-17-initial-load-performance-plan.md)
- [诊断测量报告](../test-reports/2026-09-17-initial-load-performance-baseline.md)
- [完整浏览器数据](../test-reports/assets/2026-09-17-initial-load-performance/browser.json)
- [受控实验](../test-reports/assets/2026-09-17-initial-load-performance/controlled.json)
- [静态响应摘要](../test-reports/assets/2026-09-17-initial-load-performance/assets.json)
+2
View File
@@ -472,3 +472,5 @@ Day 4-5: Phase C 完善与发布准备(20h)
---
*本索引最后更新于 2026-09-12 15:05 UTC*
- 2026-09-17:[首次加载性能诊断与改进](2026-09-17-initial-load-performance-audit.md)(三入口重启首访;方案未实施)。
+1
View File
@@ -13,6 +13,7 @@
| owner | task | branch | worktree | mutex | status |
|-------|------|--------|----------|-------|--------|
| Codex | 2026-09-17-initial-load-performance-plan | feature/initial-load-performance-plan | .worktrees/feature-initial-load-performance-plan | loading-performance-audit | done |
| Kimi | 2026-09-17-system-admin-console | feature/system-admin-console | - | web + sysadmin | done |
| Cursor-Auto | 2026-09-17-engineering-dashboard | feature/engineering-dashboard | .worktrees/feature-engineering-dashboard | engineering-dashboard | done |
| Cursor | 2026-09-17-promote-null-fk-to-test | feature/promote-null-fk-to-test | - | infra | done |
@@ -0,0 +1,212 @@
---
title: "首次加载与工作台初始化性能改进方案"
status: planned
created: 2026-09-17
updated: 2026-09-17
artifact_contract: ce-unified-plan/v1
product_contract_source: ce-plan-bootstrap
execution: code
---
# 首次加载与工作台初始化性能改进方案
## Goal Capsule
让用户首次访问就能快速看到并操作登录页,登录后优先恢复当前项目与会话,避免下载无关工具或等待所有项目初始化。
采用静态交付优化、按功能加载和请求去重,之后按测量结果优化认证与数据库读取(KTD1–KTD5)。本轮仅交付诊断和方案,业务代码尚未修改;用户授权实施后执行以下单元。
遵守 AGENTS.md/PROJECT.md、现有权限和文档治理契约。若优化要求改变授权、新增跨用户缓存、跳过职责正确性或删除历史数据,停止该单元并回报。每单元独立 feature/fix 分支与模块 mutex;合 dev/容器重建占集成槽。默认不推送,不晋升 test/main。
## Product Contract
### Problem Frame
[诊断](../audit/2026-09-17-initial-load-performance-audit.md) 已确认:8080 首屏 2.86 MB,CDP 10 Mbps 冷缓存登录可见 2.64 秒、2 Mbps 11.78 秒;再次访问约 60 ms。字体 CSS 外链阻塞首屏;3100 Vite 首访 207 请求/12.24 MB。登录后存在全项目成员扇出与会话重复同步,服务端认证、文档列表和会话统计有重复工作。
这些数字是单样本实验,不是用户实际公网体验或 p95。用户已确认 3100/8080/8081 在系统重启后首访约十数秒。现有容器启动到打印监听仅约 166/142 ms,不代表整机重启就绪;必须补齐重启矩阵再签优化收益。
### Requirements
- R1. 首次登录页只加载认证与基础样式所需资源,外部字体不可阻塞登录表单。
- R2. 登录后先显示可用工作台骨架,优先恢复当前项目/会话;其他项目成员、管理台、导出、文件预览在需要时加载。
- R3. 相同身份和查询键的初始化请求去重;慢响应、快速切换项目、登出/换人不会写回旧状态。失败必须有局部错误与重试入口。
- R4. 所有服务端授权和文档治理保持正确,权限撤销/停用不得因缓存继续放行。计数、摘要与正文沿用相同可见性语义。
- R5. 发布后旧页面仍可打开延迟模块;缓存不得导致 HTML 长期陈旧,也不得泄露用户 API 数据;SSE 流式延迟不能回退。
- R6. 优化收益用可复现冷/热缓存、慢网、字体不可达、多项目和大历史场景证明,结果与镜像/资源 hash 绑定。
### Success Criteria
以下是**拟定验收预算**,不是当前已达标数据。表中静态资源体积及绝对首屏时间预算适用于 8080/8081 构建入口;3100 保留 Vite 开发模式,不套构建产物压缩体积预算,其保留/空依赖缓存与预热状态分别由 U1 固定加载时间预算,U7 检查重启改善;U1 固定机器、Chromium 版本、样本与网络条件,预算不得在看到失败后悄悄放宽。
| 指标 | 目标/口径 |
|---|---|
| 未登录关键 JS 总传输量 | 开启压缩后 ≤250 KiB;按首屏实际请求总和而非只看入口文件 |
| 普通工作台首次可用前 JS 总量 | ≤600 KiB;不含用户明确打开的文档预览/导出模块 |
| 冷缓存登录可操作 | 10 Mbps / 80 ms、CPU 4 倍减速下 p75 ≤1.5 s;2 Mbps 同配置 p75 ≤3 s |
| 登录态热刷新 | 10 Mbps / 80 ms、CPU 4 倍减速下工作台可操作 p75 ≤1.5 s;前提必要 API 达下行预算 |
| 登录态冷刷新/登录提交成功后恢复 | 冷刷新自 navigationStart 起、登录自成功响应起,当前会话历史及输入区就绪 p75 ≤3 s;仅骨架不算完成 |
| 必要首屏 API | 同宿主 app↔DB 网络下 p95 ≤300 ms;列表大数据场景 p95 ≤500 ms,接口错误率不增加 |
| 首屏初始化请求 | 项目成员初始化从 N 次降为当前项目最多 1 次;静止初始化每个会话列表查询键最多 1 个在途请求且成功后不被成员更新重触发 |
| 字体故障 | Google 域名完全阻断或延迟 10 秒时,仍满足登录预算,不发首屏外部字体 CSS 请求 |
| 无回归 | 同数据/设备各阶段 p95 不劣化超过 10%;无新增跨用户暴露、错误循环、重复写任务 |
浏览器每场景 ≥20 次,报告 p50/p75/p95 与原始样本;接口每场景 ≥100 请求、并发 1/10/30。浏览器 p95 仅小样本诊断,不能冒充线上 SLO;稳定发布后再累计真实用户分位数。
### Scope Boundaries
包括浏览器入口、静态资源交付、当前工作台数据初始化、与首屏相关的查询。没有业务语义变更,不调整 PI 模型、Prompt、文档租约时长、授权范围或工作流门禁。
Deferred to Follow-Up Work:Redis/CDN、新 SSR 框架、全面 App.tsx 重写、全站统一数据框架、数据库分库、文档全文缓存。只有 U1 的证据表明必要时另开任务;目前不需要先增加基础设施。
### Outstanding Questions
- 待补测:已知端口 3100/8080/8081,重启涵盖宿主机/应用/Vite 的具体范围、慢发生阶段、真实网络与设备;不会阻止可独立证明的 U2/U3,但阻止宣称用户现场问题已经彻底解决。
- 待补测:登录态瀑布、真实数据规模、SQL/连接池等待;U6 的查询改写/索引必须由这些证据选择。
## Planning Contract
### Key Technical Decisions
- KTD1. 保留 React/Vite + Go 架构。正式体验使用构建产物入口;3100 保留开发热更新用途,不把开发模块请求数量当生产目标。先确认真实反代 upstream,不盲改 FRP。
- KTD2. 首屏用系统字体栈;需要保留 Inter/JetBrains Mono 时仅选必需 WOFF2 并同源部署、按需加载。保留 KaTeX 渲染所需字体,但不要求登录页加载数学资源。
- KTD3. 按功能边界动态 import,登录壳与工作台隔离;管理台、DOCX/XLSX 导出、Word/Excel 预览、Mermaid 按使用加载。仅 manualChunks 拆文件不能减少关键路径下载,必须验证依赖图没有静态回引。
- KTD4. 现有 Go serveSPA 作为默认静态交付修复点,构建期生成 gzip/br(若工具链已支持)而非每请求高压缩。若真实统一代理已负责压缩,则由它单层处理并记录;不得重复编码。哈希资源 `public,max-age=31536000,immutable`;HTML `no-cache`;压缩变体 `Vary: Accept-Encoding`;用户 API 不共享缓存。保留 identity、HEAD、条件请求、Range 正确性。
- KTD5. 将请求状态按身份/项目/会话/过滤器键管理,先用局部 hook/loader 和在途合并解决,不强制引入新数据框架。身份切换清空缓存与取消在途请求,成员更新不作为会话获取的依赖。未知成员/权限不靠全量成员预取推导:使用服务端明确的当前用户权限摘要,未就绪保持 fail-closed。
- KTD6. 请求内复用已认证 principal,授权不跨请求长期缓存。减少 session 续期写入须明确 idle TTL/绝对过期/撤销语义后再落地,保留停用与撤销实时检查。
- KTD7. 文档列表先保留治理正确性;先补齐所有写路径与历史修复,再移出 GET 同步 heal。轮询保持原有租约新鲜度,优先复用 SSE/可见性控制与单请求在途,不能直接延长租约或无限延迟 busy 显示。
### High-Level Technical Design
```mermaid
flowchart LR
A[HTML与同源基础样式] --> B[轻量认证壳]
B --> C[认证结果与工作台模块并行准备]
C --> D[当前用户可见项目与会话]
D --> E[当前项目权限与聊天窗口]
E --> F[可操作工作台]
F --> G[按需管理台/导出/预览]
```
有保存会话时,可在校验期间预加载不含用户数据的工作台代码;受保护 API 与用户内容显示仍由认证门禁控制。无会话时不加载工作台代码。项目/会话列表在认证后并行,解析有效当前项目后再发其范围内请求;不是让全站等待所有项目成员。
```mermaid
sequenceDiagram
participant UI as 浏览器
participant API as Go API
participant DB as PostgreSQL
UI->>API: 首屏必要请求(按查询键去重)
API->>DB: 验证当前会话/用户状态
API->>API: 请求内复用 principal / 权限解析
API->>DB: 当前页/当前项目数据
API-->>UI: 权限范围内的结果
UI->>UI: 核对身份与查询代次后更新
```
## Implementation Units
### U1. 固定完整性能基线(P0,约 0.5–1 天)
**Requirements:** R6;后续全部单元的依赖。
**Files:** 新增 `web/e2e/initial-load-performance.spec.ts`、诊断指标辅助模块;更新 `.doc/test-reports/`。本轮探针与结果可作为起点。
**Approach:** 固定真实入口、部署 hash、登录角色、项目数/历史量,捕获资源瀑布、FCP/LCP、长任务、输入区与历史就绪、必要 API TTFB。用专用验收账号/夹具测登录,不在真实会话发送诊断消息。先检查 GET 副作用,文档列表暂有 heal,数据库分析使用只读事务和 statement_timeout。分别测空缓存、热缓存、10/2 Mbps、CPU 4x、字体失联、1/30/100 项目与大历史。本轮不重启共享服务;实施时 U7 在隔离环境重现应用/依赖/宿主启动过程,记录 ready 与第一次导航的时间关系。
**Test scenarios:** 新浏览器、保存有效/过期会话、无项目、普通成员与管理员、慢 API/失败、项目快速切换;每份结果保留参数与原始样本但无 token/用户正文。
**Verification:** 形成基线报告,能回答耗时在哪一阶段,并确认性能指标可重复。登录态测量缺失不得用未登录测试代替。
### U2. 修复字体与静态资源交付(P0,约 0.5–1 天)
**Dependencies:** U1 的未登录基线;**Requirements:** R1、R5、R6;**Files:** `web/index.html`、`web/src/styles/index.css`、`web/vite.config.ts`、`internal/server/server.go`、新增 `internal/server/static_assets_test.go`;必要时实际 Docker 构建文件。
**Approach:** 按 KTD2/KTD4 去除外部阻塞 CSS、提供压缩变体与明确缓存头。缺失 `/assets/*` 返回 404,不回退 HTML;客户端路由仍允许 SPA fallback。发布资源保留至少当前与上一版本并明确旧标签页支持窗口;窗口外加载失败提示用户保留编辑后刷新,不自动刷新丢失草稿。
**Test scenarios:** gzip/br/identity 协商、q=0 禁用、Vary、GET/HEAD、304、Range、未知资源 404、客户端路由回退;字体域名黑洞仍出现表单;SSE 首事件/持续事件不缓冲;受保护 API 无 public 缓存;部署后旧标签页首次打开尚未加载的模块。
**Verification:** curl 与浏览器确认真正压缩响应,JS 传输量下降;HTML 不被长期缓存。此单元不宣称已达到拆包预算。
### U3. 登录壳与重功能按需加载(P0,约 1–2 天)
**Dependencies:** U1;可与 U2 分开开发,合入仍串行。**Requirements:** R1、R2、R5;**Files:** `web/src/main.tsx`、`web/src/App.tsx`、新增认证壳组件、`web/src/components/doc/preview/FilePreviewPanel.tsx`、`web/src/features/replyExport/download.ts`、`web/src/engine/docxRenderer.ts`、`web/src/components/doc/blocks/MermaidBlockView.tsx`、U1 浏览器用例。
**Approach:** 按 KTD3 建立真实 lazy 边界;将工作台 hooks/缓存初始化移入工作台挂载周期。导出按钮点击才 import 引擎;预览根据文件类型加载;Mermaid 只在存在图块时加载。每边界提供局部加载/失败重试与错误边界。限定此重构服务于加载,保留既有文档编辑状态机。
**Test scenarios:** 未登录资源中不出现导出/预览/管理台/图表重模块;登录成功恢复当前会话;失效会话不展示缓存敏感数据;首次导出 DOCX/XLSX 内容有效;Word/Excel 预览、数学公式与 Mermaid 正确;动态 chunk 失败可以重试且不重复提交业务动作;首次打开功能耗时同时记录。
**Verification:** 满足 R1 首屏预算及功能按需请求证据,不把延迟全部转移成首次按钮不可用。检查 gzip 后所有首屏 chunks 总和。
### U4. 收敛初始化扇出与重复同步(P1,约 1–2 天)
**Dependencies:** U1、U3;**Requirements:** R2、R3、R4;**Files:** `web/src/App.tsx`、`web/src/api/projectWorkflow.ts`、`web/src/api/chatSessions.ts`、`web/src/data/storage.ts`、新增初始化 loader/hook 及其测试;必要时项目列表 DTO/服务端 handler 补当前用户成员摘要。
**Approach:** 当前项目成员优先,其他完整成员按打开面板加载;会话 fetch 依赖稳定身份/查询键,项目名称映射与网络获取分离;查询代次 + AbortController + 在途合并;每次成员响应不再全量写 projects/持久化。只有实测长任务证明需要时增加去重/批量保存,不把权威服务器文档写入延迟缓存作为成功。
**Test scenarios:** 1/30/100 项目首次请求数量稳定;成员结果乱序不会重复请求会话;快速切项目/登出/换用户不串状态;普通成员看到的项目/会话范围与改动前一致;错误后手动重试;成员增删后局部失效刷新;离线缓存不绕过权限验证。
**Verification:** 达到初始化去重预算,记录必要 API 次数与工作台就绪时间;跨用户负例、当前会话恢复、文档编辑刷新持久性通过。
### U5. 认证请求去重与续期写入收敛(P1,约 0.5–1 天)
**Dependencies:** U1 的认证测量;**Requirements:** R3、R4、R6;**Files:** `internal/server/auth_handlers.go`、`internal/server/auth_middleware.go`、`internal/auth/service.go`、`internal/auth/pg_store.go` 及相邻测试。
**Approach:** `/auth/me` 复用 middleware principal;合并会话及用户读取,避免重复查同一用户。先度量 session UPDATE 锁/连接等待;只有证明其成本且明确既有过期语义后采用有界续期/last_seen 写入策略。鉴权失败不缓存成成功,撤销/停用每请求生效。
**Test scenarios:** 同 token 1/10/30 并发;me 请求中只验证一次;撤销/停用后的下一请求拒绝;idle 边界/绝对过期/并发续期;DB 异常 fail-closed;匿名静态资源仍可访问。
**Verification:** 展示改前/后 DB 操作数、锁等待和 p95;身份安全回归通过。若节流会改变过期语义,则只交付请求内复用,将节流留待独立决定。
### U6. 按测量优化列表与租约读取(P2,约 2–4 天;按证据选子项)
**Dependencies:** U1、U4;**Requirements:** R2、R4、R6;**Files:** `internal/docgov/project_docs.go`、`internal/docduty/store.go`、成员/职责写路径、`internal/ctxmgr/chat_history.go`、`internal/projectworkflow/store.go`、`web/src/App.tsx` 及相邻 PG/浏览器测试;确需索引时新增 Goose migration。
**Approach:** (a) 先验证成员创建/转交/退出/撤销各路径事务内同步职责,并以显式受控任务修复历史,之后去掉列表读时 heal;(b) 租约查询避免每次全量文档修复,单请求在途、隐藏页暂停并在可见时立即同步,复用现有 SSE 失效事件;(c) 会话先按原可见性/排序选页,再仅统计页内数据,项目列表视数据量分页。维持 COUNT/latest 内容授权规则与排序稳定性,不能把当前全量总计改为页内总计却不告知调用方。
**Test scenarios:** 文档 GET 零 UPDATE/advisory lock,重复读不变 updated_at;成员变动职责立即正确;两标签 busy/释放不晚于原可见性基线、断 SSE 可恢复;慢查询轮询不重叠;分页无重复/丢项且计数不泄漏;EXPLAIN 对照现有索引与扫描行数;项目 143 不做写入修复测试。
**Verification:** PG 真库夹具 + 真实页面操作证明治理与编辑/刷新链路无回归;SQL 扫描、锁等待和 p95 确实改善。没有对应瓶颈时不为“优化”增加索引/摘要/新接口。
### U7. 重启后首访链路与开发预热(P0 诊断 / 按证据修复,约 0.5–1.5 天)
**Dependencies:** U1;**Requirements:** R1、R2、R6;**Files:** `web/vite.config.ts`、`cmd/server/main.go`、`internal/app/app.go`、`internal/bizdev/service.go`、`scripts/env-service.sh`、实际入口健康检查配置与启动测试。
**Approach:** 重启矩阵覆盖:仅 Vite 重启(保留/空依赖缓存)、仅 app 重启、依赖服务一起启动。每项再分浏览器保留缓存/清缓存与保存登录态/未登录;记录启动命令、DB healthy、app listen、ready、首次 HTML、首 API、UI ready。共享重启须占槽并避开正在执行任务,在隔离环境优先验证。Go 初始化分段计时;Vite 确认无缓存编译主导后,使用锁定 Vite 版本支持的 server.warmup/clientFiles 预热关键入口并保留 node_modules/.vite 缓存,禁止为暖机触发聊天或文档生成。
若 DB/迁移/seed 才是主耗时,先将重复扫描与种子写入改为有版本依据的幂等增量;必需迁移和授权数据未完成时 ready 必须失败。不要为看起来快而提前假报就绪。若是健康探测/代理等待,优化实际就绪探测与放流时序,不能把 start_period 直接当固定延时删除。对于后端暂时不可用,登录壳给出服务准备中和有限重试,不能无提示白屏或把瞬时不可达误当身份失效清空会话。
**Test scenarios:** 同版本无迁移重启、首次/有迁移启动、DB延迟就绪/不可用、依赖缓存失效、warmup 失败、过期登录态、保存有效登录态但网络暂时中断;多环境隔离保持。
**Verification:** 无待执行迁移且 DB 已就绪时 app restart→ready p95 ≤3 s;完整依赖启动单列,不承诺未测硬件的整机开机时间。8080/8081 ready 后首次访问须符合 Success Criteria 绝对预算;3100 分别符合 U1 在实施前固定的 Vite 冷/暖预算,不套生产压缩体积预算。三个入口在各自匹配的真实重启场景首访耗时均较基线下降至少 50%,同时记录从启动命令到 UI ready 总耗时,防止仅把等待搬到后台预热而宣称消失。若实测无法达到,保留未达标项与原因,不伪报完成。
## Verification Contract
- 前端:`cd web && npm test && npm run build`;单独运行 U1 的 Playwright 性能用例与现有文档修订/历史恢复相关浏览器回归。检查构建依赖与实际请求,不能只检查构建日志大小。
- Go:`go vet ./...`、`go test -short ./...`;认证/文档/会话修改补相关真实 PG 集成,覆盖撤销、跨用户、分页和职责同步。
- 合入/运行验证:占 CURRENT 集成槽,在主仓 merge 后 `./scripts/env-service.sh dev rebuild`,验证 `/health`、`/ready` 与资源 hash;健康不代表性能通过。禁止 worktree 启停共享服务。
- 真实 UI:登录、刷新当前会话、切项目、首开预览、首导出、文档编辑保存后刷新/重开、双标签 busy。涉及文档治理改变时按 PROJECT 补真实 PI + MiniMax 全链路,stub 不可替代交付。
- 证据:报告记录 commit/image/资源 hash、浏览器与硬件、网络/CPU 设置、缓存状态、数据规模、样本数/分位数、失败样本、请求数和 SQL 变化。脱敏,不保存登录 token、会话正文或原始授权头。
## Definition of Done
1. R1–R6 与性能预算有前后对比和真实 UI 证据;未覆盖项明确列残余,不因 health PASS 关闭。
2. 字体不可达仍可操作,首次登录无重功能模块;初始化不随全部项目成员返回重复发会话请求。
3. 授权/文档职责/租约/保存持久性不回退,分页可见性一致,SSE 不缓冲,旧版本 chunk 失效有恢复路径。
4. 更新任务、CURRENT、FEATURE-MATRIX、test-report,完成后任务归档;明确本地分支/提交、是否合 dev、是否推送。
## Rollout and Rollback
顺序:U1 → U2/U3 与 U7 → U4/U5 → 按数据选择 U6,每波独立复测并归档,预计核心 U1–U5 约 3.5–7 工程日,U7 另需 0.5–1.5 日,U6 另需 2–4 日(估算,含回归,非交付承诺)。先 dev,再按项目既有授权晋升 test/main。
压缩/缓存只作用静态资产,发现编码/缓存问题可回退独立提交;保留对应完整 HTML+资源集,避免仅回滚 HTML。拆包可回滚组件边界,但禁止强制刷新正在编辑的用户。认证和查询优化各独立提交;职责修复迁移必须可重入,停用新读路径时保留既有安全修复,不删除数据/索引作为临时回滚。若 p95 回退超过 10%、401/403 异常、文档职责偏差或租约不可见,停止晋升并回退相应单元。
## Appendix
- [审计与数值证据](../audit/2026-09-17-initial-load-performance-audit.md)
- [诊断测量报告](../test-reports/2026-09-17-initial-load-performance-baseline.md)
- HTTP 压缩协商与 `Vary`:[MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Compression)。缓存版本化资源与 HTML 策略:[MDN HTTP caching](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Caching)。
- 外部字体与阻塞 CSS:[web.dev 字体实践](https://web.dev/articles/font-best-practices)。Vite 开发请求瀑布:[Vite performance](https://vite.dev/guide/performance)。这些资料用于原则核验;具体配置以仓库锁定的 Vite 7.1.x 为准,不照搬官网当前版本构建 API。
## Plan Review
已完成同模型的 coherence、feasibility、design、security、scope、adversarial 六视角只读复核(3 个代理分别承担两视角,不作为6份模型独立验证)。修正1项:明确3100开发入口与8080/8081构建入口使用不同资源预算;保留三入口真实重启与总耗时验证。未进行外部跨模型复核,不声称具有跨模型验证。置信度:资源/字体机制高,用户重启现场主因中等,须U1/U7闭环。
+2
View File
@@ -31,3 +31,5 @@
| [2026-09-12-three-workflows-strict-governance-remediation.md](./2026-09-12-three-workflows-strict-governance-remediation.md) | 三工作流严格治理 |
审计入口:[../audit/INDEX.md](../audit/INDEX.md)
- 2026-09-17:[首次加载性能诊断与改进](2026-09-17-initial-load-performance-plan.md)(三入口重启首访;方案未实施)。
+4
View File
@@ -90,3 +90,7 @@
2. 同一 `mutex` 同时仅一个 Agent `in_progress`(见 CURRENT)
3. 新能力行:复制 [../ops/templates/feature-matrix-row.md](../ops/templates/feature-matrix-row.md)
4. 拆行/收口须保留 before/after 与义务迁移(见上表);禁止缩口径假完成
## 2026-09-17 首次加载性能专项
首次加载与重启首访性能优化:**planned**(不改变现有功能done/发布门禁结论)。[审计](../audit/2026-09-17-initial-load-performance-audit.md) · [方案](../remediation/2026-09-17-initial-load-performance-plan.md) · [诊断测量](../test-reports/2026-09-17-initial-load-performance-baseline.md)。资源/字体瓶颈已测;用户三入口重启现场及登录态基线待U1/U7,未实现或验收优化。mutex `loading-performance-audit`。
@@ -0,0 +1,49 @@
---
title: "首次加载性能诊断测量(非优化验收)"
status: done
created: 2026-09-17
updated: 2026-09-17
---
# 诊断测量报告
## 范围与结论
只读未登录浏览器基线、静态资源响应与受控网络实验完成;没有业务代码修改,不是优化 PASS 或完整重启验收。用户补充现象为 3100/8080/8081 系统重启后第一次进入约十数秒。
[审计](../audit/2026-09-17-initial-load-performance-audit.md) 包含数值、因果链、源码定位和限制;[方案](../remediation/2026-09-17-initial-load-performance-plan.md) 含重启矩阵、性能预算与治理回归。
## 环境与步骤
- 2026-09-17;源码读取主仓 `feature/rdev-core@52f0fbc`,文档 worktree 基线 `dev@d0ef696`。
- 实际 app-dev image `sha256:355ec1af39c5233ac7e710c6c4300779681edd96d5da85258a9dde71a1698af2`;app-test `sha256:ee7df995c59eb0faf8e26160113d36fcdb57844b4e96989709f8b6edc2f00395`。未证明这些镜像与源码 commit 相同。
- 主 JS hash 分别为 8080 `index-BLnmtk_3.js`、8081 `index-Dk3TnkZv.js`、8000 `index-BT80OB_W.js`。
- 浏览器采用现有 `mcr.microsoft.com/playwright:v1.63.0-noble`、headless Chromium;不登录、不调用写接口、无截图/正文保存。基线未加 CPU 限速。
- 宿主 Playwright 因缺 libatk 启动失败;隔离浏览器容器成功,不安装/修改宿主库。
- 新 context 冷缓存 → 同 context 第二次导航;页面 input 可见后等 1.5 秒采集 Resource Timing。每场景 n=1,未报告分位数。
- 受控场景包括 CDP 10/2 Mbps + 80ms、字体立即失败、字体延迟 5 秒后失败。CDP document RTT 未精确达到设置值,因此是带宽敏感性实验,不是公网仿真认证。
- 3199 隔离 Vite 使用主仓源码、独立 `/tmp` 空 cacheDir,不清理原有 3100 依赖缓存,不更改 Vite 配置;结束后停止临时实例。
- 日志仅提取启动时间和固定事件标记,不保存原始日志。现有 app start→打印监听 dev 166 ms/test 142 ms,不等同真正 ready 或整机重启耗时。
## 结果
| 检查 | 结果 | 限制 |
|---|---|---|
| 三容器主 JS 未压缩 | 确认,约 2.80–2.82 MB | 用户上游代理压缩未知 |
| 8080 冷/热差异 | 662/62 ms | n=1,本机未登录 |
| 带宽敏感性 | 10 Mbps 2.64 s;2 Mbps 11.78 s | 模拟,不是用户网络 |
| 外部 CSS 阻塞 | 人为延迟5s → 登录5.16s | 不能称自然延迟5s |
| 3100资源成本 | 207 请求/12.24 MB | 当时 Vite 服务已运行 |
| 隔离 Vite 空缓存 | 首访1.58s/再次171ms,首访14.45MB | 启动早于浏览器连接,非完整冷启动总耗时 |
| 登录态/整机重启 | 未覆盖 | U1/U7必须补齐 |
| 功能/真实PI/优化收益验收 | 未执行 | 本轮无实现 |
## 原始数据与复现
证据目录:[assets](assets/2026-09-17-initial-load-performance/)。`browser.json`、`controlled.json`、`vite-cold.json` 为浏览器结果;`assets.json` 为静态头/体积摘要。对应 `*.cjs` 是诊断探针,不是业务测试。
复现普通浏览器基线可使用已有 Playwright 镜像,挂载本仓到 `/repo:ro`、证据临时输出目录到 `/out`,执行 `/out/probe.cjs`。脚本假设 `/repo/web/node_modules/playwright` 存在;运行前复制脚本到临时输出目录,不覆盖既有证据。字体拦截会禁用该 context 缓存,不得拿其 warm 结果与正常 warm 作性能比较。
## 交付状态
方案及证据仅在独立本地 worktree,未合 dev、未 push;无共享服务重启/镜像重建。后续实施按 CURRENT 认领与集成槽规约执行。
@@ -0,0 +1,74 @@
[
{
"port": 8080,
"asset": "/assets/index-BLnmtk_3.js",
"bytes": 2820055,
"gzip_estimate_bytes": 796146,
"http_ms": 2.55,
"headers": {
"Content-Length": "2820055",
"Content-Type": "text/javascript; charset=utf-8",
"Last-Modified": "Thu, 17 Sep 2026 07:00:36 GMT"
}
},
{
"port": 8080,
"asset": "/assets/index-DhpBfxvE.css",
"bytes": 35504,
"gzip_estimate_bytes": 9682,
"http_ms": 0.83,
"headers": {
"Content-Length": "35504",
"Content-Type": "text/css; charset=utf-8",
"Last-Modified": "Thu, 17 Sep 2026 07:00:36 GMT"
}
},
{
"port": 8081,
"asset": "/assets/index-Dk3TnkZv.js",
"bytes": 2805141,
"gzip_estimate_bytes": 791475,
"http_ms": 2.11,
"headers": {
"Content-Length": "2805141",
"Content-Type": "text/javascript; charset=utf-8",
"Last-Modified": "Thu, 17 Sep 2026 04:10:13 GMT"
}
},
{
"port": 8081,
"asset": "/assets/index-DhpBfxvE.css",
"bytes": 35504,
"gzip_estimate_bytes": 9682,
"http_ms": 0.87,
"headers": {
"Content-Length": "35504",
"Content-Type": "text/css; charset=utf-8",
"Last-Modified": "Thu, 17 Sep 2026 04:10:13 GMT"
}
},
{
"port": 8000,
"asset": "/assets/index-BT80OB_W.js",
"bytes": 2798137,
"gzip_estimate_bytes": 789362,
"http_ms": 2.6,
"headers": {
"Content-Length": "2798137",
"Content-Type": "text/javascript; charset=utf-8",
"Last-Modified": "Wed, 16 Sep 2026 04:14:21 GMT"
}
},
{
"port": 8000,
"asset": "/assets/index-DhpBfxvE.css",
"bytes": 35504,
"gzip_estimate_bytes": 9682,
"http_ms": 0.89,
"headers": {
"Content-Length": "35504",
"Content-Type": "text/css; charset=utf-8",
"Last-Modified": "Wed, 16 Sep 2026 04:14:21 GMT"
}
}
]
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,18 @@
const {chromium}=require('/repo/web/node_modules/playwright');
const fs=require('fs');
(async()=>{
const browser=await chromium.launch({headless:true,args:['--no-sandbox']});const rows=[];
for(const test of [{port:8080,mode:'font-delay-5s'},{port:8080,mode:'2Mbps-80ms'}]){
const context=await browser.newContext();const page=await context.newPage();const cdp=await context.newCDPSession(page);await cdp.send('Network.enable');
if(test.mode==='2Mbps-80ms')await cdp.send('Network.emulateNetworkConditions',{offline:false,latency:80,downloadThroughput:250000,uploadThroughput:625000});
if(test.mode==='font-delay-5s')await page.route(/fonts\.googleapis\.com/,async r=>{await new Promise(ok=>setTimeout(ok,5000));await r.abort()});
if(test.mode==='local-font-blocked')await page.route(/fonts\.(googleapis|gstatic)\.com/,r=>r.abort());
const failures=[];page.on('requestfailed',r=>failures.push({url:r.url().split('?')[0],error:r.failure()?.errorText}));
for(const phase of ['cold']){
const started=Date.now();let error=null;try{await page.goto(`http://127.0.0.1:${test.port}`,{waitUntil:'domcontentloaded',timeout:45000});await page.locator('input').first().waitFor({state:'visible',timeout:10000});}catch(e){error=e.message.slice(0,180)}
const visibleMs=Date.now()-started;await page.waitForTimeout(1500);
const perf=await page.evaluate(()=>({navigation:performance.getEntriesByType('navigation').map(n=>({ttfb:n.responseStart,dcl:n.domContentLoadedEventEnd,load:n.loadEventEnd})),paint:performance.getEntriesByType('paint').map(p=>({name:p.name,ms:p.startTime})),resources:performance.getEntriesByType('resource').map(r=>({url:r.name.split('?')[0],ms:r.duration,bytes:r.transferSize,decoded:r.decodedBodySize,initiator:r.initiatorType}))}));
const row={...test,phase,visibleMs,error,...perf,failures:[...failures]};rows.push(row);fs.writeFileSync('/out/controlled.json',JSON.stringify(rows,null,2));console.log(JSON.stringify({...test,phase,visibleMs,error,navigation:perf.navigation,paint:perf.paint,requests:perf.resources.length,bytes:perf.resources.reduce((s,r)=>s+r.bytes,0),failures}));
}await context.close();
}await browser.close();
})();
@@ -0,0 +1,117 @@
[
{
"port": 8080,
"mode": "font-delay-5s",
"phase": "cold",
"visibleMs": 5161,
"error": null,
"navigation": [
{
"ttfb": 2.899999976158142,
"dcl": 5110.699999988079,
"load": 5113.800000011921
}
],
"paint": [
{
"name": "first-paint",
"ms": 5116
},
{
"name": "first-contentful-paint",
"ms": 5164
}
],
"resources": [
{
"url": "https://fonts.googleapis.com/css2",
"ms": 5007.399999976158,
"bytes": 0,
"decoded": 0,
"initiator": "link"
},
{
"url": "http://127.0.0.1:8080/assets/index-BLnmtk_3.js",
"ms": 7.900000035762787,
"bytes": 2820355,
"decoded": 2820055,
"initiator": "script"
},
{
"url": "http://127.0.0.1:8080/assets/index-DhpBfxvE.css",
"ms": 3.199999988079071,
"bytes": 35804,
"decoded": 35504,
"initiator": "link"
},
{
"url": "http://127.0.0.1:8080/api/v1/auth/public-config",
"ms": 3,
"bytes": 353,
"decoded": 53,
"initiator": "fetch"
}
],
"failures": [
{
"url": "https://fonts.googleapis.com/css2",
"error": "net::ERR_FAILED"
}
]
},
{
"port": 8080,
"mode": "2Mbps-80ms",
"phase": "cold",
"visibleMs": 11776,
"error": null,
"navigation": [
{
"ttfb": 1.5,
"dcl": 11732,
"load": 11733.300000011921
}
],
"paint": [
{
"name": "first-paint",
"ms": 996
},
{
"name": "first-contentful-paint",
"ms": 11776
}
],
"resources": [
{
"url": "https://fonts.googleapis.com/css2",
"ms": 903.1999999880791,
"bytes": 1249,
"decoded": 17342,
"initiator": "link"
},
{
"url": "http://127.0.0.1:8080/assets/index-DhpBfxvE.css",
"ms": 367.80000001192093,
"bytes": 35804,
"decoded": 35504,
"initiator": "link"
},
{
"url": "http://127.0.0.1:8080/assets/index-BLnmtk_3.js",
"ms": 11515.199999988079,
"bytes": 2820355,
"decoded": 2820055,
"initiator": "script"
},
{
"url": "http://127.0.0.1:8080/api/v1/auth/public-config",
"ms": 87.5,
"bytes": 353,
"decoded": 53,
"initiator": "fetch"
}
],
"failures": []
}
]
@@ -0,0 +1,17 @@
const {chromium}=require('/repo/web/node_modules/playwright');
const fs=require('fs');
(async()=>{
const browser=await chromium.launch({headless:true,args:['--no-sandbox']});const rows=[];
for(const test of [{port:8080,mode:'local-cold'},{port:8080,mode:'local-font-blocked'},{port:8080,mode:'10Mbps-80ms'},{port:3100,mode:'local-cold'}]){
const context=await browser.newContext();const page=await context.newPage();const cdp=await context.newCDPSession(page);await cdp.send('Network.enable');
if(test.mode==='10Mbps-80ms')await cdp.send('Network.emulateNetworkConditions',{offline:false,latency:80,downloadThroughput:1250000,uploadThroughput:625000});
if(test.mode==='local-font-blocked')await page.route(/fonts\.(googleapis|gstatic)\.com/,r=>r.abort());
const failures=[];page.on('requestfailed',r=>failures.push({url:r.url().split('?')[0],error:r.failure()?.errorText}));
for(const phase of ['cold','warm']){
const started=Date.now();let error=null;try{await page.goto(`http://127.0.0.1:${test.port}`,{waitUntil:'domcontentloaded',timeout:45000});await page.locator('input').first().waitFor({state:'visible',timeout:10000});}catch(e){error=e.message.slice(0,180)}
const visibleMs=Date.now()-started;await page.waitForTimeout(1500);
const perf=await page.evaluate(()=>({navigation:performance.getEntriesByType('navigation').map(n=>({ttfb:n.responseStart,dcl:n.domContentLoadedEventEnd,load:n.loadEventEnd})),paint:performance.getEntriesByType('paint').map(p=>({name:p.name,ms:p.startTime})),resources:performance.getEntriesByType('resource').map(r=>({url:r.name.split('?')[0],ms:r.duration,bytes:r.transferSize,decoded:r.decodedBodySize,initiator:r.initiatorType}))}));
const row={...test,phase,visibleMs,error,...perf,failures:[...failures]};rows.push(row);fs.writeFileSync('/out/browser.json',JSON.stringify(rows,null,2));console.log(JSON.stringify({...test,phase,visibleMs,error,navigation:perf.navigation,paint:perf.paint,requests:perf.resources.length,bytes:perf.resources.reduce((s,r)=>s+r.bytes,0),failures}));
}await context.close();
}await browser.close();
})();
@@ -0,0 +1,17 @@
const {chromium}=require('/repo/web/node_modules/playwright');
const fs=require('fs');
(async()=>{
const browser=await chromium.launch({headless:true,args:['--no-sandbox']});const rows=[];
for(const test of [{port:3199,mode:'isolated-vite-empty-cache'}]){
const context=await browser.newContext();const page=await context.newPage();const cdp=await context.newCDPSession(page);await cdp.send('Network.enable');
if(test.mode==='10Mbps-80ms')await cdp.send('Network.emulateNetworkConditions',{offline:false,latency:80,downloadThroughput:1250000,uploadThroughput:625000});
if(test.mode==='local-font-blocked')await page.route(/fonts\.(googleapis|gstatic)\.com/,r=>r.abort());
const failures=[];page.on('requestfailed',r=>failures.push({url:r.url().split('?')[0],error:r.failure()?.errorText}));
for(const phase of ['cold','warm']){
const started=Date.now();let error=null;try{await page.goto(`http://127.0.0.1:${test.port}`,{waitUntil:'domcontentloaded',timeout:45000});await page.locator('input').first().waitFor({state:'visible',timeout:10000});}catch(e){error=e.message.slice(0,180)}
const visibleMs=Date.now()-started;await page.waitForTimeout(1500);
const perf=await page.evaluate(()=>({navigation:performance.getEntriesByType('navigation').map(n=>({ttfb:n.responseStart,dcl:n.domContentLoadedEventEnd,load:n.loadEventEnd})),paint:performance.getEntriesByType('paint').map(p=>({name:p.name,ms:p.startTime})),resources:performance.getEntriesByType('resource').map(r=>({url:r.name.split('?')[0],ms:r.duration,bytes:r.transferSize,decoded:r.decodedBodySize,initiator:r.initiatorType}))}));
const row={...test,phase,visibleMs,error,...perf,failures:[...failures]};rows.push(row);fs.writeFileSync('/out/vite-cold.json',JSON.stringify(rows,null,2));console.log(JSON.stringify({...test,phase,visibleMs,error,navigation:perf.navigation,paint:perf.paint,requests:perf.resources.length,bytes:perf.resources.reduce((s,r)=>s+r.bytes,0),failures}));
}await context.close();
}await browser.close();
})();
File diff suppressed because it is too large Load Diff