Skip to content

Skills · frontend

web-performance-review

Read a Lighthouse report or PageSpeed Insights result, with field data when there is any (CrUX, PSI loadingExperience or RUM p75), and say what makes the page slow for real users and what to fix first, ranked by how much each fix moves LCP, INP and CLS at the 75th percentile. Names the LCP element and which LCP phase dominates, the elements that shift and why, the long tasks and third parties by main-thread time, and treats estimated savings as lab estimates that overlap. Use when the user shares a Lighthouse JSON or PSI result and asks "why is this page slow", "what should we fix first", "we score 92 but Search Console says LCP is poor", "did our change fix INP", "review this Lighthouse report", or "how do we pass Core Web Vitals".

Pack frontendTier standardJob Build and review softwarev0.1.3

npx skilldrop-cli install web-performance-review
Other ways to install

With the sibling skills it hands off to:

npx skilldrop-cli install web-performance-review --with-related

The whole pack it belongs to:

npx skilldrop-cli install --pack frontend

By hand, from a clone of the repo:

cp -R packs/frontend/skills/web-performance-review ~/.claude/skills/

In Claude Code, with the frontend plugin installed, invoke it as:

/frontend:web-performance-review

Try it

A realistic prompt from the skill's acceptance evals:

Our marketing lead says the raincoats collection page is fine because PageSpeed gives it 92 on mobile, but Search Console keeps flagging LCP as poor. Here's the PageSpeed Insights API response: psi.json. What's actually making it slow and what should the team fix first?

What a good result looks like

Example output

Worked example: Quayside Ledger article page · the skill's own worked example, source

Worked example

Input

The user says: "Our article pages feel janky and Search Console is unhappy. Here's a Lighthouse run of the Harbour Bridge story. Our RUM dashboard says, for mobile over the last 28 days, LCP p75 3.4 s, INP p75 160 ms, CLS p75 0.31. What do we fix first?" and supplies quayside-article-lhr.json, a hand-built Lighthouse 12 result (default mobile, simulated throttling). All names and hosts are fictional.

The agent passes the RUM numbers to the script with the report:

python3 scripts/lh_summary.py examples/quayside-article-lhr.json \
  --lcp-p75 3400 --inp-p75 160 --cls-p75 0.31 --field-source "RUM, last 28 days, mobile"

Script output (real; run header and main-thread section cut)

## Field data (real users, p75): decides whether there is a problem
RUM, last 28 days, mobile

| Metric | p75 | Rating | Share good |
|---|---|---|---|
| LCP | 3.40 s | needs-improvement | - |
| INP | 160 ms | good | - |
| CLS | 0.31 | poor | - |

Core Web Vitals (field): FAILED: LCP needs-improvement, CLS poor

## Lab metrics (this run): explains why

Performance score: 78 (a lab weighting, not a Core Web Vitals result)

| Metric | Value | Lab rating | Threshold |
|---|---|---|---|
| FCP | 2.05 s | needs-improvement | good <= 1800ms, poor > 3000ms |
| LCP | 3.12 s | needs-improvement | good <= 2500ms, poor > 4000ms |
| TBT | 140 ms | good | good <= 200ms, poor > 600ms |
| CLS | 0.22 | needs-improvement | good <= 0.1, poor > 0.25 |
| Server response (lab TTFB) | 1.42 s | - | - |

## LCP element
Selector: article > figure.article-lead > img.article-lead__image
Flags: fetchpriority=high present

| Phase | Time | Share |
|---|---|---|
| TTFB | 1.65 s | 53% |
| Resource load delay | 180 ms | 6% |
| Resource load duration | 820 ms | 26% |
| Element render delay | 470 ms | 15% |
Dominant phase: TTFB

