# The State of UK Trades Websites 2026 — dataset and methodology

Part of *The Great British Trade-Off 2026*, published by Websites for Tradespeople.

**Licence.** CC BY 4.0 covers the report, its analysis, its charts and the statistics derived from
the data. The row-level file holds only measurements we made ourselves. Each website's town and
trade originate in the source directory's listings rather than in anything we measured, and we
have not established that we may redistribute them, so those two columns are withheld from the
published file and no licence is asserted over the file itself. Every figure in this report is
computed from the complete internal dataset, so none of them is affected.

**Please cite as:** Websites for Tradespeople (2026), "The State of UK Trades Websites 2026", https://www.websitesfortradespeople.co.uk/research/state-of-uk-trades-websites-2026

---

## What this dataset is

Lighthouse audits of **43,738 UK trades business websites**, collected between
2026-07-30 and 2026-09-01 through Google's
PageSpeed Insights API. Each website was requested twice, once with the mobile strategy and
once with the desktop strategy. **31,030 websites returned a result** on at least one of
the two.

Websites the API itself reported it could not fetch or render were set aside before this
dataset was exported and are not in it. That exclusion, and the settings each audit ran under,
are recorded below under *How the audits were run* — including which of those settings were
kept at collection time and which were not.

The file `state-of-uk-trades-websites-2026.csv` contains 43,737 rows and
30 columns — one row per website.

### Three columns are withheld from this file

The published file holds only measurements we made ourselves. Each website's **town** and its
**trade** — both as the directory recorded it and as normalised — originate in the source business
directory's listings rather than in anything we measured, and no review of that source's terms has
been carried out by this project. Publishing them would be an assertion about material that is not
wholly ours, made by a party that has not established it may make it, so the three columns are
left out.

Be clear about what this does and does not cost. It changes **no figure in the report**: every
statistic, including the whole of the trades chapter and the cities chapter, is computed from the
complete internal dataset, which retains all three columns. What it costs is reproducibility of
those two chapters *from this file specifically* — the per-trade and per-city aggregates are
published in full as JSON artefacts instead, with the sample size behind each one. Everything else
in the report can be recomputed from the file as it stands.

If the source's terms are later established to permit redistribution, the same pipeline emits the
complete file with no other change, and the checksum published beside it will change accordingly.

That is 1 fewer than the 43,738 websites the study attempted. The difference is a row whose town
and trade were never recorded, which left it alone in a group of one under the disclosure rule
below. It is in the analysis and in every figure here; it is left out of the downloadable file
only.

## What it is not

**These are laboratory measurements.** Every figure comes from a single automated audit of one
page, at one moment, under device and network conditions Google applied and did not report back
to us. It describes how that page behaved in that test. It is not a measurement of what
visitors experienced, and the reference thresholds used are aids to reading a lab result — not
an assessment against any third-party programme, and not a claim about search rankings. The
audits ran on Google's own servers; Google publishes that its service runs in a data centre
that can vary, and reports one of North America, Europe or Asia. The dataset does not record
which ran any audit.

**It cannot show cause.** Nothing here was assigned. Every business chose its own platform, its
own trade and its own town, and the audit measured what was already there. There is no control
group and no intervention, so a gap between two groups is a difference between the sites that
sit in them — not a measure of what a platform did to a site. Businesses that choose one
platform may differ from those that choose another in ways this dataset did not observe. Read
every comparison here as a difference between groups.

**It is not a representative sample of the UK.** Sampling was seeded on a set of major cities,
which contribute several hundred sites each, with a long tail of smaller places. The dataset is
large, but it is not random or weighted, and no figure should be read as a national estimate.

**An audit failure is not a broken website.** Where a row records an error, the collector's own
request to the audit API did not complete — it timed out, could not connect, or came back as a
server error whose cause was not recorded. A website can be temporarily unreachable or refuse
automated traffic, and a request can fail for reasons that have nothing to do with the website
at all. The failure rates in this dataset describe those request failures.

## How the audits were run

The collector stored only the four category scores and six metric values from each PageSpeed
Insights response, so most of the Lighthouse configuration that produced this dataset was never
kept. Each condition below is marked retained, partly retained or not retained. Where a condition
was not retained, what Google publishes about its service is given separately, with the page and
the date it was read; that is what the provider says its service does, not an observation of these
audits. Nothing is inferred to fill a gap.

