Core Web Vitals: mitä INP oikeasti mittaa ja miten scheduler.yield() pelastaa pääsäikeen
Core Web Vitals: mitä INP oikeasti mittaa ja miten scheduler.yield() pelastaa pääsäikeen
Mikrointeraktioita käsittelevässä artikkelissa näytin, miksi transform ja opacity animoituvat sulavasti jopa heikolla puhelimella – koska selain käsittelee ne yksinomaan kompositiovaiheessa, erillisellä GPU-säikeellä, koskematta layoutiin. Tämä pitää yhä paikkansa, mutta sen takana piilee oletus, joka kannattaa nyt paljastaa: jotta tämä animaatio ylipäätään käynnistyisi, selaimen täytyy ensin käsitellä klikkaustapahtuma pääsäikeellä – samalla säikeellä, jolla koko JavaScriptisi suoritetaan. Jos pääsäie on klikkaushetkellä varattu suorittamaan pitkää, synkronista tehtävää – suuren taulukon suodattamista, API-vastauksen jäsentämistä, monimutkaisen komponenttipuun renderöintiä – klikkaustapahtumaa ei käsitellä hetkeäkään ennen kuin tuo tehtävä päättyy. Itse animaatio voi olla niin halpa kuin mahdollista. Sillä ei ole väliä, jos kukaan ei ole vielä ehtinyt käynnistää sitä.
Interaction to Next Paint (INP) on Core Web Vitals -mittari, joka mittaa juuri tätä aukkoa – ei aikaa ensimmäiseen tavuun, ei sivun latausaikaa, vaan ajan käyttäjän minkä tahansa painalluksen ja sen hetken välillä, jolloin selain oikeasti maalaa näytölle tuon interaktion vaikutuksen. Se korvasi vanhemman FID-mittarin (First Input Delay) maaliskuussa 2024 yhdestä olennaisesta syystä: FID mittasi vain sivun ensimmäistä interaktiota, joten sivu saattoi saada erinomaisen tuloksen, vaikka jokainen sitä seuraava klikkaus nykisi sekunnin verran. INP ottaa huomioon kaikki interaktiot koko sivun elinkaaren ajalta ja raportoi käytännössä pahimman, toistuvan tapauksen – sitä ei voi "korjata" hyvällä ensivaikutelmalla.
Miksi JavaScriptia ei voi keskeyttää kesken
Keskeinen mekanismi, jota ilman INP:llä ei olisi mieltä: JavaScript toimii selaimessa run-to-completion-mallilla – kerran käynnistetty tehtävä suoritetaan alusta loppuun, ilman keskeytystä, riippumatta siitä, mitä käyttäjä yrittää sillä välin tehdä. Klikkausta tällaisen tehtävän aikana ei jätetä huomiotta – se päätyy jonoon ja odottaa, kunnes säie vapautuu. Yli 50 ms kestävä tehtävä luokitellaan muodollisesti long task-tehtäväksi (Long Tasks API) – tämä ei ole mielivaltainen luku, vaan likiarvo rajasta, jonka ylityttyä käyttäjä alkaa subjektiivisesti tuntea, että jokin "jumittaa" – hengeltään sama kuin mikrointeraktioartikkelin Doherty-kynnys, mutta mitattuna laskennallisen kustannuksen, ei verkon, puolelta.
INP jakautuu kolmeen osatekijään, ja pitkät tehtävät useimmiten paisuttavat ensimmäistä niistä:
- Syöteviive (input delay) – aika klikkauksesta siihen hetkeen, jolloin pääsäie ylipäätään alkaa käsitellä tapahtumaa. Juuri tässä pitkä tehtävä tukkii kaiken.
- Käsittelyaika (processing time) – kuinka kauan tapahtumakäsittelijäsi suoritus todellisuudessa kestää.
- Esitysviive (presentation delay) – aika käsittelijän päättymisestä siihen, kun vaikutuksen sisältävä kehys todella maalataan.
Yielding: miten luovuttaa säie selaimelle kesken työn
Koska tehtävää ei voi keskeyttää ulkopuolelta, ainoa ratkaisu on keskeyttää se tietoisesti sisältä käsin – jakaa pitkä, synkroninen työ pienempiin paloihin ja luovuttaa hallinta selaimelle niiden välissä, jotta se ehtii käsitellä odottavan syötteen ja maalata kehyksen, ennen kuin jatkat seuraavaan työpalaan.
javascript
async function processInChunks(items, processItem) {const results = [];for (let i = 0; i < items.length; i++) {results.push(processItem(items[i]));if (i % 50 === 0) {if ("scheduler" in window && "yield" in scheduler) {await scheduler.yield();} else {// Varmuuskopio: setTimeout(fn, 0), EI Promise.resolve().then()await new Promise((resolve) => setTimeout(resolve, 0));}}}return results;}
Yksityiskohta, joka on helppo ohittaa ja joka todella pilaa tämän tekniikan vaikutuksen: await Promise.resolve() tai queueMicrotask() eivät luovuta hallintaa selaimelle. Ne ovat mikrotehtäviä (microtasks) – mikrotehtäväjono tyhjennetään kokonaan, ennen kuin selain edes harkitsee odottavan syötteen käsittelyä tai kehyksen maalaamista. Jos käytät asynkronisessa silmukassa yksinomaan mikrotehtäviä työn "jakamiseen", selaimen näkökulmasta se on silti yksi, yhtäjaksoinen tehtävä – vain pilkottuna then()-kutsuihin yksinkertaisten koodirivien sijaan. Jotta säie oikeasti luovutetaan, tarvitset makrotehtävän rajan – setTimeout, MessageChannel (nopeampi kuin setTimeout(0), koska se kiertää selainten sisäkkäisille timeouteille asettaman vähimmäisviiveen) tai tähän tarkoitukseen omistetun uuden scheduler.yield()-metodin Scheduler API:sta, joka lisäksi priorisoi jatkon älykkäästi suhteessa muihin odottaviin tehtäviin sen sijaan, että se päätyisi tavallisen jonon perälle kuten setTimeout.
isInputPending(): älä luovuta säiettä, jos kukaan ei odota sitä
Hallinnan luovuttamisella jokaisen yksittäisen silmukan alkion jälkeen on oma kustannuksensa – jokainen kierros tapahtumasilmukan läpi on ylimääräistä kuormaa. Jos kukaan ei ole sillä hetkellä klikannut mitään, työn keskeyttäminen 50 iteraation välein on puhdasta ajanhukkaa, joka pidentää kokonaiskäsittelyaikaa ilman mitään hyötyä käyttäjälle. navigator.scheduling.isInputPending() ratkaisee tämän kysymällä suoraan: odottaako jonossa todella jokin käsittelemätön syötetapahtuma, ennen kuin päätät keskeyttää työn.
javascript
function processQueue(tasks) {while (tasks.length > 0) {if (navigator.scheduling?.isInputPending()) {break; // joku todella odottaa – luovuta säie nyt}doExpensiveWork(tasks.shift());}if (tasks.length > 0) {setTimeout(() => processQueue(tasks), 0);}}
Tämä kääntää logiikan päinvastaiseksi verrattuna jäykkään "joka 50. alkio" -sääntöön: sen sijaan, että arvaisit etukäteen, kuinka paljon työtä on "turvallista" tehdä kahden yieldin välissä, kysyt selaimelta jatkuvasti, onko siihen juuri sillä hetkellä tarvetta. Kun syötejono on tyhjä, silmukka voi jatkaa keskeytyksettä huomattavasti pidempään kuin 50 iteraatiota – kun taas todellinen klikkaus käsittelyn aikana saa sen luovuttamaan hallinnan välittömästi, juuri siellä missä sillä on merkitystä.
Sudenkuopat ja hyvät käytännöt
- Mikrotehtävät eivät lasketa yieldiksi. Tämä on yleisin virhe INP:n "korjaamisyrityksessä" – koodi näyttää asynkroniselta, koska siinä on
await, mutta jos tuonawait-sanan takana on vainPromise.resolve(), selain kohtelee koko juttua yhä yhtenä keskeytymättömänä tehtävänä. scheduler.yield()vaatii yhä varmuuskopion. Tuki rajoittuu pääosin Chromium-pohjaisiin selaimiin – kohtele sitä progressiivisena parannuksena, jossasetTimeouttoimii turvallisena varavaihtoehtona, aivan kuten CSS Houdini -artikkelin Paint API.- Kolmansien osapuolten skriptien pitkät tehtävät lasketaan myös INP-tulokseesi. Analytiikka, mainokset, chat-widgetit – ne elävät samalla pääsäikeellä ja tukkivat sen aivan kuten oma koodisi.
PerformanceObserver, joka kuunteleelongtask-tyyppisiä merkintöjä, näyttää ne kaikki, riippumatta siitä, kuka ne loi – kannattaa mitata todellista tuotantoliikennettä (RUM), ei vain paikallisia testejä tyhjässä, välimuistiin tallennetussa kehitysympäristössä. - INP mittaa pahinta toistuvaa tapausta koko sivun elinkaaren ajalta, ei ensivaikutelmaa. Interaktio, joka nykii vasta kymmenen minuutin sovelluksen käytön jälkeen, kun asiakaspuolen tilaan on kertynyt paljon dataa, lasketaan silti tulokseen – vain muutaman ensimmäisen klikkauksen testaaminen juuri ladatulla sivulla systemaattisesti aliarvioi todellisen ongelman.
Yhteenveto
Mikrointeraktioartikkeli näytti, miten saada itse animaatio olemaan pääsäikeelle ilmainen. INP näyttää saman yhtälön toisen puoliskon: edes maailman halvin animaatio ei auta, jos pääsäie on varattu pitkällä, keskeytymättömällä JavaScript-tehtävällä sillä hetkellä, kun käyttäjä klikkasi. Run-to-completion-malli tekee ainoaksi keinoksi responsiivisuuteen sen, että jaat tietoisesti oman raskaan työsi paloihin ja luovutat hallinnan selaimelle niiden välissä – oikean makrotehtävän, ei mikrotehtävän, kautta, mieluiten silloin kun isInputPending() todella vahvistaa, että joku odottaa sitä. Koko koetun suorituskyvyn ketju, Doherty-kynnyksestä INP:hen, on yhtä vahva kuin sen heikoin lenkki – ja tuo lenkki osoittautuu yhä useammin olevan ei verkko eikä palvelin, vaan oma, liian pitkäksi jäänyt jakamaton JavaScript.