Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsCommunityBlog
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

Back to skills

Browser Profiling

ASecurity

Profile Chrome/JavaScript/React apps to diagnose slowness. Covers triage (load vs interaction vs scroll), Playwright-based baseline capture, React Profiler component, Chrome Performance panel interpretation, bundle analysis, memory leak detection, fix verification, and programmatic large-trace analysis via Perfetto's trace_processor (the pprof-equivalent for Chrome traces). Invoke when a browser or React app is noticeably slow and you need to find and fix the bottleneck, or when you have a do...

8 stars
0 votes
0 copies
0 views
Added 9/20/2026
developmentjavascriptpythonrustgojavashellbashsqlreactnode

Works with

cliapimcp

Security Analysis

A96/100
mediumUses curl or wget to download content

Scanned 9/20/2026

Install to Claude Code

$npx -y skills add tstapler/dotfiles --skill browser-profiling --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Browser Profiling?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Browser Profiling
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/tstapler-browser-profiling/badge)](https://www.skillsdirectory.com/skills/tstapler-browser-profiling)

More formats (shields.io, HTML) on the badges page.

Download Zip
Files
SKILL.md
---
name: browser-profiling
description: Profile Chrome/JavaScript/React apps to diagnose slowness. Covers triage (load vs interaction vs scroll), Playwright-based baseline capture, React Profiler component, Chrome Performance panel interpretation, bundle analysis, memory leak detection, fix verification, and programmatic large-trace analysis via Perfetto's trace_processor (the pprof-equivalent for Chrome traces). Invoke when a browser or React app is noticeably slow and you need to find and fix the bottleneck, or when you have a downloaded `.json`/`.json.gz` Performance-panel trace to analyze.
---

# Browser / React Performance Profiling

End-to-end workflow: triage the complaint → capture numeric baseline → isolate the layer (browser vs React vs network) → deep-dive the bottleneck → fix → verify the fix matches the baseline.

## Triage Table — Classify Before You Profile

| Symptom | Likely Layer | Primary Tool |
|---------|-------------|-------------|
| Slow initial page load | Network / bundle | Lighthouse, bundle analyzer, coverage API |
| Slow after clicking / typing | JS / React renders | Performance panel, React `<Profiler>` |
| Scrolling jank / dropped frames | Layout thrashing / paint | FPS meter, Performance panel during scroll |
| Memory grows and never recovers | Memory leak | Memory panel Heap Snapshot + Allocation Timeline |
| App slow only on low-end devices | Long tasks / large bundles | Performance panel with 6× CPU throttle |

---

## Step 1 — Capture a Numeric Baseline

Never skip this. "It feels slow" is not a measurement. Run a Playwright script before touching code that captures `page.metrics()` deltas, long-task counts, and a Chrome trace file — then compare after fixes.

See [Baseline Capture and Fix Verification](references/baseline-and-verification.md) for the full `perf-baseline.js` script and the Step 6 verification workflow.

---

## Step 2 — Add React `<Profiler>` to Slow Subtrees

Wrap the suspect component tree in a `<Profiler>`; its `onRender` callback reports `actualDuration` (this render) vs `baseDuration` (worst case without memoization).

**Key ratio**: `actualDuration / baseDuration`
- Near **1.0** → memoization is absent or not helping; every component in the subtree re-renders
- Near **0.1** → memoization works; only ~10% of the subtree re-renders on updates

In the Chrome DevTools Performance panel (React 19+), the "Scheduler" and "Components" tracks show wide bars in the **Blocking** lane (synchronous updates) or a **Cascading updates** flag (a render triggering another render).

See [React Profiler and Re-render Detection](references/react-profiler.md) for the `<Profiler>` code and the `why-did-you-render` dev tool.

---

## Step 3 — Read the Chrome Performance Panel

1. Open DevTools → Performance tab
2. Set **CPU throttling** to `4×`–`6×` to simulate mobile (reproduces problems that don't show on fast machines)
3. Click **Record** → reproduce the slow action → **Stop**
4. Read the **Main** thread row: Bottom-Up tab sorted by Self Time finds the expensive function; Call Tree tab sorted by Total Time finds the entry point that triggers the most work

See [Reading the Chrome Performance Panel](references/chrome-performance-panel.md) for the flamechart legend and key event names to search for.

---

## Step 3.5 — Analyzing a Trace Programmatically (Perfetto `trace_processor` — the pprof of Chrome traces)

A DevTools Performance recording — from the manual "Record → Save profile..." flow, or `browser.startTracing()` in Step 1 — is a JSON array of trace events (`ts`, `dur`, `ph`, `name`, `cat`, `pid`/`tid`, `args`). On a real session this file is routinely **hundreds of MB**, and it is the exact same mistake as `curl`-ing a raw Go pprof profile to grep by hand: `jq`, `python -m json.load`, or `grep` over the whole array is slow, easy to get subtly wrong (matching the wrong process/thread, double-counting nested slices), and burns a huge amount of context if the output lands in a conversation. **Load it into Perfetto's `trace_processor` and query it with SQL instead** — this is the direct equivalent of `go tool pprof -top`: one purpose-built tool does the parsing/indexing, and you get ranked, aggregated answers instead of raw event soup.

### Get the tool (one-time)

```bash
curl -LO https://get.perfetto.dev/trace_processor
chmod +x trace_processor
# First run downloads the native binary for your platform to ~/.local/share/perfetto/prebuilts (cached after).
```

`trace_processor` loads Chrome's JSON trace format directly — **including gzipped `.json.gz` downloads**, no manual `gunzip` needed. It also loads `.perfetto-trace` protobuf traces, `.heapprofile` files, and Android traces, so the same tool covers Playwright's `browser.startTracing()` output and a manually-downloaded DevTools trace.

### Query mode (non-interactive — prefer this for scripted/agent use)

```bash
./trace_processor -q <(echo "SELECT name, COUNT(*) AS cnt, SUM(dur) AS total_ns FROM slice GROUP BY name ORDER BY total_ns DESC LIMIT 20;") mytrace.json.gz
```

Or drop into the interactive SQL shell for exploration: `./trace_processor mytrace.json.gz` then type queries at the `>` prompt. Durations (`dur`, `ts`) are in **nanoseconds**.

### Canonical queries (the `-top` / `-list` equivalents)

```sql
-- Top 20 event names by total time across the whole trace (go tool pprof -top)
SELECT name, COUNT(*) AS cnt, SUM(dur) AS total_ns
FROM slice GROUP BY name ORDER BY total_ns DESC LIMIT 20;

-- Long tasks (>50ms) on a named thread, with wall-clock offset from trace start
SELECT (s.ts - (SELECT MIN(ts) FROM slice)) / 1e6 AS ms_from_start,
       s.dur / 1e6 AS dur_ms, s.name, t.name AS thread
FROM slice s
JOIN thread_track tt ON s.track_id = tt.id
JOIN thread t ON tt.utid = t.utid
WHERE s.dur > 50 * 1e6
ORDER BY s.dur DESC LIMIT 30;

-- Discover what counter tracks exist before querying one blind (mirrors
-- "list available tag keys before guessing an ASL query" for Atlas) —
-- Chrome's JS-heap counter is typically named 'JS Heap' or similar; verify
-- per-trace rather than assuming.
SELECT DISTINCT name FROM counter_track;

-- JS heap size over time once you have the exact counter_track name
SELECT c.ts, c.value
FROM counter c JOIN counter_track ct ON c.track_id = ct.id
WHERE ct.name = 'JS Heap' ORDER BY c.ts;

-- GC pauses (self time), ranked — allocation-churn signal
SELECT name, COUNT(*) AS cnt, SUM(dur) AS total_ns, AVG(dur) AS avg_ns
FROM slice WHERE name LIKE '%GC%' GROUP BY name ORDER BY total_ns DESC;
```

**Gotcha learned the hard way**: a multi-process/multi-tab trace mixes every process's counters and threads in one file — `SELECT MIN(value) FROM counter ...` across all processes can return a near-zero value that belongs to an unrelated tab/extension, not the page you're debugging. Filter to the right `pid`/`upid` first (`SELECT * FROM process;` to find it) rather than trusting an unscoped aggregate.

### When to still use the DevTools UI instead

`trace_processor` is for *ranking and aggregating* — finding which function/task/counter dominates, across a trace too large to eyeball. It does not replace:
- **The flamechart itself** for understanding *why* a specific task is slow (call stack, nesting) — drag the same trace file into DevTools Performance panel for that.
- **Heap Snapshot comparison** (Step 5) — a `.heapsnapshot` is a different format (object graph, not a timeline) and needs the Memory panel or `--inuse` uses; `trace_processor` cannot substitute for finding *which objects* are retained, only *how much* memory a counter shows over time.

### LLM-native alternative

A community MCP server (`perfetto-1` on mcpmarket.com) wraps `trace_processor_shell` and lets an LLM run PerfettoSQL queries directly against a trace without shelling out — worth adopting if this analysis becomes frequent enough to justify adding an MCP server, but the `curl`+SQL-file approach above needs no setup and is fine for occasional use.

---

## Step 4 — Diagnose by Layer

Once you know the layer (from Step 1's trace or Step 3's panel), match the symptom to a fix:

| Layer | Symptom |
|-------|---------|
| React re-render cascade | `<Profiler>` ratio near 1.0; most of the tree re-renders on every keystroke |
| Long tasks / JS-heavy interaction | Red-flagged tasks >50ms on interaction; INP > 200ms |
| Layout thrashing | "Forced reflow" warnings; `Layout` events interleaved with JS |
| Bundle size / dead code | Slow initial load; large `node_modules` in the critical bundle |

See [Diagnose by Layer](references/diagnose-by-layer.md) for the anti-pattern tables and fix code for each (memoization, `scheduler.yield()`/`startTransition`, batched reads/writes, `source-map-explorer` + `React.lazy`).

---

## Step 5 — Memory Leak Detection

**Symptom**: JS Heap grows over time in Task Manager and doesn't return to baseline after GC.

Quick check: run 10 cycles of the suspect action via Playwright, force GC, and compare `page.metrics()` heap size before/after — growth over ~5MB warrants a DevTools Heap Snapshot comparison (sort by "# New" objects). The most common React leak is a missing `useEffect` cleanup for an event listener.

See [Memory Leak Detection](references/memory-leak-detection.md) for the Playwright check script and the DevTools Heap Snapshot workflow.

---

## Step 6 — Verify the Fix

Re-run the Step 1 baseline script with a new label and compare against the original trace. Pass criteria: reduced `scriptDuration` delta, fewer/shorter long tasks, improved `<Profiler>` ratio, no new long tasks elsewhere, and (for leaks) heap growth under 1MB across 10 cycles. Full checklist in [Baseline Capture and Fix Verification](references/baseline-and-verification.md).

---

## Quick Reference

| Goal | What to use |
|------|------------|
| Establish numeric baseline | `page.metrics()` before/after + `browser.startTracing()` |
| Find expensive React subtree | `<Profiler id="X" onRender={onRender}>` |
| Find why a component re-rendered | React DevTools Components track → click component → "Why did this render?" |
| Find long tasks | Performance panel red-flagged bars / `PerformanceObserver longtask` |
| Find layout thrashing | Performance panel → "Forced reflow" warnings |
| Identify unused JS | `page.coverage.startJSCoverage()` |
| Visualize bundle composition | `npx source-map-explorer 'build/static/js/*.js'` |
| Find memory leaks | Memory panel Heap Snapshot comparison (before/after cycles) |
| Move work off main thread | `startTransition`, `scheduler.yield()`, Web Workers |
| Reproduce mobile conditions | CPU throttling 4–6× in DevTools Performance panel |

## Common Pitfalls

- **Profiling a dev build** — React dev builds are 2–3× slower than production; always profile a production or `profiling` build for realistic numbers. Use `react-dom/profiling` to keep Profiler tracks in production.
- **One `<Profiler>` wrapping the entire app** — too coarse to find the bottleneck; wrap the specific subtree you suspect
- **Optimizing without a baseline** — if you don't measure before, you can't prove the fix worked; always capture `page.metrics()` deltas
- **`useMemo`/`useCallback` without React.memo children** — these only help when a memoized child exists; adding them without a `React.memo`-wrapped consumer is pure overhead
- **Fixing inline objects in JSX without measuring first** — sometimes the cost is in computation, not re-render count; check `<Profiler>` before assuming the cause
- **Missing `key` stability after "fixing" keys** — switching from index keys to IDs is correct, but only if the IDs are stable across renders; generated UUIDs on each render are worse than index keys
- **Memory panel without forcing GC first** — always click the trash can icon (Force GC) in the Memory panel before taking a snapshot to avoid counting GC-eligible objects as leaks
- **Long task fix that only moves the work** — `startTransition` delays work; it doesn't eliminate it. If the work is truly too expensive, it needs to be broken up or moved to a Worker.

Attribution

tstaplertstapler
View sourceMore from tstapler →
SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Your tool, in front of Claude Code builders.

3 founder slots · $299/mo · GSC-verified traffic · sponsors can never buy grades.

See placements

Related Skills

Browser Extension Developer

Use this skill when developing or maintaining browser extension code in the `browser/` directory, including Chrome/Firefox/Edge compatibility, content scripts, background scripts, or i18n updates.

281612 votes

Seo Optimizer

SEO optimization with keyword analysis, readability assessment, technical validation, content quality. Use for search rankings, blog posts, content audits, or encountering keyword density, readability scores, meta tags, schema markup errors.

2132 votes

Google Official Seo Guide

Official Google SEO guide covering search optimization, best practices, Search Console, crawling, indexing, and improving website search visibility based on official Google documentation

1862 votes

Tanstack Start

Build a full-stack TanStack Start app on Cloudflare Workers from scratch — SSR, file-based routing, server functions, D1+Drizzle, better-auth, Tailwind v4+shadcn/ui. Use whenever the user mentions TanStack Start, asks to scaffold a full-stack Cloudflare app with SSR, wants an SSR dashboard, or asks for a React 19 + Cloudflare Workers app with file-based routing and server functions — even if they don't name TanStack Start specifically. No template repo — Claude generates every file fresh per ...

9881 votes

Pentest

PTES-aligned adversarial security audit for backend, frontend, and mobile applications. Produces a CVSS-scored Hacker Report with verified PoCs and phased remediation.

5491 votes
View all in development →