| Condition | Status |
| --- | --- |
| Audit provider and request | Retained |
| Lighthouse version | Not retained |
| Form factor and device emulation | Partly retained |
| Throttling method | Not retained |
| CPU slowdown | Not retained |
| Network conditions | Not retained |
| Where the audit ran | Not retained |
| Runs per website | Retained |
| Redirects and the audited URL | Partly retained |
| Collection dates and timezone | Partly retained |
| What an error value means | Retained |
| Platform detection | Partly retained |
| Where the website list came from | Partly retained |
| How the dataset left the collector | Retained |

**Audit provider and request — Retained.**

Every audit was one request to Google's PageSpeed Insights API (version 5, runPagespeed) asking
for the Performance, Accessibility, Best Practices and SEO categories, with a 90-second timeout on
the collector's side. The collector kept only the four category scores and six metric values
(largest contentful paint, cumulative layout shift, total blocking time, first contentful paint,
speed index, time to interactive) from each response. Apart from the HTTP status of a failed
response, and on a 400 the first 80 characters of the API's own error message — both kept as the
row's error value — no other part of the response was stored.

*Evidence.* Collector scraper.py: PSI_ENDPOINT (line 492); psi_audit_url builds params url,
strategy, four category values and key, requests timeout=90 (495-526); the parser reads only
categories[*].score and six audits' numericValue (552-587). Collector column list run_psi_audit.py
41-60 equals the master's 32 columns.

- PageSpeed Insights API v5 reference, runPagespeed (Google) — <https://developers.google.com/speed/docs/insights/rest/v5/pagespeedapi/runpagespeed>, page dated 2024-09-03, read 2026-09-02

**Lighthouse version — Not retained.**

The Lighthouse version that produced each audit was not retained: the collector discarded that
field of every response. Google's release notes for the service record an update to Lighthouse
13.0 on 20 October 2025 and carry no later entry (page last updated 27 October 2025, read 2
September 2026) — but those notes do not record every point release, and the check run on 2
September 2026 came back as Lighthouse 13.4.1, a version the notes never mention. So the audits
ran on some 13.x release and nothing recorded which. Neither the release notes nor the September
check is an observation of the July to September audits, and a Lighthouse point release can change
how a score is calculated.

*Evidence.* lighthouseVersion is never read (scraper.py 552-587) or stored (run_psi_audit.py
41-60); absent from the master CSV and from every retained log.

- PageSpeed Insights release notes (Google) — <https://developers.google.com/speed/docs/insights/release_notes>, page dated 2025-10-27, read 2026-09-02

**Form factor and device emulation — Partly retained.**

Each website was requested once with strategy=mobile and once with strategy=desktop, and the
mobile and desktop columns in the dataset are those two requests. Which emulated device and screen
Google applied under each strategy was not retained. Google describes the mobile test as 'a
mid-tier device (Moto G4) device on a mobile network' and the desktop test as 'an emulated-desktop
with a wired connection'. The Lighthouse 13.4.1 source sets the mobile screen at 412 by 823 pixels
at 1.75x scale and the desktop screen at 1350 by 940 at 1x. The check run on 2 September 2026
returned mobile screenshots 412 pixels wide and desktop screenshots 1,335 to 1,350 pixels wide,
and a 'moto g power (2022)' Chrome user agent for mobile. That is what the runner returns now, not
an observation of the July to September audits.

*Evidence.* Retained: psi_audit_mobile_and_desktop calls psi_audit_url with 'mobile' then
'desktop' (scraper.py 589-592); paired Mobile*/Desktop* columns (run_psi_audit.py 41-60). Not
retained: configSettings.formFactor/emulatedFormFactor never parsed or stored. Corroboration files
w1-*/w2-* in collection-evidence/corroboration/: configSettings.formFactor,
fullPageScreenshot.screenshot.width, environment.networkUserAgent.

- About PageSpeed Insights (Google) — <https://developers.google.com/speed/docs/insights/v5/about>, page dated 2024-10-21, read 2026-09-02
- Lighthouse v13.4.1 source: core/config/constants.js (screen emulation, user agents, default throttling method) — <https://github.com/GoogleChrome/lighthouse/blob/v13.4.1/core/config/constants.js>, read 2026-09-02
- PageSpeed Insights API v5 reference, runPagespeed (Google) — <https://developers.google.com/speed/docs/insights/rest/v5/pagespeedapi/runpagespeed>, page dated 2024-09-03, read 2026-09-02

