← All docsGet started

Reading a report

What the score, grades, findings, severities, and the request waterfall actually mean.

The score#

The 0–100 score is Harpoon's own transparent metric — deliberately not a Lighthouse score. Five dimensions are each scored 0–100 from concrete signals in your capture, then combined with fixed weights. The gauge on every report shows the exact breakdown, so the score is never a black box:

DimensionWeightWhat moves it
Render-blocking JS/CSS30%How late render-blocking resources finish, plus how many there are.
Critical path length20%Dependency-chain depth and the absolute page-load span.
Third-party overhead20%Third parties' share of bytes and their total request time.
Cache quality15%Bytes of static assets served without adequate caching.
Asset efficiency15%Bytes recoverable via compression, de-duplication, or right-sizing.

Grades band the overall score: A ≥ 90, B ≥ 75, C ≥ 60, D ≥ 40, F below that.

Domains you own but don't serve the page from#

"Third party" means a different registrable domain from the page’s own — so a CDN, an asset host or an API on its own domain counts as third-party overhead by default, and the third-party dimension above marks you down for infrastructure you control.

Tell Harpoon which domains are yours in Settings → Internal domains, or straight from a report: open the Heaviest origins tab and choose This one’s ours on the origin. Subdomains come with it — listing example.com covers assets.example.com.

This one genuinely moves the score, so it applies from your next analysis onward. Reports you have already run keep the score they were measured with: the raw capture is deleted once its analysis is saved, and silently restating an old score would break every trend that run appears in.

Findings & severities#

Findings are discovered by deterministic rules — one per performance concern. Each carries a severity (critical, high, medium, low, info), the concrete evidence (the requests, bytes, and milliseconds involved), and its measured impact. The default trigger thresholds:

RuleFlags (defaults)
Render-blockingScripts/styles that delay first paint, individually and in aggregate.
Cache qualityCacheable static assets with no policy, or max-age under 1 day.
CompressionCompressible text assets over ~1.4 KB served uncompressed.
Large assetsScripts > 300 KB, stylesheets > 100 KB, images > 500 KB, fonts > 200 KB, anything else > 1 MB.
Slow requestsAny request taking over 1 s.
Redirect chainsTwo or more redirects in a row.
Third-party overheadThird parties over 40% of transfer or 1.5 s of blocking time.
Duplicate assetsThe same asset fetched more than once.
Critical chainsRequest dependency chains 3+ levels deep (needs initiator data in the capture).
Waterfall gapsIdle gaps of 300 ms+ on the critical timeline.
These are the trigger points the engine ships with; treat them as representative rather than contractual. The evidence attached to each finding always shows the actual measured values for your capture.

Expand any finding to see every request behind it — not a sample — with its size and, where the capture fetched the same URL more than once, a ×2 marker. Counts are per asset: a multi-step capture that navigates twice, or an app that re-mounts, fetches some files repeatedly, and one file fetched three times is still one thing to fix. The bytes are not de-duplicated, because each of those fetches really was downloaded.

The plan, and how fixes are ranked#

Every finding also appears in the report's plan as something you can decide about. Rows are ranked by severity first, then by what they are worth to the score — severity leads because a serious finding in a category the score does not measure (a slow request, say) would otherwise sink below a minor one that happens to carry score headroom.

Ticking rows projects where the score could land. That figure is derived, not guessed: because the overall score is the weighted sum of the five dimensions above, taking one dimension from its current sub-score to a perfect 100 is worth exactly weight × (100 − sub-score) overall. Each dimension's headroom is split evenly across the queued rows that feed it.

It is a ceiling, which is why the report says "as high as" rather than promising a number. It assumes each fix fully resolves its dimension; a partial fix recovers less. Fixes in categories that feed no dimension — slow requests, redirect chains, waterfall gaps — are still worth shipping, but move this score by nothing and say so. The queue is a scratchpad and is not saved.

Hiding a finding you've accepted#

Some findings are a decision you have already made — a vendor script you are not dropping, a cache header you cannot set. Expand the finding in the report's Findings tab and choose Hide this finding. It leaves the findings list, the plan, your ticket drafts, repo fix runs, exports and any link you have shared.

A hide is stored against the page, not the run, and matched on the finding's id — which the engine derives deterministically from the rule and its subject. That is what makes it stick: tomorrow's scan re-detects the same issue, recognises the same id, and keeps it hidden. It applies to every run of that page, including a HAR you upload by hand.

Hiding never moves a number. The score, the grade, the trend history and your regression alerts are all still computed from every finding the engine produced, so nobody can improve a report by tidying it. To bring something back, open Hidden on this page at the foot of the Findings tab and choose Restore. The AI write-up is written when a report is generated: hide something afterwards and it can still be mentioned there until you press Regenerate.

The request waterfall#

The waterfall lists every request with a phase-colored timing bar: blocked (queueing), dns, connect (incl. TLS), send, wait (time to first byte), and receive. A long wait is your server thinking; a long receive is a heavy response; lots of blocked means connection contention.

Sort by size, time, status, or type; filter by URL text or resource type. The table is virtualized, so captures with thousands of requests stay smooth.

Same capture, same report#

The engine is deterministic: identical capture input produces identical findings, values, and finding ids, every time. That's what makes comparisons and trends trustworthy — a change in the report always reflects a change in the capture, never noise from the tool. The AI report is a separate, clearly-marked layer on top.

Still stuck? Contact us or browse all guides.