| name | extension-perf-forensics |
| description | Diagnose extension-caused memory leaks, freezes, and CPU storms on live third-party sites via controlled attribution experiments, CDP metrics, heap snapshots, and browser-process trace profiling that works even when the renderer is starved. |
| metadata | {"author":"read-frog","version":"1.0.0"} |
Extension Performance Forensics
Use this skill when a user reports that the extension makes a page leak memory, freeze, or burn CPU ("卡死", OOM, fans spinning, page renders wrong AND slows down), especially on a specific third-party site. It turns "it feels broken" into a measured, attributed root cause with pass/fail numbers you can re-run after every fix.
This skill complements extension-real-browser-testing (functional validation, screenshots). Use that one to prove behavior; use this one to find out who is burning the resources.
Core principle: the attribution ladder
Never conclude from a single run. A perf report on a live site has at least four possible culprits, and the fix differs for each. Run the ladder top-down; each rung is the same scripted interaction, changing exactly one variable:
- No-extension control on the reported page — measures the site's own baseline (live sites leak and churn on their own).
- Extension-on run on the reported page — the reported scenario. Delta vs (1) is the extension's total contribution.
- Static-page control (e.g., a simple news site) with the extension on — separates "extension reacts to site churn" from "extension misbehaves everywhere".
- Feature-isolation runs — same page, extension on, but one suspect neutralized (e.g., pre-mark a DOM region as excluded before the extension walks it; force a different config). If neutralizing X makes the numbers clean, X is the trigger.
Two hard-won corollaries:
- Verify the effective config after the run (dump it from the extension's storage in the same profile). This session's first three runs silently tested the wrong target language because an onboarding page had overwritten it — every conclusion drawn before the dump was mis-attributed.
- Suspect the site, not just the extension. Sites ship their own unbounded fit-loops (
while (overflows()) moveItem() inside a ResizeObserver). Any extension that changes element sizes can trip them. If rung (4) with a region excluded is clean while your own code is already loop-free, read the site's bundle — the hot loop may be theirs, and the right fix is an exclusion rule, not more extension code.
Quick reference
| Topic | Reference |
|---|
| Repro harness: launching with the extension (Chrome 137+), config patching races, instrumentation, sampling loop, freeze tolerance, control modes | references/repro-harness.md |
| Metric semantics, GC-disciplined baselines, heap snapshots, memlab, retention signatures | references/heap-and-metrics.md |
| Profiling a starved renderer via the Tracing domain, mapping minified frames back to site code, the detached-DOM churn signature | references/starved-renderer-profiling.md |
Workflow
- Build the exact artifact under suspicion (or the fix candidate) and script the reported interaction (load → wait for the feature to engage → N interaction cycles → idle watch → final measurements). Log every sample as JSONL.
- Run the attribution ladder. Force-GC before baseline and final samples; snapshot heap only in runs where you are not measuring timing (snapshots pause the world).
- Read the failure signature from the metrics (see heap-and-metrics for the table). The three big ones:
- heap grows after GC with DOM unchanged → JS-side retention → heap snapshots + memlab retainers;
- CDP
Nodes explodes while getElementsByTagName("*").length stays flat AND layout/style counters freeze → a JS loop building detached DOM at full speed → CPU profile, not heap;
- everything stable but
TaskDuration runs hot → churn without retention → count your own DOM insert/remove operations per cycle.
- If probes hang (evaluate timeouts,
Profiler.start never returns), the renderer main thread is starved — capture the CPU profile through the browser-process Tracing domain instead (starved-renderer-profiling).
- Attribute the hot frames. Extension frames → fix the extension. Site frames → reproduce the site's trigger without the extension (simulate the DOM change by hand), then neutralize via exclusion/site rule and re-verify.
- After each fix: rebuild, rerun the identical harness, compare against the recorded broken/control baselines in a pass-criteria table (nodes order-of-magnitude, TaskDuration, freeze count, heap-after-GC delta, feature-still-works count). A fix that only moves one number may have missed a second cooperating root cause — this session's storm fix was correct yet the freeze persisted until the site's own loop was also neutralized.
Verdict discipline
- Distinguish harmless churn (allocated then GC'd; oscillating
Nodes) from retention (survives forced GC). Only the latter is a leak.
- Distinguish deterministic failures from storm-intensity-dependent ones; re-run the broken build if a fix seems to "work" on a single lucky run.
- Record which failure modes are absorbed by existing safety nets vs which escape them; prioritize the escaping ones.
Gotchas that cost hours
- Chrome ≥137 ignores
--load-extension. Launch with pipe: true, enableExtensions: true and call browser.installExtension(path) (Puppeteer ≥22.11). The install does not persist across sessions — re-install per run.
- Config patch races: the extension's own init/migrations/onboarding can clobber your patch seconds after install. Patch → wait → re-assert → verify before navigating, and re-verify after the run.
- Cloudflare: fresh automated profiles get challenged;
curl gets 5xx. Grab site bundles via response interception inside a real page load, or use an interactive in-app browser pane which typically passes.
- zsh nukes
PATH if a loop variable is named path (lowercase path is tied to PATH).
- Run timing-sensitive rungs sequentially, one browser at a time, separate profile dirs per experiment.
- WXT builds need the env files; copy
.env.development / .env.production into worktrees before pnpm build.