PageSpeed Sampler
Every stage a site passes through, what it falls back to when a stage fails, and the three points where the pipeline loops back on itself.
An instant read, then six phases. The interface and all of the analysis run in the browser. The only server is a small proxy that fetches sitemaps and, unless you supply your own API key, makes the metered calls to Google on your behalf.
On this page
The six phases below are a page load repeated a few hundred times, and no amount of batching makes a page load instant. Before any of it runs, the tool asks a different question of a different API, and that one is a lookup rather than a load.
The Chrome UX Report API holds 28 days of real Chrome visits per origin. Four calls per site fire together: current and historical, phone and desktop. Every site in the list is queried in the same burst, so the wall clock is one round trip whatever the site count. It returns in about a second.
| What it gives | The Core Web Vitals verdict on real traffic, the full distribution behind each p75, 25 periods of trend, phone and desktop assessed separately, the four LCP phases, whether the LCP element is usually an image, real round-trip time, and the share of navigations served from the back/forward cache. |
|---|---|
| What it cannot give | Which file caused it. Field data is origin-level and has no per-resource detail, so every opportunity, byte saving and platform-specific fix still needs phases 1 to 6. |
| When it is empty | Chrome only publishes where it has enough visits to be statistically sound. No data is a fact about traffic volume, not about speed, and the card says so rather than leaving a gap. |
This phase produces a complete deliverable set of its own, described in phase 6. The full run is offered from the same card and starts at phase 1.
Sites are independent: different hosts, different sitemaps, different proxies. Each gets its own transport context, so a four-site sweep discovers all four together and pays roughly one site's wall clock instead of four. Each site keeps its own block in the log so concurrent output still reads one site at a time.
Up to 4 sites at a time when proxies are in play, 6 when every site allows direct access.
A browser cannot read another site's robots.txt unless that site sends CORS headers, and most do not. Rather than pay a fallback chain on every request, the app probes once per site and remembers the winner.
All six transports are raced against /robots.txt at once rather than tried in turn, so a site behind a slow proxy no longer pays six sequential timeouts before discovery starts. Direct fetch gets a 1.2 second head start and, if a proxy answers first, another 0.8 seconds of grace: direct is faster and it is the only transport that does not hand every audited URL to a third party, so it wins whenever it works at all.
The probe keeps the body. robots.txt is both the probe target and the first thing discovery needs, so fetching it twice was a wasted round trip per site.
Sequential fallback means the slowest proxy sets the wall clock: you wait a full timeout to learn nothing, then start over somewhere else. Instead a second transport is started while the first is still outstanding and whichever answers first wins, so a stalled proxy costs the hedge delay rather than the timeout.
The probe winner starts as the only proxy in the rotation. An alternate joins it by actually answering, and three failures drops it for the rest of that site. Sitemap concurrency then follows how many proxies are genuinely working, at 3 requests per working proxy: spreading load helps only while they all work, and rotating onto one that is stalling just moves the stall around.
Proxy latency and failure rates are scored, shared across every site in the run, and kept in localStorage between runs, so the ordering starts from whatever was actually fast last time instead of relearning it each run.
The public proxies share datacentre address ranges, and a site behind a WAF blocks those ranges wholesale. The proxy connects, the site answers 403, and no amount of retrying changes it. This is the one discovery failure a browser-only tool cannot engineer around, because the block is on the address, not on the request.
A proxy on a known address is the way out, and it is also better for privacy: the audited URLs go to a server this deployment runs rather than to a stranger’s. This build ships one, a Cloudflare Worker, configured in the source rather than typed into a field. It is tried straight after direct fetch and ahead of the public pool.
The same Worker does a second, larger job: it holds the API keys, so a visitor does not need a Google Cloud project to use this at all. That side of it is described in phase 4.
worker/ in the repository. Set CONFIG.apiBase to '' and the hosted allowance turns off, leaving every visitor to bring their own key.* is an open proxy anyone can point at anything, and it will be found.robots.txt is checked first for a Sitemap: directive, which is authoritative when present. If there is none, 22 common paths are probed in parallel, 10 at a time through a proxy and 14 direct:
/sitemap.xml /sitemap_index.xml /sitemap-index.xml /sitemap/sitemap.xml /wp-sitemap.xml /sitemap-0.xml /sitemap1.xml /sitemaps/sitemap.xml /sitemap/index.xml /sitemap.php /sitemap.txt /sitemapindex.xml /page-sitemap.xml /post-sitemap.xml /product-sitemap.xml /sitemap/ /sitemap-pages.xml /sitemap_pages.xml /xmlsitemap.xml /sitemap/sitemap-index.xml /api/sitemap.xml /server-sitemap.xml
Where several hit, an index beats a leaf urlset, then the shallowest path wins.
A <sitemapindex> is walked breadth-first into its children, to a depth of eight. There is no cap on the number of child sitemaps: the wall-clock budget is the only ceiling that ever mattered, and a count cap stopped a walk that still had time left while letting a slow one run on. Plain-text sitemaps are parsed too. Gzipped sitemaps are skipped, because a browser cannot decompress them from fetch.
The parallel pass fails fast at 16 seconds. Any child that timed out is then swept again through different proxies, four at a time, because a batch of timeouts usually means the proxy was saturated rather than the files being absent. Recovered children rejoin the inventory.
A wall-clock budget (90 seconds by default, adjustable) bounds the whole walk, checked per sitemap rather than per depth level so a single index handing back twelve children cannot run past it. Whatever was collected when the budget expires is used.
This runs inside the site's own lane rather than after every site has finished, since nothing downstream of discovery waits on it. The homepage is fetched on the same transport and fingerprinted: 20 platforms, 8 image CDNs, the generator meta tag, and on WordPress the installed plugin and theme slugs read out of asset paths. This is a side input; it changes what phase 5 says, not what gets measured.
Every URL is resolved absolute, filtered to the same host ignoring www, stripped of query and fragment, de-duplicated, and filtered against asset extensions. A 20,000 URL ceiling applies.
The URL measured is the URL the sitemap published, trailing slash and all. The slash-stripped form is the de-duplication key and nothing more. Stripping it from the URL itself meant that on any site whose sitemap ends in slashes, and whose server canonicalises to them, every single page was requested in a form the server 301s away from: an extra round trip on every measurement, TTFB and LCP inflated by it across the whole run, and a redirect report blaming the site for redirects this tool had introduced.
Collection is the first path segment. /blog/x and /blog/y become blog. Root-level one-off pages such as /about and /contact collapse into a single top-level pages group rather than becoming twenty groups of one. The homepage gets its own group.
17 patterns tag pages by role: home, about, contact, services, pricing, products, blog, faq, team, careers, locations, work, reviews, privacy, terms, resources. Roles are what make like-for-like comparison possible across sites that share no URL structure.
Only when more than one site is measured. Two sites rarely name the same thing the same way, so pairings are seeded from exact names plus a 13-cluster synonym table with plural stripping: blog / news / articles / insights, products / shop / store / collections, case-studies / work / portfolio / projects, and so on. A pairing is proposed only when at least two sites match.
Measuring every page of a 4,000-page site is neither affordable nor necessary. The sample is built in a fixed order so that the things a comparison depends on can never be squeezed out by the budget:
| Order | What goes in | Why first |
|---|---|---|
| 1 | One page per detected role | Home against home only works if both were measured |
| 2 | Every paired collection's sample | A pairing with data on one side only is not a comparison |
| 3 | Round-robin across remaining collections | Stops one 5,000-page collection eating the whole budget |
Within a collection the pick is an even, deterministic spread across the sorted list rather than the first N, so a blog sample is not twenty consecutive posts from one week. Caps are 10 per collection and 100 per site, both adjustable. The homepage is always included.
A key in the field wins: the browser calls Google directly, spends the visitor’s own quota, and the backend is not involved. With the field empty, the run claims its whole call count from a daily per-visitor allowance up front, gets a signed short-lived token, and makes every call through the backend, which adds a key it never reveals.
Reserving up front rather than counting each call is what makes the metering affordable, at one write per sweep instead of one per call, and it means a visitor who does not have enough allowance left is told before anything is measured rather than halfway through. When it runs out the run stops having spent nothing and offers the key field as the way to continue now.
The backend also caches results for six hours, keyed without the API key, so every visitor shares one cache. A page someone else already measured costs nothing to measure again, which is worth more to capacity than raising the cap would be.
One job per selected URL per strategy. 100 URLs across mobile and desktop is 200 jobs.
The 240-queries-per-minute quota is not the binding constraint. Each call holds a Lighthouse run on Google's side for 10 to 30 seconds, and one project's share of that pool is far narrower than 240. Firing 200 at once saturates it and the overflow returns 500 ERRORED_DOCUMENT_REQUEST, not 429.
So concurrency is discovered rather than declared:
localStorage, rather than relearning the same number from 40 every time.429 pauses the whole batch 7 seconds; a cluster of 5xx pauses it 3.NO_FCP or an invalid URL. Anything still failing is grouped by Google's own error text with a manual retry button.Each response yields far more than seven numbers. A run whose runtimeError is set is discarded here rather than averaged in, because Lighthouse documents that field as meaning the result may need to be thrown away:
| Lab metrics | Performance score, LCP, CLS, TBT, FCP, TTFB, Speed Index, and accessibility, best practices and SEO when enabled |
|---|---|
| Field data | CrUX at both page and origin level, with the p75 and the distribution across good, needs improvement and poor. Page-level exists only for URLs with enough traffic; origin-level exists for almost any site with real traffic, and the two are never averaged together. |
| Opportunities | 24 audits that name a cause, each with the specific files responsible and their bytes and milliseconds |
| Diagnostics | The LCP element's selector and snippet, layout-shift elements, third-party entities and their blocking time |
| Stack packs | Lighthouse's own per-platform advice, keyed by audit id |
| SEO and quality | 12 SEO checks and 9 best-practice checks, when those categories are enabled. SEO is on by default and costs nothing extra per call. |
| Composition | Transferred bytes and request counts by resource type, plus request totals, round-trip time and server latency |
| Validity | runtimeError, runWarnings and captchaResult |
| Provenance | Requested URL against final document URL, form factor, throttling method, run duration |
Findings are rolled up so that one slow asset on forty pages is one finding, not forty. Figures are reported per page throughout: Lighthouse savings summed across a sample produce numbers like "90 seconds wasted", which is not time anyone can save. A resource's size is its own weight, not that weight multiplied by the page count. The across-sample totals survive only as the ranking signal.
17 rules decide what counts as a finding, which is a different job from listing every audit that returned data. Each declares a condition, a severity, a fix risk, an effort level, an evidence set and a fix. Two of them combine signals a spreadsheet row cannot: a collection performing well below the site mean, and a competitor beating the subject.
Rules fire in declaration order and sort by severity. Nothing is written by a language model. Every sentence is a template filled from the same numbers its evidence table shows.
Per finding, per site, resolution runs in a fixed order: the curated playbook first, then Lighthouse's own stack pack, then nothing. It never invents advice for a platform it has no entry for, and Lighthouse-sourced text is labelled.
The playbook covers 14 platforms by audit id, and 18 plugin slugs carry their own note. If ShortPixel is already installed, the image finding is a bulk run in a plugin they own rather than a purchase.
Two sets, produced at two different times from two different sources. The instant set lands about a second after the key is entered and answers whether there is a problem and what shape it is. The full set lands minutes later and answers which file causes it and what the fix is worth.
| Card | One panel per site: the verdict, the distribution behind each p75, the desktop line, the LCP anatomy, the back/forward cache share and a trend sparkline. |
|---|---|
| CSV | One row per site per form factor, 36 columns: p75, good share and poor share for all six metrics, the four LCP subparts in milliseconds and as shares, the LCP resource type and the bfcache share. |
| JSON | The same, nested, with the thresholds and the full trend series. |
| Report | Print-ready document in six sections: method, the verdict, what LCP is made of, direction, findings, and what comes next. |
Six outputs, all built from the same model so none of them can drift from the others:
| App | An operator console. Run validity, comparison, headline figures, real-user data and ranked opportunities stay on screen; the reference material collapses behind disclosures that carry their own headline number. The reports are where the full detail belongs. |
|---|---|
| CSV | Flat rows, one per measurement. |
| JSON | Nested per site, per collection, per role, with the pairings recorded. |
| Excel | 14 tabs in the house palette, with the charts rasterised in. |
| Google Sheet | Created live in your Drive under the drive.file scope, with native charts. |
| Report | Print-ready document: method, stack, findings, recommendations with severity and fix risk, roadmap, rubric, and an appendix listing every rule that was evaluated including those that stayed silent. |
fetch.