**Throttling method — Not retained.**

Not retained, and not returned by the API: the configSettings block of a PageSpeed Insights
response carries only the form factor, locale, channel and category list, with no throttling
settings at all, so a collector that kept every response in full would still not hold this value.
Google describes the service as analysing pages 'in a simulated environment'; Lighthouse's default
throttling method is 'simulate', and its documentation says that setup 'matches the setup of
PageSpeed Insights'. If that is what ran here, the timing figures are model estimates for a target
connection rather than load times measured over a real one — but that is the provider describing
its service, and no throttling setting was recorded for any audit in this dataset.

*Evidence.* configSettings.throttlingMethod never parsed or stored (scraper.py 552-587;
run_psi_audit.py 41-60). The four corroboration responses of 2 September 2026 contain no
'throttling', 'throttlingMethod', 'cpuSlowdown' or 'screenEmulation' key anywhere; the API
reference documents ConfigSettings as onlyCategories, emulatedFormFactor, locale, channel,
formFactor.

- PageSpeed Insights API v5 reference, runPagespeed (Google) — <https://developers.google.com/speed/docs/insights/rest/v5/pagespeedapi/runpagespeed>, page dated 2024-09-03, read 2026-09-02
- About PageSpeed Insights (Google) — <https://developers.google.com/speed/docs/insights/v5/about>, page dated 2024-10-21, read 2026-09-02
- Lighthouse v13.4.1 source: core/config/constants.js (screen emulation, user agents, default throttling method) — <https://github.com/GoogleChrome/lighthouse/blob/v13.4.1/core/config/constants.js>, read 2026-09-02
- Lighthouse v13.4.1 documentation: docs/throttling.md — <https://github.com/GoogleChrome/lighthouse/blob/v13.4.1/docs/throttling.md>, read 2026-09-02

**CPU slowdown — Not retained.**

Not retained, and not returned by the API. Google's release notes record that on 5 December 2024
'the CPU throttling factor for PageSpeed Insights has been adjusted to account for the low CPU
performance benchmarks typical in PageSpeed Insights production environments'. The Lighthouse
13.4.1 preset for the hosted mobile service sets a 1.2x CPU slowdown multiplier; the hosted
desktop preset selects the 'desktopDense4G' profile, whose multiplier is 1x, meaning no CPU
slowdown. These are the provider's published settings, not values observed in this collection. The
generic Lighthouse command-line default of 4x is a different setting and does not describe the
hosted service.

*Evidence.* configSettings.throttling.cpuSlowdownMultiplier never stored; absent from the API's
ConfigSettings (see throttling method). Provider: lr-mobile-config.js cpuSlowdownMultiplier: 1.2
with the comment 'Determined using PSI CPU benchmark median'; lr-desktop-config.js sets no
cpuSlowdownMultiplier.

- PageSpeed Insights release notes (Google) — <https://developers.google.com/speed/docs/insights/release_notes>, page dated 2025-10-27, read 2026-09-02
- Lighthouse v13.4.1 source: core/config/lr-mobile-config.js (the hosted-service mobile preset) — <https://github.com/GoogleChrome/lighthouse/blob/v13.4.1/core/config/lr-mobile-config.js>, read 2026-09-02
- Lighthouse v13.4.1 source: core/config/lr-desktop-config.js (the hosted-service desktop preset) — <https://github.com/GoogleChrome/lighthouse/blob/v13.4.1/core/config/lr-desktop-config.js>, read 2026-09-02
- Lighthouse v13.4.1 documentation: docs/throttling.md — <https://github.com/GoogleChrome/lighthouse/blob/v13.4.1/docs/throttling.md>, read 2026-09-02

**Network conditions — Not retained.**

Not retained, and not returned by the API. Lighthouse's documentation gives its simulated mobile
network throttling as 150 ms latency and 1.6 Mbps down / 750 Kbps up with no packet loss;
separately, and about the throttling method rather than those numbers, it says simulated
throttling 'matches the setup of PageSpeed Insights'. The hosted desktop preset names a
'desktopDense4G' profile. These are the provider's published settings for its own tooling, not
values observed in this collection.

