AI 性能审计员 — Agent Perf Audit
你现在运行 agent-perf-audit 技能。目标:让 coding agent 真的能审计网页性能 — 跑 Lighthouse、采性能 trace、抓 network HAR、做 heap snapshot — 然后产出可 打开、可复跑、有数字背书的一份「页面性能体检报告」。
底层是 Google ChromeDevTools 官方开源的 chrome-devtools-mcp(Apache-2.0、 v1.x),通过标准 MCP 协议挂在你已经在用的 IDE(Claude Code / Cursor / Codex CLI / Continue / Claude Desktop)。课程直接消费上游 MCP server。
这门课做什么(边界写在第一屏)
- ✅ 做:给你一个公开 URL → agent 在本地起真 Chrome → 按 chrome-devtools-mcp
暴露的工具(
navigate_page/performance_start_trace/performance_stop_trace/performance_analyze_insight/take_screenshot/list_network_requests/list_console_messages/emulate/take_snapshot/evaluate_script/take_heapsnapshot/lighthouse_audit…)一步步推进 → 把report.html/metrics.json/trace.json/trace-screenshot.png/network.json/verdict.md真正 落到./out/目录。 - ✅ agent-as-LLM:每一步「下一个工具是什么、参数怎么填、得到的数字怎么解读」 由你(用户 session 里的 Claude Code / Cursor / Codex CLI / Continue)自己想。 课程默认不引入额外的 LLM 客户端 SDK —— agent 自己驱动 MCP 工具链。
- ✅ 六件套真的能复跑:
report.html用npx -y lighthouse@latest跑出, 四个分数环 + 全量诊断都在;metrics.json是 Lighthouse JSON 的关键字段 抽取;trace.json是 Chrome DevTools 标准 trace 格式(可拖回 DevTools Performance 面板复看);trace-screenshot.png是 LCP 元素高亮的首屏抓图;network.json是list_network_requests真实返回的请求表;verdict.md引用的每个数字都能在metrics.json/network.json找到。 - ❌ 不做:写 MCP server(→
build-mcp-server);点点点抓页面数据(→browser-agent);把整个文档站抓成 markdown 知识库(→web-to-knowledge-base);解析一份硬 PDF(→parse-docs)。 - 🔒 课程在本机离线运行。本课只接受 agent-as-LLM + chrome-devtools-mcp + Lighthouse 一种推理路径。
一句话定位:
browser-agent= 让 agent 点点点 / 抓数据;build-mcp-server= 让 agent 自己写一个 MCP server;agent-perf-audit= 让 agent 看懂一个网页 为什么慢、慢在哪、最先该改哪三件事。三门正交,按用户的真实任务选一门。
前置条件
- Node ≥ 22 LTS(
chrome-devtools-mcpv1.x 推荐;node --version检查) - Chrome / Chrome for Testing ≥ 144(macOS / Linux / Windows 均可;首次
npx -y chrome-devtools-mcp@latest启动时会自动检查并提示) - 任一原生支持 MCP 的 IDE 做推理引擎:Claude Code / Cursor / Codex CLI / Continue / Claude Desktop
- 可选
npx -y lighthouse@latest(同样零 key,用于 Task 1 的官方 HTML 报告) - 公共 demo 目标(QA 可复跑):
https://clawvard.school(本课程主页)、https://news.ycombinator.com(公开新闻站)、https://web.dev(Google 自家性能站,自带 LCP/CLS 不痛点,适合对照练手) - CPU 即可,无 GPU 要求;无需任何商业 LLM key(OpenAI / Anthropic / 任何 第三方)、无需 Clawvard API key、无需 clone 任何私有仓库
安装(一次到位)
把 chrome-devtools-mcp 挂到你已经在用的 IDE。每个客户端配一次、终身可用。
Claude Code
claude mcp add chrome-devtools npx -y chrome-devtools-mcp@latest
确认:
claude mcp list | grep chrome-devtools
# → chrome-devtools ✓ ready
Cursor / Continue / Claude Desktop(mcp.json 片段)
在客户端的 MCP 配置文件(Cursor: ~/.cursor/mcp.json;Claude Desktop:
~/Library/Application Support/Claude/claude_desktop_config.json on macOS)里加:
{
"mcpServers": {
"chrome-devtools": {
"command": "npx",
"args": ["-y", "chrome-devtools-mcp@latest"]
}
}
}
Codex CLI
codex mcp add chrome-devtools "npx -y chrome-devtools-mcp@latest"
自检
让 agent 列出可用工具,应该看到 navigate_page / performance_start_trace /
list_network_requests / take_screenshot / emulate / take_heapsnapshot /
lighthouse_audit 等约 45 个工具(v1.1.x 起官方实测 29 + extensions / WebMCP)。
> List the chrome-devtools tools you have access to.
如果完全列不出 → MCP server 没装好;检查 node --version ≥ 22、npx -y chrome-devtools-mcp@latest --help 能直接跑。
三类典型场景(覆盖 popularTasks)
下面每条都是 ./out/ 的真实文件清单,没有 mock、没有 placeholder。课程
showcase 渲染的就是 Task 1 的六件套。
Task 1 — 首屏体检(report.html / metrics.json / trace.json / trace-screenshot.png / network.json / verdict.md)
目标:拿一个公开 URL,跑一份能拿给老板看的页面性能报告。
步骤序:
navigate_page({ url: "<URL>" })→ 等首屏稳定(chrome-devtools-mcp 默认会等 DOM load + 一段 networkidle)。如果想确保某个文本就绪,紧跟wait_for({ text: ["<headline>", "<other selector text>"] })——注意 v1.1.x 的wait_for.text是 数组(必填),不是字符串;可选timeout是 毫秒整数(默认 30000)。performance_start_trace({ reload: true, autoStop: true })→performance_stop_trace()→ trace 自动落 chrome-devtools-mcp 报告的路径 (可显式传filePath: "./out/trace.json")。关键 web vitals(LCP / CLS / INP / TBT / FCP / Speed Index)就在performance_stop_trace返回的 metric summary 里,直接抽出来写./out/metrics.json;不要调performance_analyze_insight()拿这些数字——v1.1.x 的performance_analyze_insight是深度诊断工具,必须传insightSetId和insightName(都是必填字符串),用来对performance_stop_trace返回的 insight 列表里的某一条(如"LCPDiscovery"/"RenderBlocking"/"DocumentLatency")做深挖。无参直接调会返回MCP error -32602: insightSetId Required, insightName Required。 推荐用法:metrics 走 step 2;可选 step 2.5 = 对你最关心的 insight 跑performance_analyze_insight({ insightSetId: "<id from stop_trace>", insightName: "<name from stop_trace>" }),把 actionable 建议合并进verdict.md。take_screenshot({ format: "png" })→ 抓一张 1366×768 / 1280×800 首屏; 若performance_stop_trace返回的 metric summary 报告了 LCP element, 在截图前evaluate_script注入 Appendix A 的脚本把 LCP 元素绿框高亮再截。 落./out/trace-screenshot.png。list_network_requests({ pageSize: 200 })→ 全量请求表写./out/network.json;agent 客户端侧再按transferSizeBytes降序、durationMs降序各取 Top 20 写入topBySize/topByDuration,便于verdict.md引用。- 主路径 — CLI Lighthouse:同一 Chrome 复用
--remote-debugging-port=9222(chrome-devtools-mcp 默认就是这条线),跑npx -y lighthouse@latest <url> --port=9222 --output html --output json --output-path ./out/report, 拿到 Performance / Accessibility / Best Practices / SEO 四个真实分数的 HTML + JSON 报告。重要:v1.1.x 的lighthouse_audit工具在描述里写 明 “covers accessibility, SEO, best practices, and agentic browsing. This excludes performance.”,所以它不能作为 Performance 报告的来源; 只在你需要 Accessibility / SEO / BP / agentic-browsing 的同 Chrome 补充 分数、或你的客户端屏蔽了 CLI Lighthouse 时,才单独调lighthouse_audit({ device: "desktop" })——这时它的产物明确不含 Performance score。 - 写
./out/verdict.md:一句话洞察 + 三条最高 ROI 优化建议;每条都引用metrics.json/network.json的具体数字,不许凭空写「图片好像很大」。 双语(中文 + English),便于把报告同时贴给中文/英文 stakeholder。
自检(绿灯标准):
./out/report.html≥ 200 KB,浏览器打开能看到 Performance / Accessibility / Best Practices / SEO 四个真实分数环。./out/metrics.json的scores.performance是 0–100 整数,不是null/ 占位的 99 /"99"。./out/trace.json是 Chrome DevTools 标准 trace(事件数 ≥ 几千条),不是 空数组。./out/trace-screenshot.png≥ 50 KB(一张全白的 1366×768 PNG 只有 ~3 KB; < 50 KB 通常意味着 navigation 没等好、截早了)。./out/network.json的totalRequests≥ 5(任何现代页面都至少 5 个请求);topBySize/topByDuration各 ≥ 10 条。./out/verdict.md的三条建议每条都至少出现一个report.html/metrics.json/network.json里能搜得到的数字。
Task 2 — 慢网 + 低端机模拟(slow-trace.json / slow-firstpaint.png / console.log / slow-verdict.md)
目标:看页面在低端 Android + 慢 3G 下到底有多痛。
步骤序:
emulate({ networkConditions: "Slow 3G", cpuThrottlingRate: 4 })—— v1.1.x 官方把 CPU + 网络合并到单一emulate工具,不是emulate_cpu/emulate_network。networkConditions是 enum,只接受"Offline"/"Slow 3G"/"Fast 3G"/"Slow 4G"/"Fast 4G"—— 任何其他字符串 (包括之前 round-1 文案里写的"No throttling")都会被服务端返回invalid_enum_value。cpuThrottlingRate是数字倍数(1 = 不限速, 4 = 低端 Android)。navigate_page({ url: "<URL>" })→take_snapshot()拿一份 accessibility tree 存./out/slow-tree.json。performance_start_trace({ reload: true, autoStop: true })→performance_stop_trace({ filePath: "./out/slow-trace.json" })→ 慢网 LCP 就在performance_stop_trace的返回里,记下来。take_screenshot({ format: "png" })→ 首屏;这张通常明显残缺/转圈/字体没 加载完。落./out/slow-firstpaint.png。list_console_messages()→ 把所有warning/error落./out/console.log一行一条。- 重置回正常网络再跑一次 baseline LCP:因为
networkConditions没有"No throttling"这个 enum,要清除节流有两条合法路径——- 简单路径:直接复用 Task 1 跑出的
./out/metrics.json里的 LCP 数字 做对照(推荐); - 同 session 清节流:再调一次
emulate({ cpuThrottlingRate: 1 })省略networkConditions字段(chrome-devtools-mcp 会把上一次Slow 3G设置清掉、恢复默认网络),然后再performance_start_trace→performance_stop_trace拿一份正常态 trace。 把限速下 LCP 与正常态 LCP 写入./out/slow-compare.json。
- 简单路径:直接复用 Task 1 跑出的
- 写
./out/slow-verdict.md:一句话总结「这个页面在慢网下用户体验如何」- 列出三个最阻塞首屏的资源(用
list_network_requests的真实 url + size 引用)。
- 列出三个最阻塞首屏的资源(用
自检:限速下 LCP 必须 > 正常态 LCP(否则 emulate 没生效);
./out/slow-firstpaint.png ≥ 30 KB;console.log 至少包含 trace start /
stop 这两条系统级 entry。
Task 3 — Heap snapshot 找内存泄漏(heap-before.heapsnapshot / heap-after.heapsnapshot / network.json / console.log / leak-verdict.md)
目标:怀疑应用在长时间使用后内存泄漏,对比两次 heap snapshot 把嫌疑 对象列出来。
可复现 demo 目标(QA / 新手第一次跑就用这条):https://clawvard.school/courses
https://clawvard.school/courses/ai-slides(两条 Next.js 路由,已在生产 环境长期存在;这两条 URL 即可产出全套 deliverable)。真实 诊断时换成你自己应用的两条 URL。
步骤序:
-
navigate_page({ url: "https://clawvard.school/courses" })→wait_for({ text: ["Hand your agent"] })等首屏稳定。 注意 v1.1.x 的wait_for.text是string[]必填——传单字符串会拿到Expected array, received string;wait_for没有time字段 (round-1 文案错把它当 sleep 用),单纯停顿请改用evaluate_script("await new Promise(r => setTimeout(r, 500))")或把wait_for({ text: [...] })直接等下一个文本出现。可选timeout是毫秒 整数(默认 30000)。wait_for匹配的是页面 实际渲染出来 的文本: 也就是 chrome-devtools-mcp 取 accessibility tree 里 "visible" 的字符串。 两个 round-4 现实陷阱: (a) SSR HTML 里有但页面没显示 → 不算;例如/courses的 SSR HTML 里 有"Hottest courses right now"字符串(Hot rail),但 CI 环境 渲染时该节点要么被 conditional render 跳掉、要么没进 accessibility tree,wait_for 抓不到,r4 已实测会 timeout。 (b) 带text-transform: uppercase的元素,DOMtextContent是 sentence case,但 accessibility tree 给的是渲染后大写。例如/courses/<id>详情页的"What you get"头其实带 Tailwinduppercaseclass,wait_for 看到的是"WHAT YOU GET"。 选 wait_for text 时只挑两类:① h1 / 课程标题这种 mixed-case 必渲染的 字符串;② 页面 lede / hero 第一句中text-transform: none的明文。本课 下面挑的两条均符合,适合作为稳定的可见文本锚点。 -
before snapshot:
take_heapsnapshot({ filePath: "./out/heap-before.heapsnapshot" })—— v1.1.x 官方工具就叫take_heapsnapshot,不是performance_collect_heap_snapshot。filePath 是必填字符串。可选先evaluate_script跑一句if (window.gc) window.gc(); else {}触发一次 非强制 GC(需要 Chrome 启动时加--js-flags="--expose-gc",否则 Chrome 会在 snapshot 前自动 GC,没有也行)。 -
触发可疑路径 N 次 — demo 目标在生产
clawvard.school上 走两条 真实存在的路由 来回切换,不要用 selector 抓 SPA 链接:QA 已经在 round-3 复现过document.querySelectorAll('a[href^="/courses/"]')在navigate_page刚返回的瞬间因 React hydration 未完成而返回[]的 边界情况,所以这里全程用location.href = "..."走 完整 URL 导航—— 它不依赖任何 selector / 元素索引 / hydration 时机,永远可复现:evaluate_script("location.href='/courses'")→wait_for({ text: ["Hand your agent"] })(/courses页 hero lede 第一句 "Hand your agent one card — it learns on the spot and ships the real thing.",无 CSS 大写转换, 可以作为稳定可见文本。不要等"Hottest courses right now"——它只在 SSR HTML 里 accessibility snapshot 抓不到,r4 实测会 timeout。)evaluate_script("location.href='/courses/ai-slides'")→wait_for({ text: ["AI Slides"] })(AI Slides是详情页<h1>课程标题,mixed-case 必渲染,跨页面 最稳。不要等"What you get"——同节点带 Tailwindtext-transform: uppercase,accessibility tree 给的是"WHAT YOU GET",r4 已实测wait_for(["What you get"])抓不到。 如果非要等大写版可写wait_for({ text: ["WHAT YOU GET"] }),但 h1 更稳。)- 想给浏览器额外 500 ms 喘息:
evaluate_script("await new Promise(r => setTimeout(r, 500))")。 - 循环 5 次。
重要的诊断学说明:
location.href = "..."是 完整页面刷新,每轮都 会把上一轮的 JS heap 清空——所以 demo 目标抓出来的heap-before/heap-after量级很接近、Top constructor 差量不会很大,这是正常的、 不代表抓 snapshot 失败;QA 只要看.heapsnapshot文件存在、能拖回 DevTools 打开就算 deliverable 合格。真实诊断你自己的 SPA 时,请 (a) 把 demo 路由换成你应用里两条会复用同一个 React tree 的路径; (b) 改用 SPA 原生 路由(Next.js 用window.next?.router?.push("/...")或 React Router 用history.pushState({}, "", "/...")+ dispatchpopstate),保留 React tree、放大 Detached DOM / closure / listener 泄漏信号。 -
after snapshot:
take_heapsnapshot({ filePath: "./out/heap-after.heapsnapshot" })。 -
diff 分析(不要凭想象写) —— v1.1.x heap snapshot 工具用
filePath作为 snapshot 句柄,不是heapsnapshotId;class nodes 用id(class id,整数);retainers 用nodeId(节点 id,整数)。错传会拿到filePath Required/id Required验证错误。正确流程:get_heapsnapshot_summary({ filePath: "./out/heap-after.heapsnapshot" })→ 拿到 class 直方图,每条带id/name/count/selfSize/retainedSize。- 对 Top N 可疑 constructor(
Detached HTMLDivElement/Detached HTMLAnchorElement/EventListener/Closure/ 你自己 SPA 的 component class),用 summary 返回的整数id跑get_heapsnapshot_class_nodes({ filePath: "./out/heap-after.heapsnapshot", id: <class-id> })拿这一类节点的列表(每条带nodeId)。 get_heapsnapshot_retainers({ filePath: "./out/heap-after.heapsnapshot", nodeId: <node-id> })找谁还在持有它。- 同样三套查询对
./out/heap-before.heapsnapshot各跑一遍拿基线。
-
list_network_requests()+list_console_messages()一并落./out/network.json/./out/console.log。 -
写
./out/leak-verdict.md:对比 before / after 的 constructor 直方图,列 Top 5 增长最快的 constructor 类型;每条引用get_heapsnapshot_retainers返回的 retainer 路径;给一句话结论「是否疑似泄漏、最值得检查的 Detached DOM / closure / event listener 是哪类」。
自检:两份 .heapsnapshot 必须能被拖回 Chrome DevTools Memory 面板
正常打开(snapshot 格式由 Chrome 序列化,不是任意 JSON)。leak-verdict.md
里引用的 Top 5 constructor 必须在 snapshot 里能用 DevTools 搜得到,且能在
agent 这一轮 get_heapsnapshot_class_nodes 的返回里按 id 找到对应的
节点列表。
铁律(红线)
- agent 自带模型即可。SOP、参考脚本、popularTasks 的默认执行路径 不调任何额外的 LLM 推理 API,也不依赖任何付费第三方 perf service (PageSpeed Insights API 直连免费,但本课不依赖它,所有 perf 数字来自 本地 chrome-devtools-mcp + Lighthouse 直跑)。
- 不接 Clawvard 一方 service SDK:本课
commercialApi=false、runsLocally=true; 不引入 Clawvard SDK、不调用service.clawvard.*命名空间;service 化是 后续产品化方向,不在本课范围。 - deliverable 必须真实:所有
./out/*必须由 MCP 工具或 Lighthouse 真实 生成。任何一项是 hand-typed mock / placeholder / 灰盒占位 = 任务失败。verdict.md引用的每个数字都能在metrics.json/network.json找到, 对不上 = 任务失败。 - 目标站尊重:单站单 Chrome、连续两次
performance_start_trace之间 ≥ 1 s;遵守目标站 robots.txt 与公开 ToS;课程公开 popularTasks 推荐用自 己的网站、https://clawvard.school、https://web.dev、https://news.ycombinator.com等公共站点,不要替用户去碰生产业务站。 - 不替用户管凭据:登录后页面的 perf 审计需要用户在每次 prompt 里显式 贴入凭据;agent 不持久化、不写进 trace 元数据、不上传到任何第三方。
产出物(Task 1 六件套)
out/
├── report.html # Lighthouse 13.x 原生 HTML 报告(四分数环 + 全量诊断)
├── metrics.json # scores + LCP / FCP / SI / TBT / CLS / TTI / 服务端响应
├── trace.json # Chrome DevTools 标准 perf trace(可拖回 Performance 面板)
├── trace-screenshot.png # 1366×768 首屏 + LCP 元素绿框高亮
├── network.json # 全量请求 + byType + topBySize + topByDuration
└── verdict.md # 双语一句话洞察 + 三条最高 ROI 优化(每条引用真实数字)
六个文件一起就是这门课的 唯一 deliverable。课程详情页的 showcase 直接 渲染同名六件套——QA出来的更好版本可以直接覆盖回 PR。
Appendix A — LCP 元素高亮注入脚本
如果你的 chrome-devtools-mcp 没在 take_screenshot 阶段把 LCP element 自动
高亮(不同版本行为可能不同),在 take_screenshot 之前用 evaluate_script
注入下面这段,让 LCP 元素四周出现一圈绿框 + label:
(async () => {
// 利用 PerformanceObserver 抓 LCP element 的真实坐标
const lcp = await new Promise((resolve) => {
let last = null;
try {
new PerformanceObserver((list) => {
last = list.getEntries().pop();
}).observe({ type: 'largest-contentful-paint', buffered: true });
} catch {}
setTimeout(() => {
if (!last) return resolve(null);
const el = last.element;
const r = el?.getBoundingClientRect?.();
resolve(r && { tag: el.tagName.toLowerCase(), x: r.x, y: r.y, w: r.width, h: r.height });
}, 250);
});
if (!lcp) return null;
const overlay = document.createElement('div');
overlay.style.cssText = `position:fixed;left:${lcp.x}px;top:${lcp.y}px;width:${lcp.w}px;height:${lcp.h}px;border:3px solid #16a34a;background:rgba(22,163,74,.05);box-shadow:0 0 0 9999px rgba(0,0,0,.18);z-index:2147483647;pointer-events:none;border-radius:6px;`;
const tag = document.createElement('div');
tag.textContent = 'LCP element · ' + lcp.tag;
tag.style.cssText = 'position:absolute;left:0;top:-26px;background:#16a34a;color:#fff;font:700 12px ui-sans-serif,system-ui;padding:3px 8px;border-radius:4px;';
overlay.appendChild(tag);
document.body.appendChild(overlay);
return lcp;
})();
Appendix B — IDE 无 MCP 也能跑(puppeteer-core fallback)
某些环境里 MCP 客户端不可用(CI / 老编辑器)。下面这段独立 Node 脚本不依赖
任何 MCP,靠 puppeteer-core 复用 Chrome for Testing 二进制就能产出同款
trace.json / network.json / trace-screenshot.png。仍然零商业 LLM key——puppeteer-core 只是浏览器自动化,不是推理客户端。
npm init -y >/dev/null && npm install --no-save puppeteer-core@22
// fallback.mjs — node fallback.mjs <url>
import puppeteer from 'puppeteer-core';
import { writeFileSync, mkdirSync } from 'node:fs';
const CHROME = process.env.CHROME_PATH || '/usr/bin/google-chrome';
const URL = process.argv[2] || 'https://clawvard.school/';
mkdirSync('./out', { recursive: true });
const browser = await puppeteer.launch({
executablePath: CHROME, headless: true,
args: ['--no-sandbox', '--disable-gpu'],
defaultViewport: { width: 1366, height: 768 },
});
const page = await browser.newPage();
const requests = []; const responses = new Map();
page.on('request', (r) => requests.push({ url: r.url(), method: r.method(), resourceType: r.resourceType(), startTime: Date.now() }));
page.on('response', (r) => responses.set(r.url(), { status: r.status(), mimeType: r.headers()['content-type'] || '', transferSizeBytes: Number(r.headers()['content-length'] || 0), endTime: Date.now(), fromCache: r.fromCache() }));
await page.tracing.start({ path: './out/trace.json', screenshots: true });
await page.goto(URL, { waitUntil: 'networkidle2', timeout: 60000 });
await page.screenshot({ path: './out/trace-screenshot.png', clip: { x: 0, y: 0, width: 1366, height: 768 } });
await page.tracing.stop();
const merged = requests.map((r) => {
const x = responses.get(r.url) || {};
return { ...r, ...x, durationMs: (x.endTime || r.startTime) - r.startTime };
});
const topBySize = [...merged].sort((a, b) => b.transferSizeBytes - a.transferSizeBytes).slice(0, 20);
const topByDuration = [...merged].sort((a, b) => b.durationMs - a.durationMs).slice(0, 20);
writeFileSync('./out/network.json', JSON.stringify({ targetUrl: URL, totalRequests: merged.length, totalBytes: merged.reduce((s, x) => s + (x.transferSizeBytes || 0), 0), topBySize, topByDuration, all: merged.slice(0, 50) }, null, 2));
await browser.close();
console.log('done');
Lighthouse 仍走 npx -y lighthouse@latest <url> --output html --output json --output-path ./out/report,
和上面 fallback 互不冲突。
学习完成后
告诉用户:
我已经学会了 agent-perf-audit。给我一个公开 URL(你的网站、竞品、随便一 个想体检的页面),我在本地起 Chrome 用 chrome-devtools-mcp 跑 Lighthouse、 采性能 trace、抓 network HAR;落
out/report.html+out/metrics.json+out/trace.json+out/trace-screenshot.png+out/network.json+out/verdict.md六件套:双语 verdict 引用真实数字、三条建议各自指向具体 资源。全程本地、零商业 LLM key、零 Clawvard 后端调用。