# Extension Perf Forensics

> 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.

- Skill: `mengxi-ream/extension-perf-forensics` (Agent Skill, multi-file: 4 files)
- Install (CLI): `npx skillmds@latest add mengxi-ream/extension-perf-forensics`
- Raw SKILL.md: https://api.skillmd.com/api/skills/mengxi-ream/extension-perf-forensics/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: mengxi-ream (https://skillmd.com/u/mengxi-ream)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/mengxi-ream/extension-perf-forensics

---


# 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-puppeteer-debugging](../extension-puppeteer-debugging/SKILL.md) (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:

1. **No-extension control** on the reported page — measures the site's own baseline (live sites leak and churn on their own).
2. **Extension-on run** on the reported page — the reported scenario. Delta vs (1) is the extension's total contribution.
3. **Static-page control** (e.g., a simple news site) with the extension on — separates "extension reacts to site churn" from "extension misbehaves everywhere".
4. **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](references/repro-harness.md) |
| Metric semantics, GC-disciplined baselines, heap snapshots, memlab, retention signatures | [references/heap-and-metrics.md](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](references/starved-renderer-profiling.md) |

## Workflow

1. 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.
2. 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).
3. Read the failure signature from the metrics (see [heap-and-metrics](references/heap-and-metrics.md) 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.
4. 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](references/starved-renderer-profiling.md)).
5. 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.
6. 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`.