*Evidence.* configSettings.throttling.rttMs/throughputKbps never stored; absent from the API's
ConfigSettings. Provider: docs/throttling.md ('Latency: 150ms, Throughput: 1.6Mbps down / 750 Kbps
up. Packet loss: none.'); lr-desktop-config.js throttling: constants.throttling.desktopDense4G.

- Lighthouse v13.4.1 documentation: docs/throttling.md — <https://github.com/GoogleChrome/lighthouse/blob/v13.4.1/docs/throttling.md>, read 2026-09-02
- Lighthouse v13.4.1 source: core/config/lr-desktop-config.js (the hosted-service desktop preset) — <https://github.com/GoogleChrome/lighthouse/blob/v13.4.1/core/config/lr-desktop-config.js>, read 2026-09-02

**Where the audit ran — Not retained.**

Where Google ran each audit was not recorded. Google states that the test 'runs in a Google
datacenter that can vary based on network conditions' and reports 'one of: North America, Europe,
or Asia', so audits in this dataset may have run from different regions. The collector that sent
the requests was run by the publisher from one machine in the UK; that is the operator's account
of the run, not a value stored with the data.

*Evidence.* environment and analysisUTCTimestamp never stored; no region field in any retained log
(psi_day1, psi_resume, psi_resume2, psi_tail, psi_final). The corroboration responses' environment
block carries only hostUserAgent, networkUserAgent, benchmarkIndex and credits.

- About PageSpeed Insights (Google) — <https://developers.google.com/speed/docs/insights/v5/about>, page dated 2024-10-21, read 2026-09-02

**Runs per website — Retained.**

One request per strategy per run, with no automatic retry. Where the API answered with a
rate-limit response the row was dropped and the website was requested again in a later run, so
some websites were requested more than once; five websites whose listed address carried a stray
space after the scheme separator also slipped past the runner's 'already audited' check and were
audited again on later runs. Where a domain has more than one row, the pipeline keeps the most
recent (27 rows removed across 5 domains). Every figure in the report therefore rests on a single
Lighthouse run per device, with the run-to-run variation a single run carries.

*Evidence.* scraper.py 589-592 (one mobile call, one desktop call, no retry loop);
run_psi_audit.py 110-125 (skip set from 'PSI Audit' and 'PSI Audit - Broken'), 282-294
(rate_limited rows not written, run bails after 50 consecutive), 64-79 against 114 (an address
with a space after the scheme separator escapes the skip set: 24 of the 31 repeat rows are
'http:// ' and 7 are 'https:// '); pipeline clean.mjs 237 keeps the most recent row per domain.

**Redirects and the audited URL — Partly retained.**

The address sent to the API was the website address as listed in the collector's source sheet,
trimmed of whitespace. Google follows redirects and audits the page it arrives at; the final URL
was not retained, so a row may describe a page other than the listed address, including a
different host. The check run on 2 September 2026 showed this for one of the two websites checked:
the API reported a redirect to a different host and a warning about it, neither of which the
collector would have stored. The scheme in a listed address (http or https) is the source sheet's,
not the scheme the page was finally served on, which is why the pipeline drops the listed address
entirely.

*Evidence.* Retained: url param passed as listed (scraper.py 495-526; run_psi_audit.py 180-194,
260). Not retained: requestedUrl/finalUrl/runWarnings never parsed or stored. Corroboration w1-*:
finalUrl host differs from requestedUrl host, runWarnings length 1.

- PageSpeed Insights API v5 reference, runPagespeed (Google) — <https://developers.google.com/speed/docs/insights/rest/v5/pagespeedapi/runpagespeed>, page dated 2024-09-03, read 2026-09-02

**Collection dates and timezone — Partly retained.**

Each row's 'Audited At' is the collector's local clock when the audit finished, stored without a
timezone. The machine kept Europe/London time, and cross-checks against the collector's
UTC-stamped session record place it at BST (UTC+01:00) throughout: the five retained run logs fix
that offset at five points inside wave 2, and wave 1 rests on the session record alone, because no
wave-1 run log survives. That is an inference from those checks, not a value in the data. Rows
fall on seven local days: a 7-row test on 30 July 2026, wave 1 on 3 to 5 August (17,237 rows
including the test), and wave 2 on 30 August to 1 September (15,491 rows). The pipeline emits
every timestamp with an explicit +01:00 offset; nothing is converted.

*Evidence.* run_psi_audit.py 335 and app.py 1181: time.strftime('%Y-%m-%d %H:%M:%S'), naive local
time. Rows per local day: 30 Jul 7; 3 Aug 6,938; 4 Aug 10,176; 5 Aug 116; 30 Aug 8,253; 31 Aug
3,137; 1 Sep 4,101 (audited sheet), plus the set-aside rows on the same days. The five retained
run logs are all wave 2 — psi_day1 (30 Aug 13:58), psi_resume (21:14), psi_resume2 (21:52),
psi_tail (31 Aug 09:27), psi_final (1 Sep 15:36) — checked against UTC-stamped session records.
Wave 1's run log was not retained; its offset rests on timestamp fragments in the session record.
The 30 July rows' code path is not recorded.

**What an error value means — Retained.**

An error value describes the collector's request to the API, not the website. 'Timed out' and
'connection error' mean the collector's own call to the API did not complete within 90 seconds or
could not connect; the connection errors cluster in two short bursts, on 3 August 20:35 to 20:36
and on 30 August 15:05 to 15:09 and 15:24 to 15:25 (BST). 'HTTP 500' means the API answered with a
server error whose body was not kept, so nothing is known about its cause. Where the API itself
reported that it could not fetch, render or parse the page (its 400-class responses, labelled
FAILED_DOCUMENT_REQUEST, NO_FCP or NOT_HTML), the collector wrote the row to a separate sheet:
11,037 of the 43,765 rows written across the two sheets (25.2%), 341 of which recorded no error on
the other device and 331 of which carry a Performance score. Those rows are part of this dataset.
An earlier version of it held only the audited sheet, which understated the failure rate and
narrowed the population every share described; no script records how or when the two sheets were
separated.

*Evidence.* scraper.py 509-550 error labels (timeout, connection_error, request:<Exc>,
rate_limited, '400:<message>', http_<code>); 544-545 discards the 500 body. Master error cells:
connection_error 3,955, http_500 470, timeout 204, request:ChunkedEncodingError 1; no '400:' value
remains. Read-only aggregate check of the collector's 'PSI Audit - Broken' sheet on 2 September
2026: 11,037 rows, per-day 3 Aug 2,369; 4 Aug 3,851; 5 Aug 74; 30 Aug 2,280; 31 Aug 1,016; 1 Sep
1,447; 341 rows with one scored device. The 7 August snapshot of the 'PSI Audit' sheet (23,531
rows, 29 columns) still holds wave 1's 400-class rows and reconciles the move. No script writes
that sheet.

**Platform detection — Partly retained.**

Platform detection (CMS and page builder) was a separate fetch of each homepage by the collector,
with a 15-second timeout, redirects followed and the first 1 MB read. It was not part of the
Lighthouse audit. For wave 2 it ran immediately before each website's audit; for wave 1 and the 30
July test it was back-filled in one session on 28 August 2026 (21:17 to 22:21 BST), 23 to 29 days
after those audits. No per-row flag records which; rows audited before 28 August are the
back-filled ones.

*Evidence.* tech_detect.py 563-597 (requests.get, timeout 15, allow_redirects, 1 MB cap);
run_psi_audit.py 253 and 260 (detect before the audit in run_one); retained logs tech_backfill.log
(21:17 to 22:04 BST, 8,182 processed) and tech_backfill_v2.log (22:05 to 22:21 BST, 15,856
candidates).

**Where the website list came from — Partly retained.**

Websites were found through business listings on a UK online business directory, searched by the
same tool. The tool's completion list records 756 trade-and-city pairs (35 trade categories in 16
cities, plus extended plumber and decorator searches), but it is a completion marker rather than a
search log, so 756 is a floor: the data itself carries 848 distinct trade-and-city combinations.
Each listing's website field was reduced to a bare domain and de-duplicated. The date each listing
was collected and its position in the directory's results were not retained.

