1
At a glance the headline numbers for JS libraries impact
Performance impact by framework provider.
21
Providers ranked
After min-sites filter
272,530
Sites in sample
Combined across all items
1.7s
Best LCP (p75)
Turbo/Hotwire
The State of Web Vitals · Q2 2026 · 189,915 sites · all devices field datacorewebvitals.io/state-of-cwv
2
The ranking sorted by LCP at p75, fastest first
| # | Provider | LCP p75 | Passing | Sites | |
|---|---|---|---|---|---|
| 1 | Turbo/Hotwire | 1.7s | 94.6% | 521 | |
| 2 | Stimulus/Hotwire | 2.0s | 89.7% | 3,023 | |
| 3 | Slick Carousel | 2.2s | 82.0% | 14,391 | |
| 4 | Next.js | 2.2s | 82.1% | 3,974 | |
| 5 | Polyfill.io | 2.2s | 78.8% | 88 | |
| 6 | jQuery | 2.3s | 80.7% | 129,494 | |
| 7 | Owl Carousel | 2.3s | 79.9% | 8,214 | |
| 8 | Ember.js | 2.3s | 77.1% | 115 | |
| 9 | Flickity | 2.3s | 79.4% | 5,515 | |
| 10 | Lodash | 2.3s | 78.4% | 25,426 | |
| 11 | Lazysizes | 2.4s | 78.9% | 12,992 | |
| 12 | React | 2.4s | 79.7% | 449 | |
| 13 | Lit | 2.4s | 76.5% | 10,274 | |
| 14 | Vue.js | 2.4s | 76.3% | 10,740 | |
| 15 | Moment.js | 2.5s | 74.6% | 7,680 | |
| 16 | Swiper | 2.5s | 74.0% | 29,837 | |
| 17 | Nuxt | 2.6s | 72.4% | 1,199 | |
| 18 | GSAP | 2.6s | 72.4% | 4,923 | |
| 19 | Backbone.js | 2.7s | 69.0% | 1,540 | |
| 20 | Angular | 3.0s | 62.2% | 1,908 | |
| 21 | Ionic | 3.7s | 49.1% | 227 |
The State of Web Vitals · Q2 2026 · 189,915 sites · all devices field datacorewebvitals.io/state-of-cwv
3
Passing LCP per provider which group passes the LCP most often
ProviderSitesPassing LCPp75
Solid 0%
3
2.2s
Qwik 0%
2
1.2s
Turbo/Hotwire 0.3%
521
1.7s
Polymer 0%
17
1.5s
Stimulus/Hotwire 1.6%
3,023
2.0s
Next.js 2.1%
3,974
2.2s
Slick Carousel 7.6%
14,391
2.2s
jQuery 68.1%
129,494
2.3s
Owl Carousel 4.3%
8,214
2.3s
React 0.2%
449
2.4s
Flickity 2.9%
5,515
2.3s
lazysizes 6.8%
12,992
2.4s
Polyfill.io 0%
88
2.2s
Lodash 13.4%
25,426
2.3s
Ember.js 0.1%
115
2.3s
Lit 5.4%
10,274
2.4s
Vue.js 5.6%
10,740
2.4s
Moment.js 4%
7,680
2.5s
Swiper 15.7%
29,837
2.5s
GSAP 2.6%
4,923
2.6s
Nuxt.js 0.6%
1,199
2.6s
Backbone.js 0.8%
1,540
2.7s
Angular 1%
1,908
3.0s
Marko 0%
20
3.4s
Ionic 0.1%
227
3.7s
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 Turbo/Hotwire sites pass LCP. Ionic trails 46 points behind, leaving 51% of its sites failing. computed
The State of Web Vitals · Q2 2026 · 189,915 sites · all devices 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
Slick Carousel 7.6%
822.2s
96108ms
850.05
761.8s
541.3s
Moment.js 4%
752.5s
95119ms
820.06
682.0s
511.4s
Next.js 2.1%
822.2s
78188ms
760.10
821.6s
73829ms
Flickity 2.9%
792.3s
98113ms
900.03
721.9s
541.4s
Vue.js 5.6%
762.4s
95127ms
830.06
711.9s
521.4s
Stimulus/Hotwire 1.6%
902.0s
98104ms
910.04
861.5s
82714ms
React 0.2%
802.4s
75203ms
700.16
751.8s
481.2s
GSAP 2.6%
722.6s
98106ms
890.04
662.1s
441.6s
Turbo/Hotwire 0.3%
951.7s
95136ms
950.02
881.4s
74808ms
Lit 5.4%
762.4s
94120ms
830.06
721.9s
571.3s
Ionic 0.1%
493.7s
86170ms
610.20
472.7s
67955ms
Ember.js 0.1%
772.3s
89130ms
780.09
711.9s
67981ms
Polyfill.io 0%
792.2s
99109ms
850.02
721.9s
611.1s
Polymer 0%
941.5s
8188ms
820.01
881.3s
82758ms
Solid 0%
1002.2s
50236ms
1000.07
1001.7s
671.1s
Swiper 15.7%
742.5s
97112ms
880.04
672.1s
451.6s
jQuery 68.1%
812.3s
97104ms
880.04
731.9s
511.4s
Lodash 13.4%
782.3s
95106ms
860.04
731.9s
581.3s
lazysizes 6.8%
792.4s
96113ms
870.05
721.9s
501.4s
Owl Carousel 4.3%
802.3s
97105ms
810.07
741.8s
501.3s
Nuxt.js 0.6%
722.6s
89155ms
690.14
731.9s
631.0s
Angular 1%
623.0s
85174ms
500.30
692.0s
80689ms
Backbone.js 0.8%
692.7s
96112ms
820.07
612.2s
411.6s
Marko 0%
603.4s
100122ms
850.10
302.9s
02.6s
Qwik 0%
1001.2s
10059ms
1000.05
1001.1s
100353ms
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.
Turbo/Hotwire leads on LCP: 95% of its sites pass. Ionic trails at 49%. computed
The State of Web Vitals · Q2 2026 · 189,915 sites · all devices field datacorewebvitals.io/state-of-cwv
Related signals
A/B testing (12) →
Personalization (6) →
E-commerce (16) →
CDN (16) →
Chrome field data from 189,915 sites, representing millions of real page loads · How we measured
Chrome field data from 189,915 sites, representing millions of real page loads. How we measured.
Live queries (3) — admin only
Query #1:
105.3 ms
SELECT 1 AS ok FROM site_providers LIMIT 0;
Query #2:
2,173.8 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 = 'framework'
AND p.key IS NOT NULL AND CAST(p.key AS VARCHAR) <> ''
GROUP BY p.key;
Query #3:
46.0 ms
SELECT COUNT(*) AS n FROM sites;
JSON file lookups (5)
20260625/meta.json
0.05 ms
20260625/types.json
3.06 ms
20260625/loaf-scripts.json
2.80 ms
20260625/menu.json
0.27 ms
20260625/providers.json
6.89 ms