## Layout shift culprits (load window only; shifts after load are not in a lab run)
- body > div.ad-slot.ad-slot--top: 0.150, cause: Injected iframe (https://ads.adharbor.example/serve/frame.html)
- article > figure.article-lead > img.article-lead__image: 0.070, cause: Unsized image element

## Failing opportunities and diagnostics, ranked by the vital they move
| Audit | Est. load saving | Metric savings | Note |
|---|---|---|---|
| * server-response-time: Reduce initial server response time | 1.32 s | FCP 1.32 s, LCP 1.32 s |  |
| * unused-css-rules: Reduce unused CSS | 150 ms / 69 KiB | FCP 150 ms, LCP 150 ms |  |
| * layout-shifts: Avoid large layout shifts | - | CLS 0.22 |  |
| * unsized-images: Image elements do not have explicit width and height | - | none | acts on CLS directly |

What the agent decides

  • CLS is the vital furthest past its threshold (0.31 against 0.1, poor), so the shift fixes go first, even though the script's table lists the server fix first by milliseconds.
  • The LCP image already has fetchpriority="high" and almost no load delay, so the preload advice a plain review gives would change nothing. TTFB is 53% of LCP: the server is the LCP fix.
  • INP is good in the field, so TBT (140 ms) and the ad script's long task are not on the list.
  • Field CLS (0.31) is above lab CLS (0.22): part of the shift happens after load, where this run can't see it.

Reply

Readers do have a problem, and layout shift is the bigger one: on mobile, CLS at p75 is 0.31 (poor; good is 0.1) and LCP is 3.4 s (needs improvement; good is 2.5 s). Responsiveness is fine (INP 160 ms). Most of the shift comes from the top ad slot, and most of the LCP time is spent waiting for the server.

  1. Reserve space for the top ad. div.ad-slot--top grows when the ad iframe arrives, which is 0.15 of the 0.22 shift in this run. Give the slot a min-height equal to the tallest ad it serves on mobile. Moves CLS.
  2. Give the lead image dimensions. img.article-lead__image (bridge-1200.webp) has no width or height, which is another 0.07 of the shift. Add both attributes, or an aspect-ratio, so the space is held before the image loads. Moves CLS.
  3. Cut the article's server response. The HTML took 1.42 s to arrive, and time to first byte is 1.65 s of the 3.12 s lab LCP. Serve the article HTML from the CDN cache, or find where the backend spends that time. Lighthouse estimates up to 1.3 s off LCP in the lab. Moves LCP and FCP.
  4. Then trim the stylesheet. site.css is 77% unused, about 150 ms of LCP in the lab. Small; do it after the three above.

What this run can't show: field CLS is higher than the run's 0.22, so some shifting happens after load (later ads, scrolling) that a single page-load test doesn't record. RUM that reports which element shifted would name it. The numbers come from one run on an emulated mid-range phone on slow 4G; re-test with the median of three to five runs, then watch RUM p75 over the following weeks.

To confirm: the ad sizes the top slot serves on mobile, which set its min-height.

How it's checked

Measured against plain Claude Code (2026-10-23): it met 59% of its checks, against 46% without it, and a blind judge preferred it. How skills are checked

39 assertions across 5 acceptance evals, and 9 trigger queries. Every skill's checks

What the output must do
  • Opens by saying real mobile users do have a problem: field LCP at p75 is 4.3 s, which is poor against the 2.5 s threshold, and that the 92 is a lab score from one run rather than a Core Web Vitals verdict
  • Names the LCP element as img.collection-hero__image (the raincoats hero image) and identifies that it has loading="lazy", with resource load delay (about 1.19 s, half of the lab LCP) as the largest phase
  • Makes removing loading="lazy" from the hero image and adding fetchpriority="high" the first fix
  • Does not rank 'Serve images in next-gen formats' (est. 2.85 s / 1,240 KiB) as an LCP fix, and says it covers the product-grid images and has no LCP saving
  • Reads field CLS as 0.14 (needs improvement), not 14, notes it is higher than the lab CLS of 0.04 so some shifts happen after load and are not in this run, and names the unsized product-card images or the late web font as the culprits the run does show
  • Does not make TBT, long tasks or the third-party scripts a top priority, because field INP is 168 ms, which is good
  • Does not add the estimated savings of different audits into a total, and mentions that this is a single lab run on an emulated mid-range phone with slow 4G
  • Does not invent metrics, elements, URLs or field numbers that are not in psi.json
  • Answers in the first lines that these runs do not show INP is fixed
  • Points out the two runs are not comparable because after.json was run with a 2x CPU slowdown and before.json with 4x, so part or all of the TBT and score change comes from the settings
  • States that TBT is a lab proxy and not INP, that a load run records no interactions, and that the check is RUM INP p75 after the change against the 200 ms threshold (380 ms was needs improvement)
  • Says each side is a single run and recommends the median of 3 to 5 runs per side with identical settings
  • Confirms ChatterBox no longer appears in the after run's long tasks or third-party list, so the deferral did take it out of the load
  • Names the first-party seatmap.4f2a91.js as the largest remaining long task (620 ms before, 410 ms after at 2x CPU) and the likely next target for INP work
  • Does not invent RUM or field numbers for the period after the change
  • Says that whether the page passes or fails Core Web Vitals cannot be told from a lab run alone, and that field data at p75 (Search Console, CrUX or PageSpeed field data, or RUM) is needed
  • Does not present TBT 1,450 ms as INP; calls it a lab proxy
  • Identifies the LCP element as the CSS background image on div.pricing-hero and resource load delay (4.9 s of 6.8 s) as the dominant phase
  • Recommends making the hero image discoverable early: an <img> in the HTML with fetchpriority="high", or a preload of /img/pricing-hero.jpg with fetchpriority="high"
  • Does not add the 2.9 s and 1.1 s savings into a combined figure, and does not claim the unused-JavaScript saving moves LCP by 2.9 s
  • Mentions that this is one lab run on emulated mobile
  • Does not invent numbers, elements or field data not in the message
  • Stays under 200 words
  • Answers the actual question up front: the change helped but did not fix INP, so the ticket should not be closed. It cites the RUM daily file, where PDP mobile INP p75 drops from roughly 539-580 ms (Oct 6-12) to roughly 459-481 ms (Oct 14-19). That is still above the 200 ms 'good' threshold (in the needs-improvement band).
  • Explains that the Lighthouse score and TBT gains (78 -> 91, 640 -> 290 ms) cannot show INP is fixed, because a lab navigation run has no user interactions and does not measure INP. TBT is only a load-time proxy.
  • Points out that the PSI field data has origin_fallback: true, so the INP p75 of 540 ms is for the whole fernhollow-outfitters.example origin, not this PDP URL. It also notes that CrUX/Search Console use a rolling 28-day window that is mostly from before the Oct 13 deploy, so they cannot confirm or refute the change yet. Lag is only part of the explanation.
  • Names button.add-to-cart as the main remaining INP problem (inp p75 486 ms, 4120 interactions). Its processing time dominates (processing p75 362 ms vs input delay p75 38 ms), and it is attributed to pdp-cart.8c1e.js. The fix offered is to shorten or split that handler, e.g. yield to the main thread, paint the feedback first and defer the non-visual work.
  • Uses the attribution data correctly: input delay on add-to-cart is small, so ChatNest contention is no longer the main cause there. It still flags that the ChatNest launcher interaction is poor (610 ms p75, input delay 290 ms, though only 520 interactions), and that ChatNest was deferred, not removed (820 ms main-thread time and two long tasks after TTI in the lab run).
  • Keeps the focus on INP. It does not lead with LCP, render-blocking CSS or the unused-JavaScript opportunity, and if it mentions them it says they do not bear on the INP question.
  • Gives a concrete way to verify the next fix: watch the RUM INP p75 per day and per target after deploying, and check CrUX/Search Console again once a full 28-day window has passed since the fix. Any lookup that needs the network (PSI/CrUX API, Search Console) is written as a command or step for the user, not reported as a result.
  • Invents nothing. Every number, selector, script URL, entity name and date it cites appears in the three provided files or the prompt, and it does not claim a URL-level CrUX value or post-deploy CrUX figures that the files do not contain.
  • Answers directly at the start: no, the unused-JavaScript project will not get the homepage passing Core Web Vitals. The field data fails only on CLS (p75 0.27, poor, above 0.25), while LCP p75 is 2100 ms and INP p75 is 160 ms, both already 'good'.
  • Explains that the 1.9 s figure is a lab estimate tied to load metrics (the report lists FCP 900 / LCP 1050 metric savings) and does nothing for layout shift. It also notes that Lighthouse's estimated savings overlap (for example with render-blocking-resources' 620 ms on FCP/LCP), so they cannot simply be added up.
  • Names the biggest shift correctly. The element that moves is body > main#content (score 0.091), and it moves because the late TallyConsent cookie banner (cdn.tallyconsent.example/v2/banner.js) is inserted above it; the banner itself is not the shifted element. The fix offered is to render the banner as a fixed or bottom overlay, or to reserve its space up front.
  • Names the second shift: section.hero > h1 (0.038), caused by the web font northwind-sans-bold.woff2 loading. It notes that the passing font-display audit (text visible with swap) does not prevent this shift, and recommends preloading the font and/or matching the fallback font's metrics (size-adjust / ascent-override), or using font-display: optional.
  • Ranks the fixes by CLS impact: cookie banner first, web font second, and the unsized Brightmoor logo image in ul.logo-strip (0.011, also flagged by unsized-images) last as a small width/height or aspect-ratio fix.
  • Notes that lab CLS (0.14) understates the field p75 (0.27), so a good Lighthouse number after the fix does not prove the page passes. Success should be confirmed in field data (CrUX's 28-day window, collection period here Sep 19 - Oct 16, or RUM), which takes up to about four weeks to show.
  • Is sized for forwarding to a VP: a short answer (roughly under 300 words, or a few bullets) in plain language. It is not an audit of every Lighthouse item, and it does not dwell on the 0.68 performance score, which is not what Core Web Vitals assessment uses.
  • Invents nothing. Every metric, selector, URL, vendor name and date it cites appears in lighthouse-home-mobile.json, crux-home-phone.json or the prompt, and it does not give made-up effort estimates or post-fix CLS numbers as if they were measured.
When it should and shouldn't activate

Should activate:

  • Here's our Lighthouse JSON for the product page, why is it slow and what should we fix first?
  • PageSpeed says 92 but Search Console says LCP is poor on mobile. Which one is right?
  • We deferred the analytics scripts and the Lighthouse score went up 12 points. Did that fix INP?
  • Review this PSI result and tell me how to get the homepage passing Core Web Vitals
  • What is the LCP element in this Lighthouse report and why is load delay so high?

Should leave to another skill:

  • Lighthouse flagged contrast and missing labels on the checkout form, can you audit it against WCAG?
  • Our main.js is 1.4 MB, here's the webpack stats file, what's making it so big?
  • Should the blog and product pages be SSR or static with revalidation in Next.js?
  • Set up real-user monitoring and alerts for Core Web Vitals across our site

Hands off to

Related skills

accessibility-audit, bundle-size-review, rendering-strategy

Source

SKILL.md · the whole folder on GitHub.