gstack /benchmark 性能回归检测:基于 browse 守护进程的页面性能基线化、对比与趋势追踪实战
本指南深入解析 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: benchmark、preamble-tier: 1、version: 1.0.0、allowed-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 等指标的直接数据来源。
关键指标定义
在导航时序的基础上,技能抽取以下核心指标:
- TTFB:
responseStart - requestStart - FCP(First Contentful Paint):来自 PerformanceObserver 或
paintentries - LCP(Largest Contentful Paint):来自 PerformanceObserver
- DOM Interactive:
domInteractive - navigationStart - DOM Complete:
domComplete - navigationStart - Full Load:
loadEventEnd - 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
贯穿全流程的工程原则
技能末尾以五条铁律收束,体现其设计哲学:
- 度量,不要猜测(Measure, don't guess):一律使用真实
performance.getEntries()数据,禁止估算。 - 基线不可或缺:没有基线就只能报告绝对数值而无法检测回归,应始终鼓励先采集基线。
- 相对阈值而非绝对阈值:2000ms 对复杂仪表盘可能完全正常、对落地页却不可接受,因此要与"你自己的基线"对比,而不是拍一个通用绝对值。
- 第三方脚本只是上下文:标出它们,但把优化建议集中在第一方可控资源上。
- 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/ 下,可直接进仓库、进评审、进历史。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0626
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00