web-vitals-kirjasto käytännössä 2026: Kenttädatan kerääminen ja Core Web Vitals -mittaus tuotannossa
Käytännön opas web-vitals-kirjaston käyttöön tuotannossa 2026: INP-, LCP-, CLS-, TTFB- ja FCP-mittaus, attribution-data, sendBeacon-lähetys ja oman p75-arvon vertailu CrUX-dataan.
web-vitals-kirjasto on Googlen virallinen JavaScript-kirjasto Core Web Vitals -mittareiden keräämiseen oikeilta käyttäjiltä (RUM, Real User Monitoring) suoraan selaimessa. Vuonna 2026 se on käytännössä ainoa luotettava tapa saada selville, mitä sivustollasi todella tapahtuu, koska Lighthouse ja PageSpeed Insights antavat vain hetkellisen laboratoriokuvan. web-vitals-kirjasto puolestaan raportoi jokaisen todellisen käyttäjän kokemuksen: hitaan 3G-yhteyden Android-laitteesta uusimpaan MacBookiin. Kentältä kerätty p75-persentiili on se, mitä Google käyttää hakukoneoptimoinnissa (ei siis synteettinen pisteesi).
web-vitals v4 (ja tuleva v5) mittaa LCP:n, INP:n, CLS:n, TTFB:n ja FCP:n suoraan käyttäjän selaimessa ja on ainoa tapa saada dataa, joka vastaa Chrome UX Reportin (CrUX) lukuja.
INP korvasi FID:n virallisena Core Web Vital -mittarina maaliskuussa 2024, ja onINP() on nykyään pakollinen osa jokaista RUM-integraatiota.
Käytä sendBeacon-API:a datan lähetykseen. Tavallinen fetch menettää tapahtumat, kun käyttäjä sulkee välilehden ennen pyynnön valmistumista.
Attribution-versiot (esim. onINP() attribution-lipulla) kertovat, mikä DOM-elementti tai skripti aiheutti hitauden. Ilman tätä et voi korjata ongelmaa.
Segmentoi data laiteluokan ja verkkoyhteyden mukaan: mediaaniluku piilottaa sen tosiasian, että low-end Androidilla INP voi olla 4× hitaampi kuin desktopilla.
Vertaa omaa p75-arvoasi CrUX-datan p75:een. Jos ne eroavat merkittävästi, sampling-strategiassasi tai integraatiossasi on ongelma.
Miksi kenttädata on tärkeämpää kuin Lighthouse-pisteet
Olen viettänyt liian monta iltaa katselemassa tiimien juhlistavan vihreää Lighthouse-pistettä sillä aikaa, kun heidän oikeat käyttäjänsä kärsivät 2,5 sekunnin INP-arvoista Samsung Galaxy A-sarjan puhelimillaan. Synteettinen testaus mittaa yhtä lataamista yhdellä konfiguraatiolla. Lighthouse pyörii tyypillisesti moderoidulla Moto G4 -simulaatiolla tai Chromen tehokkaalla lokaalilla suoritusalustalla. Kentällä käyttäjäsi eivät ole simulaatioita: heillä on cache-tila, browser-laajennuksia, taustavälilehtiä syömässä muistia ja epävakaa mobiiliverkko.
Google käyttää hakukoneoptimoinnissa Chrome User Experience Reportia (CrUX), joka aggregoi anonyymiä kenttädataa oikeilta Chrome-käyttäjiltä. CrUX raportoi p75-persentiilin, eli arvon, jonka alle 75 % vierailuista jää. Jos synteettinen LCP-arvosi on 1,8 s mutta CrUX-p75 on 3,4 s, hakusijoituksesi määräytyy jälkimmäisen mukaan. web-vitals-kirjasto on ainoa tapa saada käsiisi sama data ennen kuin CrUX (joka päivittyy 28 päivän liukuvana keskiarvona) tuo sen 4 viikon viiveellä.
Käytännön esimerkki asiakkaastani: sähkökauppa, jonka Lighthouse-tulos oli 92 mutta INP-p75 mobiilissa 480 ms (surkea siis). Kentällä ostoprosessin painike käytti kolmannen osapuolen A/B-testauslaskuria, joka blokkasi pääsäikeen 300 ms jokaisella klikkauksella. Lighthouse ei ajanut ostoflowia loppuun, joten se ei koskaan nähnyt ongelmaa. RUM-data näki sen ensimmäisen 15 minuutin aikana.
Mikä on web-vitals-kirjasto ja mitä uutta 2026
web-vitals on Googlen ylläpitämä avoimen lähdekoodin JavaScript-kirjasto (n. 2 KB gzipattuna), joka tarjoaa yhtenäisen API:n Core Web Vitals -mittareiden mittaamiseen selaimessa täsmälleen samalla tavalla kuin CrUX ne raportoi. Se abstrahoi PerformanceObserver-API:n monimutkaisuudet ja käsittelee bfcache-palautukset, sivuvaihdot ja iframe-tapahtumat oikein. Ne ovat juuri niitä asioita, joissa oma implementaatio menee lähes varmasti pieleen.
Vuoden 2026 tilanteessa v4-linja on tuotantovakaus ja v5 on työn alla. Merkittävimmät muutokset viimeisen kahden vuoden aikana:
onFID poistettu: First Input Delay lopetettiin virallisena Core Web Vital -mittarina maaliskuussa 2024. Käytä onINP-funktiota.
Attribution-versiot integroituna päälinjaan:web-vitals/attribution-alipaketti tarjoaa laajemmat metatiedot jokaiselle mittarille, esim. INP:n eventTarget, loadState ja longAnimationFrameEntries.
bfcache-palautukset: Kaikki funktiot raportoivat oletusarvoisesti myös bfcache-palautuksista, mikä on kriittistä Safarissa ja mobiili-Chromessa, joissa jopa 20 % navigaatioista tulee bfcachesta.
onLCP tukee Soft Navigations -kokeilua: SPA-sovelluksissa, joissa reittivaihto ei laukaise täydellistä navigaatiota, uusi reportSoftNavs-lippu antaa realistisemman kuvan.
Asennus ja peruskäyttö tuotannossa
Asennus tapahtuu npm:llä tai suoraan CDN:n kautta. Suosittelen npm-riippuvuutta, koska silloin bundlerisi voi tree-shakata käyttämättömät mittarit pois. CDN-versio on kätevä kevyeen testaukseen, mutta se lisää kolmannen osapuolen pyynnön kriittiselle polulle.
npm install web-vitals@^4
Yksinkertaisin toimiva integraatio, joka mittaa kaikki viisi mittaria ja lokittaa ne konsoliin:
// src/lib/rum.ts
import { onLCP, onINP, onCLS, onTTFB, onFCP } from 'web-vitals';
// Jokainen funktio kutsuu callbackia kerran per mittaus.
// LCP ja CLS raportoivat lopullisen arvon, kun sivu piilotetaan
// tai käyttäjä navigoi pois. Älä yritä lukea arvoa aikaisemmin.
onLCP(console.log);
onINP(console.log);
onCLS(console.log);
onTTFB(console.log);
onFCP(console.log);
Tuotannossa et tietenkään lokita konsoliin, vaan lähetät datan analytiikkapalvelimellesi. Lataa RUM-koodi asynkronisesti pääbundlen ulkopuolella, jotta se ei itse hidasta sivua:
// src/app/layout.tsx (Next.js 15 -esimerkki)
'use client';
import { useEffect } from 'react';
export default function VitalsBootstrap() {
useEffect(() => {
// Dynaaminen import lataa web-vitalsin idle-vaiheessa
// eikä blokkaa hydrataatiota tai kriittistä polkua.
if ('requestIdleCallback' in window) {
requestIdleCallback(() => import('@/lib/rum').then(m => m.init()));
} else {
setTimeout(() => import('@/lib/rum').then(m => m.init()), 1);
}
}, []);
return null;
}
Tässä on koko homman piilotettu ansa: RUM-koodi ei saa itse olla syy INP-huonoontumiseen. Testasin kerran ratkaisun, jossa RUM-skripti lisäsi 40 ms pääsäikeen työtä jokaiselle interaktiolle. Se pilasi juuri sen mittarin, jota yritettiin mitata. Idle-lataus ja pieni bundle ovat siis välttämättömiä.
Miten INP mitataan käytännössä
INP (Interaction to Next Paint) on Core Web Vitalsin nuorin ja hankalin mittari. Toisin kuin FID, joka mittasi vain ensimmäisen interaktion viivettä, INP raportoi huonoimman interaktion viiveen koko sivun eliniän ajalta (95. persentiili sivun sisäisistä interaktioista). Se pitää sisällään input-viiveen, tapahtuman käsittelyn kokonaiskeston JA seuraavan maalauksen viiveen. Siis koko käyttäjän kokeman "napin painallus, ruutu muuttuu" -latenssin.
Yksinkertainen onINP-kutsu tuottaa peruslukemat, mutta tuotannossa haluat attribution-version, joka kertoo mikä elementti ja mikä vaihe (input delay, processing time, presentation delay) oli pullonkaula:
Kun näet dashboardissasi, että INP-p75 = 380 ms ja 62 % huonoista interaktioista kohdistuu button#checkout-elementtiin processingDuration-vaiheessa, tiedät että checkout-napin klikkihändleri on liian raskas. Ilman attribution-dataa katsoisit vain lukua "380 ms" etkä tietäisi, mistä aloittaa.
Datan lähetys analytiikkaan sendBeacon-API:lla
Kriittinen tekninen yksityiskohta: älä lähetä RUM-dataa fetch-kutsulla oletuskonfiguraatiolla. Kun käyttäjä sulkee välilehden tai siirtyy pois, selain katkaisee keskeneräiset fetch-pyynnöt. Menetät juuri sen datan, joka on kaikkein arvokkainta, koska LCP:n lopullinen arvo raportoidaan usein visibilitychange-tapahtumassa. Käytä Navigator.sendBeacon-API:a, joka takaa lähetyksen jopa kun sivu jo puretaan.
// src/lib/rum.ts
import { onLCP, onINP, onCLS, onTTFB, onFCP } from 'web-vitals/attribution';
const ENDPOINT = '/api/rum';
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
navigationType: metric.navigationType,
attribution: metric.attribution,
// Konteksti-metadata, jonka lisäät itse
url: location.pathname,
deviceMemory: navigator.deviceMemory ?? null,
connection: navigator.connection?.effectiveType ?? null,
hardwareConcurrency: navigator.hardwareConcurrency ?? null,
ts: performance.now(),
});
// sendBeacon toimii myös unload-vaiheessa; palauttaa false
// jos queue on täynnä, jolloin fallbackaamme fetch keepaliveen.
if (!navigator.sendBeacon(ENDPOINT, body)) {
fetch(ENDPOINT, { body, method: 'POST', keepalive: true });
}
}
export function init() {
onLCP(sendToAnalytics);
onINP(sendToAnalytics);
onCLS(sendToAnalytics);
onTTFB(sendToAnalytics);
onFCP(sendToAnalytics);
}
Attribution-data: mikä elementti aiheutti hitauden
Perustason web-vitals-mittarit kertovat että jokin on huonosti. Attribution-data kertoo miksi. Kirjaston web-vitals/attribution-alipaketti lisää jokaiseen mittariin attribution-objektin, joka on yksi vuoden 2024–2026 merkittävimmistä RUM-parannuksista.
Käytännön esimerkkejä siitä, mitä attribution paljastaa:
LCP:attribution.element (CSS-selektori LCP-elementille), attribution.url (LCP-kuvan URL), attribution.timeToFirstByte, attribution.resourceLoadDelay, attribution.resourceLoadDuration, attribution.elementRenderDelay. Nyt tiedät, kuluuko aika TTFB:hen, kuvan lataamiseen vai renderöintiin.
CLS:attribution.largestShiftTarget (mikä elementti liikkui eniten), attribution.largestShiftValue, attribution.loadState. Yleensä paljastaa banneri-mainoksen tai kuvan ilman width/height-atribuuttia. Lisää vinkkejä löytyy CLS-optimointioppaastamme.
TTFB:attribution.waitingDuration, attribution.dnsDuration, attribution.connectionDuration, attribution.requestDuration. Erottelee, onko ongelma DNS:ssä, TLS-kättelyssä vai backendin vastauksessa.
Kun keskinkertaisen SaaS-tuotteen INP-p75 oli 320 ms, attribution paljasti että 84 % huonoista interaktioista klusteroitui viiteen elementtiin, ja ne kaikki liittyivät saman React Context Providerin re-renderöintiin. Yhden useMemo-lisäyksen jälkeen INP-p75 tippui 140 ms:iin viikossa. Ilman attributionia olisimme profiloineet väärää polkua kuukausia. Sama pätee LCP-optimointiin: attribution-data kertoo, laitatko fetchpriority="high"-atribuutin oikealle elementille.
Datan segmentointi laiteluokan ja verkon mukaan
Yksi luku, jota kaikki katsovat (kokonais-p75), piilottaa enemmän kuin paljastaa. Sivustollasi voi olla erinomainen LCP p75 = 1,9 s kokonaisuudessaan, mutta samaan aikaan p75 = 4,8 s mobiilin low-end-segmentissä (deviceMemory ≤ 2 GB, hardwareConcurrency ≤ 4). Jos 30 % liikevaihdostasi tulee siitä segmentistä, se kokonaisluku valehtelee sinulle.
Suositukseni jokaisen mittarin taggaamiseen:
Ulottuvuus
Datalähde
Tyypilliset arvot
Miksi tärkeä
Laiteluokka
navigator.deviceMemory, hardwareConcurrency
low / mid / high
INP eroaa 3–5× low- ja high-endin välillä
Verkkoyhteys
navigator.connection.effectiveType
4g / 3g / 2g / slow-2g
LCP ja TTFB skaalautuvat suoraan yhteysnopeuden kanssa
Sivutyyppi
Oma reititys / URL-mätsäys
etusivu / lista / tuote / checkout
Ongelmat klusteroituvat sivutyypeittäin
Navigaatiotyyppi
metric.navigationType
navigate / reload / back-forward / prerender
bfcache-palautukset ovat aina nopeita ja vääristävät keskiarvoa
Selain/OS
navigator.userAgentData
Chrome/Android, Safari/iOS...
iOS Safari raportoi eri mittareita eri tavoin
Kokeilut/A/B
Oma testaus-framework
variant A / B / control
Vertaa suorituskykyeroa varianttien välillä
Kun raportoit dataa dashboardissa, piirrä ainakin kaksi p75-käyrää jokaisesta mittarista: yksi mobiili + slow-4g, toinen desktop + 4g. Jos näet ne samassa akselissa, hidastuvat trendit ovat ilmiselviä. Mediaanit valehtelevat aina, joten käytä p75:tä (sama kuin CrUX) ja mielellään p95:tä katastrofien havaitsemiseen.
p75-persentiili ja vertailu CrUX-dataan
Kun oma RUM-datasi on kunnossa, seuraava askel on validoida se CrUX:n vasten. CrUX-data on saatavilla kolmella tavalla: PageSpeed Insights -API (URL- ja origin-taso, edellisen 28 päivän data), CrUX API (kevyt ohjelmallinen pääsy) ja CrUX BigQuery -dataset (kuukausittaiset historialliset aggregaatit, joilla voit vertailla domain-tasolla).
Jos oma p75-arvosi ja CrUX-p75 eroavat yli 20 %, tarkista nämä yleiset syyt:
Sampling-vinouma: Kerätkö dataa vain kirjautuneilta käyttäjiltä tai vain onnistuneilta sivunlatauksilta? CrUX näkee kaikki. Kerää RUM ennen autentikaatiogatea.
Selainkate: web-vitals toimii vain Chrome-pohjaisissa selaimissa täydellisesti (LCP ja INP eivät Safarissa). CrUX raportoi vain Chromen dataa, joten tässä ei pitäisi olla ristiriitaa. Mutta jos vertaat oman datasi kaikkia selaimia CrUX:iin, saat vääristyneen kuvan.
bfcache-käsittely: Jos et raportoi bfcache-palautuksia, oma p75:si näyttää huonommalta kuin CrUX. Kirjaston oletuskäytös on oikea, joten älä ylikirjoita.
Reitityskohtainen segmentointi: CrUX raportoi URL- ja origin-tasolla. Vertaa vastaavaa granulariteettia.
Käytännön nyrkkisääntö: jos rakennat uutta RUM-integraatiota, tavoittele että oma p75-arvosi on ±15 % CrUX-arvosta ensimmäisen kahden viikon aikana. Jos ei, sampling-strategiassasi on todennäköisesti bugi. (Näin kävi omalla projektillani viime keväänä, ja syy oli yksi puuttuva reitti robots.txt-poissulkulistassa.)
Yleiset sudenkuopat ja edistyneet tekniikat
Tässä muutama vuosi RUM-integraatioita tekemällä opituista sudenkuopista:
Mittarien poimiminen ennen aikaa. LCP ja CLS päivittyvät koko sivun eliniän ajan. Lue arvot vain callbackissa, jonka web-vitals kutsuu. Älä yritä lukea niitä DOMContentLoaded-tapahtumassa.
SPA-navigaatiot. Perinteinen web-vitals mittaa vain kovia navigaatioita. SPA-reittivaihdoissa (Next.js App Router, React Router) vanhempi arvo säilyy, ellet käytä reportSoftNavs-lippua tai emuloi navigaatiota käsin. Tämä on aktiivinen kehityskohde 2026.
Iframe-sivut. web-vitals mittaa vain sen frame'n, jossa se ladataan. Jos LCP-elementti on iframen sisällä, joudut mittaamaan sen erikseen.
Sampling. Jos sivustollasi on 10M+ latauksia päivässä, älä kerää 100 % dataa vaan samplaa 10 %. Käytä johdonmukaista hashia (esim. userId % 10 === 0), jotta sama käyttäjä joko on tai ei ole näytteessä. Muuten et pysty jäljittämään yksittäisen käyttäjän session tapahtumia.
Kolmannen osapuolen skriptit. Jos INP-attribution osoittaa toistuvasti kolmannen osapuolen skriptiin (esim. tag manager, chat-widget, mainosverkko), sinulla ei ole valtaa niiden koodiin. Mutta sinulla on valta niiden lataustapaan. Lykkää niitä requestIdleCallbackilla, käytä Partytown-tyyppistä web worker -siirtoa tai yksinkertaisesti poista ne.
TTFB-kytkös backendiin. Kun RUM-data näyttää TTFB-p75 = 1200 ms, mutta oma synteettinen mittauksesi on 300 ms, syynä on lähes aina CDN-cache-miss-jakauma reaalikäyttäjillä. Katso TTFB-optimointi-opas.
Mitä datalle sitten tehdään
Kerätty data on hyödytöntä ilman tarkastelurutiinia. Suosittelen viikoittaista rytmiä: maanantaiaamuisin katsomme p75-käyrät kaikilta pääasiallisilta sivutyypeiltä + laiteluokat, verrataan edellisviikkoon ja avataan Jira-tiketti jokaisesta > 10 % huonontumasta. Kuukausittain vertaamme CrUX:iin ja säädämme sampling-strategiaa tarpeen mukaan.
Automatisoi hälytys: aseta CI-pipelineen tarkistus, joka kysyy RUM-dashboardilta viimeisen 24 tunnin p75-arvot ennen releaseä ja jälkeen. Jos INP-p75 kasvaa > 30 ms tai LCP-p75 > 200 ms, blokkaa release. Tämä yksinkertainen sääntö on estänyt useamman regression kuin mikään yksittäinen testi (puhun kokemuksesta viime syksyn shippailun pohjalta).
Usein kysytyt kysymykset
Mikä on web-vitals-kirjasto?
web-vitals on Googlen virallinen JavaScript-kirjasto Core Web Vitals -mittareiden (LCP, INP, CLS, TTFB, FCP) mittaamiseen selaimessa. Se tuottaa täsmälleen samat arvot kuin Chrome UX Report, joten voit kerätä oman kenttädatasi CrUX:n rinnalle ilman viivettä.
Miten mitata Core Web Vitals tuotannossa?
Asenna web-vitals-kirjasto (npm install web-vitals), rekisteröi callbackit onLCP-, onINP-, onCLS-, onTTFB- ja onFCP-funktioille ja lähetä data omalle analytiikkaendpointillesi Navigator.sendBeacon-API:lla. Käytä attribution-versiota saadaksesi selville, mikä elementti aiheutti hitauden.
Mitä eroa on kenttä- ja laboratoriodatalla?
Laboratoriodata (Lighthouse, WebPageTest) mittaa yhden latauksen kontrolloidussa ympäristössä ja on toistettava. Kenttädata (RUM, CrUX) kerää mittauksia oikeilta käyttäjiltä oikeilla laitteilla ja verkoilla. Google käyttää kenttädataa hakukoneoptimoinnissa, joten se on ainoa data, joka vaikuttaa sijoituksiisi.
Miksi Lighthouse-pisteeni eivät vastaa CrUX-dataa?
Lighthouse ajaa moderoidulla laitteella yhden latauksen kerrallaan, kun taas CrUX raportoi 28 päivän aggregaatin oikeista käyttäjistä eri laitteilla ja verkoilla. Ero voi olla helposti 2–3× varsinkin INP:ssä, joka riippuu sivun kanssa vuorovaikutuksesta (Lighthouse ei simuloi realistista klikkausflowia).
Miten lähetän Web Vitals -datan Google Analyticsiin?
Käytä GA4:n gtag-eventtiä tai Measurement Protocol -API:a. Kutsu jokaisen web-vitals-callbackin sisällä gtag('event', metric.name, { value: Math.round(metric.value), metric_id: metric.id, metric_rating: metric.rating, non_interaction: true }). Muista lisätä sendBeacon-fallback, jotta data ei katoa sivun sulkeutuessa.
Mitä attribution-data tarkoittaa käytännössä?
Attribution kertoo, mikä DOM-elementti tai vaihe aiheutti mittarin arvon. Esim. INP-attribution paljastaa klikatun elementin, input delayn, prosessointiajan ja renderöintiviiveen. Ilman attributionia näet vain "INP = 380 ms", mutta sen kanssa näet "checkout-nappi, 340 ms prosessointia React-re-renderistä".
Käytännön opas responsiivisiin kuviin vuonna 2026: srcset ja sizes oikein, AVIF/WebP-fallback picture-elementillä, lazy loading ja fetchpriority LCP:n parantamiseksi. Sisältää valmiit koodiesimerkit, kuvaputken ohjeet ja Lighthouse-tarkistuslistan.
Miten hallita HTTP-välimuistia oikein 2026: Cache-Control-direktiivit, stale-while-revalidate ja CDN-invalidointi Cloudflaressa, Fastlyssa ja Vercelissä. Kaavat, koodit ja mittausohjeet.
Käytännön opas kolmannen osapuolen skriptien optimointiin 2026: mittaus LoAF-API:lla, facade-kuvio raskaille upotuksille, Partytown web workerissa ja bundle-budjetit CI:ssä. Sisältää valmiit koodinäytteet ja verkkokaupasta poimitut mittaustulokset.