CMS performance

How the most-used CMS platforms perform on real-user Core Web Vitals data.

Field data PhoneDesktopAll Scope All sites Q2 2026 edition · Phone field outcomes
Metric LCP INP CLS FCP TTFB
1

At a glance the headline numbers for CMS performance

How the most-used CMS platforms perform on real-user Core Web Vitals data.

94.8%
Best: Shopify
good LCP
88.3%
Typical CMS
good LCP
69.5%
Worst: WordPress
good LCP

Shopify passes LCP most often of any CMS. 94.8% of its sites pass. WordPress (the most-used CMS) passes least at 69.5%. The gap is 25 percentage points.

The State of Web Vitals · Q2 2026 · 189,915 sites · phone field datacorewebvitals.io/state-of-cwv
2

The ranking sorted by LCP at p75, fastest first

# CMS LCP p75 Passing Sites
1 TYPO3 1.5s 95.5%
95
2 Wix 1.6s 94.4%
5,217
3 Shopify 1.7s 94.8%
7,625
4 Drupal 1.9s 89.9%
2,990
5 Joomla 2.0s 88.3%
2,325
6 Squarespace 2.2s 85.8%
2,028
7 Adobe Commerce 2.4s 76.7%
989
8 WordPress 2.7s 69.5%
59,040
The State of Web Vitals · Q2 2026 · 189,915 sites · phone field datacorewebvitals.io/state-of-cwv
3

Passing LCP per CMS which group passes the LCP most often

CMSSitesPassing LCPp75
Webflow 0% 15 100% 1.7s
Ghost 0% 37 100% 1.4s
Hubspot cms 0% 5 100% 1.5s
TYPO3 0% 95 96% 1.5s
Shopify 4% 7,625 95% 1.7s
Wix 2.7% 5,217 94% 1.6s
Drupal 1.6% 2,990 90% 1.9s
Joomla 1.2% 2,325 88% 2.0s
Squarespace 1.1% 2,028 86% 2.2s
Adobe Commerce 0.5% 989 77% 2.4s
WordPress 31.1% 59,040 69% 2.7s
Storyblok 0% 11 60% 2.8s
Good Needs Improvement Poor Sorted best-passing first · median colored by its own rating · pass = good LCP (2.5s at p75) · one value per site

95% of Shopify sites pass LCP. WordPress trails 26 points behind, leaving 31% of its sites failing. computed

The State of Web Vitals · Q2 2026 · 189,915 sites · phone field datacorewebvitals.io/state-of-cwv
4

All five vitals at once the whole category without toggling - cell is the pass rate, small number the p75

LCP
INP
CLS
FCP
TTFB
Wix 2.7%
941.6s
9696ms
970.00
941.1s
85591ms
Shopify 4%
951.7s
94138ms
940.01
951.2s
96526ms
Storyblok 0%
602.8s
88148ms
500.31
701.9s
221.3s
TYPO3 0%
961.5s
88108ms
960.01
901.3s
71899ms
Webflow 0%
1001.7s
100110ms
1000.00
1001.1s
86734ms
WordPress 31.1%
692.7s
97118ms
900.01
582.3s
321.8s
Drupal 1.6%
901.9s
94128ms
910.02
831.6s
65970ms
Joomla 1.2%
882.0s
95104ms
930.00
791.7s
541.1s
Adobe Commerce 0.5%
772.4s
95137ms
790.08
662.1s
301.6s
Squarespace 1.1%
862.2s
99105ms
910.02
821.6s
93578ms
Ghost 0%
1001.4s
92104ms
910.06
1001.2s
85592ms
Hubspot cms 0%
1001.5s
100128ms
1000.00
1001.3s
80709ms
60%95%+ passing Cell: pass rate, small number = p75 · faded rows: under 100 sites

One row per CMS, one column per vital - the cell is the share of sites passing, the small number the p75. No toggling needed to see where the category actually differs.

Shopify leads on LCP: 95% of its sites pass. WordPress trails at 69%. computed

The State of Web Vitals · Q2 2026 · 189,915 sites · phone field datacorewebvitals.io/state-of-cwv
5

Why this matters for the Core Web Vitals, and where to start fixing it

The CMS decides a lot about your Core Web Vitals before you write a line of your own code. It controls the HTML that ships, the scripts and styles that load by default, and how images are handled. Two sites on the same platform can score very differently depending on the theme and plugins, but the platform sets the starting point and the ceiling for how much you can tune.

Start with what the platform loads that you do not need. Strip the default scripts and styles that come with themes and plugins, defer the JavaScript that is not needed for the first paint, and make sure images go out in modern formats at the right size. On script-heavy platforms the biggest wins are usually in cutting and deferring what loads on every page.

