Image CDNs impact

Performance impact by image_cdn provider.

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

At a glance the headline numbers for Image CDNs impact

Performance impact by image_cdn provider.

9
Providers ranked
After min-sites filter
27,273
Sites in sample
Combined across all items
124ms
Best INP (p75)
Sirv
The State of Web Vitals · Q2 2026 · 189,915 sites · phone field datacorewebvitals.io/state-of-cwv
2

The ranking sorted by INP at p75, fastest first

# Provider INP p75 Passing Sites
1 Sirv 124ms 94.7%
57
2 Bunny Image Optimizer 133ms 93.0%
17,162
3 Akamai Image Manager 136ms 92.4%
7,236
4 Fastly Image Optimizer 137ms 90.8%
1,142
5 Thumbor 151ms 88.5%
188
6 Imgix 156ms 87.0%
805
7 ImageKit 170ms 80.0%
79
8 Uploadcare 175ms 86.3%
92
9 Cloudinary 220ms 70.2%
512
The State of Web Vitals · Q2 2026 · 189,915 sites · phone field datacorewebvitals.io/state-of-cwv
3

Passing INP per provider which group passes the INP most often

ProviderSitesPassing INPp75
Sirv 0% 57 95% 124ms
Bunny Image Optimizer 9% 17,162 93% 133ms
Akamai Image Manager 3.8% 7,236 92% 136ms
Fastly Image Optimizer 0.6% 1,142 91% 137ms
Thumbor 0.1% 188 88% 151ms
imgix 0.4% 805 87% 156ms
Uploadcare 0% 92 86% 175ms
ImageKit 0% 79 80% 170ms
Cloudinary 0.3% 512 70% 220ms
Good Needs Improvement Poor Sorted best-passing first · median colored by its own rating · pass = good INP (200ms at p75) · one value per site

93% of Bunny Image Optimizer sites pass INP. Cloudinary trails 23 points behind, leaving 30% 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
ImageKit 0%
672.7s
80170ms
880.02
642.0s
541.3s
Sirv 0%
802.4s
95124ms
870.03
602.2s
461.4s
Akamai Image Manager 3.8%
792.3s
92136ms
860.03
741.8s
551.2s
Bunny Image Optimizer 9%
812.2s
93133ms
880.03
751.8s
591.2s
imgix 0.4%
852.1s
87156ms
860.05
841.5s
75807ms
Thumbor 0.1%
832.3s
88151ms
720.11
781.8s
70909ms
Fastly Image Optimizer 0.6%
861.8s
91137ms
870.02
881.4s
77772ms
Uploadcare 0%
882.2s
86175ms
820.08
831.5s
80719ms
Cloudinary 0.3%
752.5s
70220ms
690.17
731.9s
551.1s
60%95%+ passing Cell: pass rate, small number = p75 · faded rows: under 100 sites

One row per provider, 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.

Bunny Image Optimizer leads on INP: 93% of its sites pass. Cloudinary trails at 70%. computed

The State of Web Vitals · Q2 2026 · 189,915 sites · phone field datacorewebvitals.io/state-of-cwv
Related signals Search (6) → Font services (4) → Personalization (6) → CDN (16) → Chrome field data from 189,915 sites, representing millions of real page loads · How we measured
Live queries (3) — admin only
Query #1: 30.3 ms
SELECT 1 AS ok FROM site_providers LIMIT 0;
Query #2: 453.2 ms
SELECT CAST(p.key AS VARCHAR) 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 site_providers p
                    JOIN sites s ON s.origin = p.origin
                    WHERE p.category = 'image_cdn'
                      AND p.key IS NOT NULL AND CAST(p.key AS VARCHAR) <> ''
                    GROUP BY p.key;
Query #3: 31.3 ms
SELECT COUNT(*) AS n FROM sites;
JSON file lookups (5)
20260625/meta.json 0.02 ms
20260625/types.json 0.83 ms
20260625/loaf-scripts.json 0.33 ms
20260625/menu.json 0.11 ms
20260625/providers.json 3.81 ms