首页
/ gstack /benchmark 性能回归检测:基于 browse 守护进程的页面性能基线化、对比与趋势追踪实战

gstack /benchmark 性能回归检测:基于 browse 守护进程的页面性能基线化、对比与趋势追踪实战

2026-09-06 19:04:36作者:仰钰奇

本指南深入解析 gstack 内置技能 /benchmark 的完整工作原理与操作流程。该技能以一名资深 Performance Engineer 的角色,借助 browse 守护进程的 perf 命令与页内 JavaScript 求值(eval),对真实运行页面进行加载性能度量,建立基线(baseline)、对比每个 PR 前后差异、持续追踪性能趋势,从而把"慢在哪里、何时开始变慢"变成可量化、可定位、可回溯的报告。读完本文,你将掌握如何在任意 gstack 工作区中执行完整性能审计、捕获基线、解读回归阈值、给出慢资源诊断与性能预算校验。

技能定义文件位于仓库 benchmark/SKILL.md,由 benchmark/SKILL.md.tmpl 自动生成(文件头标注 AUTO-GENERATED from SKILL.md.tmpl — do not edit directly,重新生成为 bun run gen:skill-docs)。技能元信息在 YAML frontmatter 中:name: benchmarkpreamble-tier: 1version: 1.0.0allowed-tools 为 Bash / Read / Write / Glob / AskUserQuestion。

技能定位:性能不会一次性崩坏,而是死于千刀万剐

技能对自身角色的定位值得先理解:性能不是在一次大回归中劣化的,而是在大量小退化中累积的——每个 PR 增加 50ms、20KB,某一天应用加载要 8 秒,却没人知道它是从什么时候开始变慢的。/benchmark 的职责就是四件事:度量(measure)、基线化(baseline)、对比(compare)、告警(alert),全部基于 browse 守护进程从真实运行页面采集的数据,而非估算。

技能触发条件覆盖关键词 "performance"、"benchmark"、"page speed"、"lighthouse"、"web vitals"、"bundle size"、"load time",并提供语音别名(speech-to-text):"speed test"、"check performance"。

命令行参数一览

/benchmark 面向用户直接调用,入口形式与含义如下:

命令形式 作用
/benchmark <url> 完整性能审计并带基线对比
/benchmark <url> --baseline 采集基线(在改动代码之前运行)
/benchmark <url> --quick 单次时序检查(无需基线)
/benchmark <url> --pages /,/dashboard,/api/health 指定要测的页面路径集合
/benchmark --diff 仅对当前分支涉及的页面做基准
/benchmark --trend 基于历史数据展示性能趋势

其中 --pages 支持逗号分隔的多个路由;--diff--trend 属于模式开关,改变了数据范围与输出形态。

Phase 1:环境准备与目录初始化

技能在正式采集前先做两件基础设施工作:解析当前仓库 slug 用于归属报告,并创建报告目录树。

eval "$(~/.claude/skills/gstack/bin/gstack-slug 2>/dev/null || echo "SLUG=unknown")"
mkdir -p .gstack/benchmark-reports
mkdir -p .gstack/benchmark-reports/baselines

由此确立了技能的全部产物位置:运行期报告写入 .gstack/benchmark-reports/,历史基线集中存放于 .gstack/benchmark-reports/baselines/。技能还要求在执行任何 browse 命令前先做 SETUP 探活——检查 browse 可执行文件是否已构建(仓库内 _ROOT/.claude/skills/gstack/browse/dist/browse 或用户级 ~/.claude/skills/gstack/browse/dist/browse),若返回 NEEDS_SETUP,则需向用户请求一次约 10 秒的一次性构建(./setup),bun 缺失时按指定版本与校验和安装。

Phase 2:页面发现(Page Discovery)

默认被测页面与 /canary 技能一致,即从应用导航结构中自动发现;也可以用 --pages /,/dashboard,/api/health 显式指定。

--diff 模式只测当前分支改动涉及的页面,通过 git 与 GitHub CLI 解析基准分支:

git diff $(gh pr view --json baseRefName -q .baseRefName 2>/dev/null || gh repo view --json defaultBranchRef -q .defaultBranchRef.name 2>/dev/null || echo main)...HEAD --name-only

该命令依次尝试获取当前 PR 的 baseRefName、默认仓库分支名,最终回退到 main,从而把变更文件列表作为页面筛选依据。

Phase 3:性能数据采集

对每个页面先导航、再取页面加载时序,然后通过页内 JavaScript 深挖细粒度指标:

$B goto <page-url>
$B perf
$B eval "JSON.stringify(performance.getEntriesByType('navigation')[0])"

perf 命令背后的源码实现