*Evidence.* scraper.py listing scrape (784, 874, 634); the tool's done-list of 756 unique
service-and-city pairs; scripts/build_websites_tab.py rebuilt the source sheet on 30 July, 3
August, 30 August and 1 September 2026 (18,565 / 23,528 / 38,196 / 43,738 domains); every master
row carries Source Tab 'Websites'.

**How the dataset left the collector — Retained.**

The dataset file is byte-identical to a CSV export of the collector's 'PSI Audit' sheet downloaded
on 1 September 2026 at 18:58 BST (7,618,420 bytes, SHA-256 beginning c2441e07), and its 32 columns
match the collector's column list exactly. The export wrote 5,510 timestamps with an unpadded
hour, which the pipeline normalises. The 400-class rows were held in a second sheet, exported on 2
September 2026 (11,037 rows, SHA-256 beginning 4ecbf5aa); the two together are the dataset this
study analyses. No script records how the two sheets came to be separated.

*Evidence.* data/research/raw/SHA256SUMS line 9; the downloaded export's timestamp and origin
metadata; 32 columns equal run_psi_audit.py 41-60; CRLF, no BOM. Earlier snapshot of the same
sheet from 7 August 2026 (23,531 rows, 29 columns) retained under collection-evidence/snapshots/,
together with the 2 September 2026 export of the set-aside sheet whose wave-1 rows it reconciles
exactly.