How does your CMS affect Core Web Vitals?

Among the most-used CMS platforms, TYPO3 reaches a good LCP on 95.5% of sites; WordPress on 69.5%.

Related signals CDN (7) → Frameworks (11) → Chrome field data from 189,915 sites, representing millions of real page loads · How we measured
Live queries (2) — admin only
Query #1: 500.0 ms
SELECT s.entities.cms AS e, COUNT(*) AS sites,
  COUNT(*) FILTER (WHERE s.crux."all".lcp IS NOT NULL) AS all_lcp_n,
  quantile_disc(s.crux."all".lcp, 0.5) AS all_lcp_median,
  quantile_disc(s.crux."all".lcp, 0.75) AS all_lcp_p75,
  COUNT(*) FILTER (WHERE s.crux."all".lcp <= 2500) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."all".lcp IS NOT NULL), 0) AS all_lcp_good,
  COUNT(*) FILTER (WHERE s.crux."all".lcp > 2500 AND s.crux."all".lcp <= 4000) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."all".lcp IS NOT NULL), 0) AS all_lcp_ni,
  COUNT(*) FILTER (WHERE s.crux."all".lcp > 4000) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."all".lcp IS NOT NULL), 0) AS all_lcp_poor,
  COUNT(*) FILTER (WHERE s.crux."all".inp IS NOT NULL) AS all_inp_n,
  quantile_disc(s.crux."all".inp, 0.5) AS all_inp_median,
  quantile_disc(s.crux."all".inp, 0.75) AS all_inp_p75,
  COUNT(*) FILTER (WHERE s.crux."all".inp <= 200) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."all".inp IS NOT NULL), 0) AS all_inp_good,
  COUNT(*) FILTER (WHERE s.crux."all".inp > 200 AND s.crux."all".inp <= 500) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."all".inp IS NOT NULL), 0) AS all_inp_ni,
  COUNT(*) FILTER (WHERE s.crux."all".inp > 500) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."all".inp IS NOT NULL), 0) AS all_inp_poor,
  COUNT(*) FILTER (WHERE s.crux."all".cls IS NOT NULL) AS all_cls_n,
  quantile_disc(s.crux."all".cls, 0.5) AS all_cls_median,
  quantile_disc(s.crux."all".cls, 0.75) AS all_cls_p75,
  COUNT(*) FILTER (WHERE s.crux."all".cls <= 0.1) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."all".cls IS NOT NULL), 0) AS all_cls_good,
  COUNT(*) FILTER (WHERE s.crux."all".cls > 0.1 AND s.crux."all".cls <= 0.25) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."all".cls IS NOT NULL), 0) AS all_cls_ni,
  COUNT(*) FILTER (WHERE s.crux."all".cls > 0.25) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."all".cls IS NOT NULL), 0) AS all_cls_poor,
  COUNT(*) FILTER (WHERE s.crux."all".fcp IS NOT NULL) AS all_fcp_n,
  quantile_disc(s.crux."all".fcp, 0.5) AS all_fcp_median,
  quantile_disc(s.crux."all".fcp, 0.75) AS all_fcp_p75,
  COUNT(*) FILTER (WHERE s.crux."all".fcp <= 1800) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."all".fcp IS NOT NULL), 0) AS all_fcp_good,
  COUNT(*) FILTER (WHERE s.crux."all".fcp > 1800 AND s.crux."all".fcp <= 3000) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."all".fcp IS NOT NULL), 0) AS all_fcp_ni,
  COUNT(*) FILTER (WHERE s.crux."all".fcp > 3000) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."all".fcp IS NOT NULL), 0) AS all_fcp_poor,
  COUNT(*) FILTER (WHERE s.crux."all".ttfb IS NOT NULL) AS all_ttfb_n,
  quantile_disc(s.crux."all".ttfb, 0.5) AS all_ttfb_median,
  quantile_disc(s.crux."all".ttfb, 0.75) AS all_ttfb_p75,
  COUNT(*) FILTER (WHERE s.crux."all".ttfb <= 800) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."all".ttfb IS NOT NULL), 0) AS all_ttfb_good,
  COUNT(*) FILTER (WHERE s.crux."all".ttfb > 800 AND s.crux."all".ttfb <= 1800) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."all".ttfb IS NOT NULL), 0) AS all_ttfb_ni,
  COUNT(*) FILTER (WHERE s.crux."all".ttfb > 1800) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."all".ttfb IS NOT NULL), 0) AS all_ttfb_poor,
  COUNT(*) FILTER (WHERE s.crux."phone".lcp IS NOT NULL) AS phone_lcp_n,
  quantile_disc(s.crux."phone".lcp, 0.5) AS phone_lcp_median,
  quantile_disc(s.crux."phone".lcp, 0.75) AS phone_lcp_p75,
  COUNT(*) FILTER (WHERE s.crux."phone".lcp <= 2500) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."phone".lcp IS NOT NULL), 0) AS phone_lcp_good,
  COUNT(*) FILTER (WHERE s.crux."phone".lcp > 2500 AND s.crux."phone".lcp <= 4000) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."phone".lcp IS NOT NULL), 0) AS phone_lcp_ni,
  COUNT(*) FILTER (WHERE s.crux."phone".lcp > 4000) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."phone".lcp IS NOT NULL), 0) AS phone_lcp_poor,
  COUNT(*) FILTER (WHERE s.crux."phone".inp IS NOT NULL) AS phone_inp_n,
  quantile_disc(s.crux."phone".inp, 0.5) AS phone_inp_median,
  quantile_disc(s.crux."phone".inp, 0.75) AS phone_inp_p75,
  COUNT(*) FILTER (WHERE s.crux."phone".inp <= 200) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."phone".inp IS NOT NULL), 0) AS phone_inp_good,
  COUNT(*) FILTER (WHERE s.crux."phone".inp > 200 AND s.crux."phone".inp <= 500) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."phone".inp IS NOT NULL), 0) AS phone_inp_ni,
  COUNT(*) FILTER (WHERE s.crux."phone".inp > 500) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."phone".inp IS NOT NULL), 0) AS phone_inp_poor,
  COUNT(*) FILTER (WHERE s.crux."phone".cls IS NOT NULL) AS phone_cls_n,
  quantile_disc(s.crux."phone".cls, 0.5) AS phone_cls_median,
  quantile_disc(s.crux."phone".cls, 0.75) AS phone_cls_p75,
  COUNT(*) FILTER (WHERE s.crux."phone".cls <= 0.1) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."phone".cls IS NOT NULL), 0) AS phone_cls_good,
  COUNT(*) FILTER (WHERE s.crux."phone".cls > 0.1 AND s.crux."phone".cls <= 0.25) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."phone".cls IS NOT NULL), 0) AS phone_cls_ni,
  COUNT(*) FILTER (WHERE s.crux."phone".cls > 0.25) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."phone".cls IS NOT NULL), 0) AS phone_cls_poor,
  COUNT(*) FILTER (WHERE s.crux."phone".fcp IS NOT NULL) AS phone_fcp_n,
  quantile_disc(s.crux."phone".fcp, 0.5) AS phone_fcp_median,
  quantile_disc(s.crux."phone".fcp, 0.75) AS phone_fcp_p75,
  COUNT(*) FILTER (WHERE s.crux."phone".fcp <= 1800) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."phone".fcp IS NOT NULL), 0) AS phone_fcp_good,
  COUNT(*) FILTER (WHERE s.crux."phone".fcp > 1800 AND s.crux."phone".fcp <= 3000) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."phone".fcp IS NOT NULL), 0) AS phone_fcp_ni,
  COUNT(*) FILTER (WHERE s.crux."phone".fcp > 3000) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."phone".fcp IS NOT NULL), 0) AS phone_fcp_poor,
  COUNT(*) FILTER (WHERE s.crux."phone".ttfb IS NOT NULL) AS phone_ttfb_n,
  quantile_disc(s.crux."phone".ttfb, 0.5) AS phone_ttfb_median,
  quantile_disc(s.crux."phone".ttfb, 0.75) AS phone_ttfb_p75,
  COUNT(*) FILTER (WHERE s.crux."phone".ttfb <= 800) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."phone".ttfb IS NOT NULL), 0) AS phone_ttfb_good,
  COUNT(*) FILTER (WHERE s.crux."phone".ttfb > 800 AND s.crux."phone".ttfb <= 1800) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."phone".ttfb IS NOT NULL), 0) AS phone_ttfb_ni,
  COUNT(*) FILTER (WHERE s.crux."phone".ttfb > 1800) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."phone".ttfb IS NOT NULL), 0) AS phone_ttfb_poor,
  COUNT(*) FILTER (WHERE s.crux."desktop".lcp IS NOT NULL) AS desktop_lcp_n,
  quantile_disc(s.crux."desktop".lcp, 0.5) AS desktop_lcp_median,
  quantile_disc(s.crux."desktop".lcp, 0.75) AS desktop_lcp_p75,
  COUNT(*) FILTER (WHERE s.crux."desktop".lcp <= 2500) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."desktop".lcp IS NOT NULL), 0) AS desktop_lcp_good,
  COUNT(*) FILTER (WHERE s.crux."desktop".lcp > 2500 AND s.crux."desktop".lcp <= 4000) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."desktop".lcp IS NOT NULL), 0) AS desktop_lcp_ni,
  COUNT(*) FILTER (WHERE s.crux."desktop".lcp > 4000) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."desktop".lcp IS NOT NULL), 0) AS desktop_lcp_poor,
  COUNT(*) FILTER (WHERE s.crux."desktop".inp IS NOT NULL) AS desktop_inp_n,
  quantile_disc(s.crux."desktop".inp, 0.5) AS desktop_inp_median,
  quantile_disc(s.crux."desktop".inp, 0.75) AS desktop_inp_p75,
  COUNT(*) FILTER (WHERE s.crux."desktop".inp <= 200) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."desktop".inp IS NOT NULL), 0) AS desktop_inp_good,
  COUNT(*) FILTER (WHERE s.crux."desktop".inp > 200 AND s.crux."desktop".inp <= 500) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."desktop".inp IS NOT NULL), 0) AS desktop_inp_ni,
  COUNT(*) FILTER (WHERE s.crux."desktop".inp > 500) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."desktop".inp IS NOT NULL), 0) AS desktop_inp_poor,
  COUNT(*) FILTER (WHERE s.crux."desktop".cls IS NOT NULL) AS desktop_cls_n,
  quantile_disc(s.crux."desktop".cls, 0.5) AS desktop_cls_median,
  quantile_disc(s.crux."desktop".cls, 0.75) AS desktop_cls_p75,
  COUNT(*) FILTER (WHERE s.crux."desktop".cls <= 0.1) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."desktop".cls IS NOT NULL), 0) AS desktop_cls_good,
  COUNT(*) FILTER (WHERE s.crux."desktop".cls > 0.1 AND s.crux."desktop".cls <= 0.25) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."desktop".cls IS NOT NULL), 0) AS desktop_cls_ni,
  COUNT(*) FILTER (WHERE s.crux."desktop".cls > 0.25) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."desktop".cls IS NOT NULL), 0) AS desktop_cls_poor,
  COUNT(*) FILTER (WHERE s.crux."desktop".fcp IS NOT NULL) AS desktop_fcp_n,
  quantile_disc(s.crux."desktop".fcp, 0.5) AS desktop_fcp_median,
  quantile_disc(s.crux."desktop".fcp, 0.75) AS desktop_fcp_p75,
  COUNT(*) FILTER (WHERE s.crux."desktop".fcp <= 1800) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."desktop".fcp IS NOT NULL), 0) AS desktop_fcp_good,
  COUNT(*) FILTER (WHERE s.crux."desktop".fcp > 1800 AND s.crux."desktop".fcp <= 3000) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."desktop".fcp IS NOT NULL), 0) AS desktop_fcp_ni,
  COUNT(*) FILTER (WHERE s.crux."desktop".fcp > 3000) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."desktop".fcp IS NOT NULL), 0) AS desktop_fcp_poor,
  COUNT(*) FILTER (WHERE s.crux."desktop".ttfb IS NOT NULL) AS desktop_ttfb_n,
  quantile_disc(s.crux."desktop".ttfb, 0.5) AS desktop_ttfb_median,
  quantile_disc(s.crux."desktop".ttfb, 0.75) AS desktop_ttfb_p75,
  COUNT(*) FILTER (WHERE s.crux."desktop".ttfb <= 800) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."desktop".ttfb IS NOT NULL), 0) AS desktop_ttfb_good,
  COUNT(*) FILTER (WHERE s.crux."desktop".ttfb > 800 AND s.crux."desktop".ttfb <= 1800) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."desktop".ttfb IS NOT NULL), 0) AS desktop_ttfb_ni,
  COUNT(*) FILTER (WHERE s.crux."desktop".ttfb > 1800) * 1.0 / NULLIF(COUNT(*) FILTER (WHERE s.crux."desktop".ttfb IS NOT NULL), 0) AS desktop_ttfb_poor
                FROM sites s
                WHERE s.entities.cms IS NOT NULL AND CAST(s.entities.cms AS VARCHAR) <> ''
                GROUP BY s.entities.cms;
Query #2: 38.0 ms
SELECT COUNT(*) AS n FROM sites;
JSON file lookups (5)
20260625/meta.json 0.03 ms
20260625/types.json 0.66 ms
20260625/loaf-scripts.json 0.47 ms
20260625/menu.json 0.16 ms
20260625/technologies.json 0.74 ms