perf 是 browse 守护进程 CLI 的 Inspection 类命令,命令注册表位于 browse/src/commands.ts,其条目说明为 'perf': { category: 'Inspection', description: 'Page load timings' }(见文件顶部命令集合与命令描述列表)。其核心实现位于 browse/src/read-commands.ts,内部通过 performance.getEntriesByType('navigation')[0](即 PerformanceNavigationTiming)计算九个时序桶:

输出字段 计算方式(源码级) 含义
dns domainLookupEnd - domainLookupStart DNS 解析耗时
tcp connectEnd - connectStart TCP 建连耗时
ssl secureConnectionStart > 0 ? connectEnd - secureConnectionStart : 0 TLS/SSL 握手耗时(非 HTTPS 时为 0)
ttfb responseStart - requestStart 首字节时间
download responseEnd - responseStart 响应体下载耗时
domParse domInteractive - responseEnd DOM 解析耗时
domReady domContentLoadedEventEnd - startTime DOMContentLoaded 结束相对导航起点
load loadEventEnd - startTime load 事件结束相对导航起点
total loadEventEnd - startTime 全量加载耗时(与 load 同源)

所有值经 Math.round 取整后以 key.padEnd(12) + value + 'ms' 的定宽文本行输出;若页面尚无 navigation timing 数据则返回 No navigation timing data available.。这套底层实现正是技能 Phase 3 中 TTFB、DOM Interactive、DOM Complete、Full Load 等指标的直接数据来源。

关键指标定义

在导航时序的基础上,技能抽取以下核心指标:

  • TTFBresponseStart - requestStart
  • FCP(First Contentful Paint):来自 PerformanceObserver 或 paint entries
  • LCP(Largest Contentful Paint):来自 PerformanceObserver
  • DOM InteractivedomInteractive - navigationStart
  • DOM CompletedomComplete - navigationStart
  • Full LoadloadEventEnd - navigationStart

资源级分析

资源级负载通过 performance.getEntriesByType('resource') 提取文件名、initiatorType、传输体积与耗时,并按耗时倒序取前 15 条:

$B eval "JSON.stringify(performance.getEntriesByType('resource').map(r => ({name: r.name.split('/').pop().split('?')[0], type: r.initiatorType, size: r.transferSize, duration: Math.round(r.duration)})).sort((a,b) => b.duration - a.duration).slice(0,15))"

JS / CSS bundle 体积是回归检测的确定性前导指标,单独过滤统计:

$B eval "JSON.stringify(performance.getEntriesByType('resource').filter(r => r.initiatorType === 'script').map(r => ({name: r.name.split('/').pop().split('?')[0], size: r.transferSize})))"
$B eval "JSON.stringify(performance.getEntriesByType('resource').filter(r => r.initiatorType === 'css').map(r => ({name: r.name.split('/').pop().split('?')[0], size: r.transferSize})))"

网络层汇总(请求总数、总传输字节、按 initiatorType 分桶排序)使用一段 IIFE 一次完成:

$B eval "(() => { const r = performance.getEntriesByType('resource'); return JSON.stringify({total_requests: r.length, total_transfer: r.reduce((s,e) => s + (e.transferSize||0), 0), by_type: Object.entries(r.reduce((a,e) => { a[e.initiatorType] = (a[e.initiatorType]||0) + 1; return a; }, {})).sort((a,b) => b[1]-a[1])})})()"

Phase 4:基线采集(--baseline 模式)

基线建议在任何改动之前采集,作为后续对比的锚点。基线文件为 JSON,按页面聚合各指标:

{
  "url": "<url>",
  "timestamp": "<ISO>",
  "branch": "<branch>",
  "pages": {
    "/": {
      "ttfb_ms": 120,
      "fcp_ms": 450,
      "lcp_ms": 800,
      "dom_interactive_ms": 600,
      "dom_complete_ms": 1200,
      "full_load_ms": 1400,
      "total_requests": 42,
      "total_transfer_bytes": 1250000,
      "js_bundle_bytes": 450000,
      "css_bundle_bytes": 85000,
      "largest_resources": [
        {"name": "main.js", "size": 320000, "duration": 180},
        {"name": "vendor.js", "size": 130000, "duration": 90}
      ]
    }
  }
}

写入路径固定为 .gstack/benchmark-reports/baselines/baseline.json。该 schema 完整覆盖了时序、传输量、JS/CSS 体积与最大资源四类信息,是后面对比与趋势分析的基础数据结构。

Phase 5:对比与回归判定

若存在基线,技能将当前数据与基线逐行对比并输出报告。其判定规则是一套相对阈值 + 绝对阈值混合的量化标准:

指标类型 条件 判定
时序指标 增幅 >50% 或 绝对值 +500ms REGRESSION
时序指标 增幅 >20% WARNING
Bundle 体积 增幅 >25% REGRESSION
Bundle 体积 增幅 >10% WARNING
请求数量 增幅 >30% WARNING

报告中每个指标行呈现 Baseline / Current / Delta / Status 四列,并汇总回归清单及可能的根因提示:

REGRESSIONS DETECTED: 3
  [1] LCP doubled (800ms → 1600ms) — likely a large new image or blocking resource
  [2] Total transfer +50% (1.2MB → 1.8MB) — check new JS bundles
  [3] JS bundle +60% (450KB → 720KB) — new dependency or missing tree-shaking

Phase 6:最慢资源定位

对比之外,技能输出 TOP 10 最慢资源清单(含类型、体积、耗时),并针对每条给出可执行建议,例如:vendor.chunk.js 过大建议 code-splitting;analytics.js 阻塞渲染 250ms 建议 async/defer;hero-image.webp 建议补 width/height 防 CLS 并考虑懒加载。第三方脚本会被明确标注为 ← third-party——它们只作上下文参考,因为用户无法修复外部服务慢的问题,建议聚焦于第一方资源。

Phase 7:性能预算校验

技能内置了一套行业常用预算作为对照:

指标 预算 状态判定
FCP < 1.8s PASS / FAIL
LCP < 2.5s PASS / FAIL
Total JS < 500KB PASS / FAIL
Total CSS < 100KB PASS / FAIL
Total Transfer < 2MB 接近上限时 WARNING(如 90%)
HTTP Requests < 50 PASS / FAIL

输出会按通过项数给出综合等级(如 Grade: B (4/6 passing)),将单点数据拉回可沟通的整体健康度。

Phase 8:趋势分析(--trend 模式)

技能读取历史基线文件,聚合最近数次基准的 FCP、LCP、Bundle、Requests 与等级,生成趋势表,并给出结论性研判,例如:

TREND: Performance degrading. LCP doubled in 8 days.
       JS bundle growing 50KB/week. Investigate.

这正是该技能区别于一次性 Lighthouse 扫描的核心价值:跨 PR、跨时间的趋势记忆让"性能哪天变慢、是谁带进来的"有据可查。Phase 1 中为每次基准建立独立基线文件并按日期归档的做法,为趋势分析提供了数据前提。

Phase 9:报告落盘

审计完成后技能把报告写成两个文件,方便纳入代码评审与团队历史:

  • .gstack/benchmark-reports/{date}-benchmark.md
  • .gstack/benchmark-reports/{date}-benchmark.json

贯穿全流程的工程原则

技能末尾以五条铁律收束,体现其设计哲学:

  1. 度量,不要猜测(Measure, don't guess):一律使用真实 performance.getEntries() 数据,禁止估算。
  2. 基线不可或缺:没有基线就只能报告绝对数值而无法检测回归,应始终鼓励先采集基线。
  3. 相对阈值而非绝对阈值:2000ms 对复杂仪表盘可能完全正常、对落地页却不可接受,因此要与"你自己的基线"对比,而不是拍一个通用绝对值。
  4. 第三方脚本只是上下文:标出它们,但把优化建议集中在第一方可控资源上。
  5. Bundle 体积是前导指标:加载时间随网络波动,而 bundle 体积是确定性的,应持续追踪。

此外技能是只读的:产出报告,除非用户明确要求,否则不修改任何代码。

工作流生命周期与收尾协议

与 gstack 其它技能一致,/benchmark 并非孤立的一次性命令,而是套在完整技能生命周期中:启动时通过 gstack-skill-start --skill "benchmark" --model "claude" --parent-pid "$PPID" 执行 preamble(读取 KEY: value 状态行,处理一次性 onboarding/consent 指令块);流程结束前必须执行"运营自我改进",将可复用的会话经验通过 gstack-learnings-log 落库(显式空结果也需声明);收尾时以单条命令上报遥测:

~/.claude/skills/gstack/bin/gstack-skill-end --skill "benchmark" --outcome OUTCOME \
  --session-id "SESSION_ID" --tel-start "TEL_START" --used-browse USED_BROWSE \
  --error-message "ERROR_MESSAGE" --failed-step "FAILED_STEP" 2>/dev/null || true

状态上报遵循 DONE / DONE_WITH_CONCERNS / BLOCKED / NEEDS_CONTEXT 协议;连续 3 次失败、涉及不确定的安全敏感变更或超出可验证范围时必须升级(格式:STATUS / REASON / ATTEMPTED / RECOMMENDATION)。

落地建议

/benchmark 接入日常工作流的最佳姿势:任何性能相关改动之前先跑一次 --baseline 固化基准;改动合入 PR 后在评审中执行 /benchmark <url>/benchmark --diff 让对比报告随分支走;每隔一段时间用 --trend 检视趋势表,及早发现 bundle 持续膨胀或 LCP 缓慢爬升这类"千刀万剐式"退化。数据与报告全部落在工作区 .gstack/benchmark-reports/ 下,可直接进仓库、进评审、进历史。

登录后查看全文
热门项目推荐
相关项目推荐