**Corroboration run, 2026-09-02.**

The collector's unchanged request code was run once more for one website from each wave, chosen as
the smallest site identifier among websites scored on both devices in that wave, and this time the
full API response was retained. Scores from the check were not compared with the original audits
and are not used anywhere in this report.

The request code is byte-identical between the collector's only commit (31 July 2026 — the day
after the 7-row test of 30 July, and before the 3 to 5 August bulk of wave 1) and its working copy
on 2 September 2026. No intermediate version is retained, so continuity in between rests on those
two points and on the session record. That record shows the runner script being written and
repeatedly edited during collection, including on 28 August 2026 to add the platform-detection
fetch and on 30 August 2026 to add a skip list; none of the recorded edits touches the request
code, but a session record can only evidence edits made inside a recorded session, and the runner
is untracked, so no version of it survives from the collection window itself. The check shows what
the runner does now; it is not evidence of the conditions that produced the dataset.

What the check returned:

- Lighthouse 13.4.1 on all four responses.
- configSettings: formFactor and emulatedFormFactor 'mobile' or 'desktop' matching the strategy sent; locale en-US; channel 'lr'; onlyCategories performance, accessibility, best-practices, seo.
- No throttling settings, CPU multiplier, throttling method or screen-emulation block anywhere in the responses.
- Host user agent HeadlessChrome 151 on Linux x86_64; network user agent 'moto g power (2022)' Chrome 151 Mobile for mobile and Macintosh Chrome 151 for desktop.
- Full-page screenshots 412 pixels wide for mobile; 1,335 and 1,350 pixels wide for desktop.
- The unchanged parser produced all four category scores and six metric values for both devices of both websites.
- One website's listed address redirected to a different host; the API reported the final URL and a warning, which the collector would not have stored.
- The environment block carries no region or location.

Full responses under data/research/raw/collection-evidence/corroboration/ (not published: they
name the websites).

The collection evidence behind the lines above — the collector source, the run logs and the
spreadsheet snapshots — names real businesses, so it is held privately rather than published.
It is available to anyone checking a specific claim.

## Privacy

Every website in this dataset is publicly accessible, and its audit scores are facts anyone could
measure. The published file still carries no identifying field, and we have taken these steps to
make working back to a specific business hard:

- No domain, no URL and no business name is included.
- Each row carries a `Site ID` derived from the domain with HMAC-SHA256 under a private key we
  hold and do not publish. The key is specific to this edition, so an id here cannot be matched
  against an id published in a different study.
- Rows are ordered by that identifier, so the file's ordering reveals nothing either.
- Every published city-and-trade combination describes at least 5 websites.
  Where one described fewer, a value was withheld for those rows only —
  77 service value(s);
  60 city value(s);
  137 in total — and marked `(withheld)`
  rather than left blank so it stays distinguishable from a value that was never recorded.
  This is small-cell suppression on that one combination — not a formal k-anonymity guarantee.
- Timestamps are reduced to a date. Dates are the collection machine's own Europe/London
  local date; the study does not convert them.

None of this makes re-identification impossible, and we do not claim it does. Every site here is
publicly reachable, so anyone willing to re-run the same audit could match a row to a domain.
What these steps do is remove the shortcut: the file itself does not hand anyone a name.

If you believe a row describes your business and you would like it removed, contact us and we
will remove it from the next revision.

## How the numbers were produced

### Trade and town labels

The trade and town attached to each website are the source directory's own labels. Two things are
done to them and nothing else. Both are lowercased so that grouping is not split by
capitalisation. And one trade spelling variant is mapped onto another: `roofer` is counted as
`roofers`, which affects 116 of the 43,738 websites attempted.

No other mapping exists and no categories are combined. Carpenters and joiners are separate labels
here, as are gardeners and landscape gardeners — merging them would be an editorial judgement
about the trades, not a correction to the data. Towns are not merged either: `newcastle` and
`newcastle upon tyne` were seeded separately and are reported separately.

- **27 duplicate rows** were removed, keeping the most recent audit for
  each domain.
- **Medians, not means**, for timing metrics — the distributions have long right tails.
- **Sample size is published alongside every figure.** Small cohorts are reported but never ranked
  against large ones: below 100 sites no median is shown at all, and below
  300 a cohort is never placed in a league table.
- **Quantiles use the R type 7 definition** — linear interpolation between the two nearest
  order statistics — pinned to that one method so a published figure cannot move because a
  different convention was applied.
- **A controlled comparison needs 100 sites in each platform-and-trade cell**, and each
  median must clear that threshold on its own metric basis rather than on the cell's row count.
- **Nothing is silently dropped.** Sites where no platform could be detected, and sites with no
  trade label, are reported as their own cohorts.
- **Every headline was re-tested inside each collection wave**, and any figure that did not hold
  across both was not published as a headline.

## Why these figures differ from field-data studies

<!-- research-claims-ok:begin explains the laboratory/field distinction the gate enforces -->

- Other public studies of website speed report Largest Contentful Paint from Google's Chrome User
Experience Report, and often report figures a good deal faster than the ones here. The two are not
competing measurements of the same thing, and neither is a correction of the other.

- This study is laboratory data. Every figure comes from one automated Lighthouse test of one
listed URL, run twice through the PageSpeed Insights API — once with its mobile strategy and once
with its desktop strategy — under conditions the service applied and did not report back to us.

- The Chrome User Experience Report is field data. Google describes it as “a dataset that reflects
how real-world Chrome users experience popular destinations on the web”, collected from real
browsers according to browser settings that determine which users are eligible.

- They also need not describe the same websites. Google states that not all origins or pages are
represented in that dataset, and that the eligibility criteria require a page to be publicly
discoverable and to have “a large enough number of visitors in order to create a statistically
significant dataset”; the exact threshold is not published. This study is a long tail of local
trades businesses, and it made no attempt to establish which of them meet that bar.

- So a difference between the two is expected rather than surprising. A laboratory test and an
aggregate of real visits measure different things, over populations that need not overlap. Nothing
in this report says what these websites would look like measured in the field, because this study
did not measure that.

- Chrome UX Report overview (Google) — <https://developer.chrome.com/docs/crux>, page dated 2024-02-08, read 2026-09-02
- Chrome UX Report methodology (Google) — <https://developer.chrome.com/docs/crux/methodology>, page dated 2024-06-20, read 2026-09-02

<!-- research-claims-ok:end -->

## Verifying this data

|  |  |
| --- | --- |
| Source dataset SHA-256 | `7736a4183c00ed8f15a00d9e2f0f5a050f92191a0e7427787d82541dfaa33298` |
| Published rows | 43,737 |
| Published file SHA-256 | `b03742f5751a45231870904b4b9187c7f5934055729197a50baf81fec880b4c2` |
| Pipeline version | 2.0.0 |

Two things are published: the anonymised row-level dataset above, and this methodology. The
analysis pipeline that produced every figure, the identifying source dataset and the detailed QA
record are retained privately by Websites for Tradespeople — the source data names real
businesses — and are used to reproduce and verify any figure on request. Please do not read the
file paths quoted in this document as links: they are internal, and they are named so that a
figure can be traced when we are asked about it.

The source dataset's SHA-256 is given above so that a copy can be confirmed as the one these
figures came from.
