Vad är Interaction to Next Paint (INP)?
Interaction to Next Paint (INP) är Googles Core Web Vital-mätvärde som mäter hur responsiv en webbsida upplevs av användaren. Sedan mars 2024 har INP officiellt ersatt First Input Delay (FID) som det primära interaktivitetsmåttet i Core Web Vitals — och ärligt talat var det på tiden. FID mätte bara fördröjningen innan webbläsaren kunde börja bearbeta den allra första interaktionen. INP går betydligt djupare och mäter den totala tiden från att en användare interagerar med sidan till dess att webbläsaren visar nästa visuella uppdatering.
Det som gör INP riktigt användbart är att det övervakar alla klick, tryck och tangentbordsinmatningar under hela sidbesöket och rapporterar den längsta interaktionsfördröjningen (med vissa undantag för extremvärden). Du får alltså en mycket mer realistisk bild av hur responsiv din webbplats faktiskt är.
INP-tröskelvärden
Google definierar tre tröskelvärden för INP:
- Bra: 200 millisekunder eller mindre — användaren upplever sidan som snabb och responsiv
- Behöver förbättras: Mellan 200 och 500 millisekunder — märkbar fördröjning som kan irritera användare
- Dåligt: Över 500 millisekunder — sidan känns trög och i princip oanvändbar
Enligt Chrome User Experience Report (CrUX)-data från 2025 klarar bara ungefär 53 % av alla webbplatser samtliga tre Core Web Vitals-tröskelvärden inklusive INP. På mobil är siffran ännu lägre — bara 44 % av WordPress-sajter klarar alla tre tester. Det är, milt uttryckt, ganska nedslående. Men det betyder också att det finns enormt utrymme för förbättring.
Anatomin bakom en interaktion
För att kunna optimera INP behöver du förstå vad som faktiskt händer under ytan när en användare interagerar med din webbsida. Varje interaktion består av tre distinkta faser:
1. Input Delay (Ingångsfördröjning)
Tiden från att användaren utlöser interaktionen (t.ex. klickar på en knapp) till dess att webbläsaren börjar köra den tillhörande event-hanteraren. Den här fördröjningen uppstår när huvudtråden redan är upptagen med annat — kanske ett tredjepartsskript som malts på, eller en stor renderingsuppgift.
2. Processing Time (Bearbetningstid)
Tiden det tar att faktiskt köra alla event-hanterare som är kopplade till interaktionen. Click-handlers, keydown-handlers och liknande callbacks. Om din event-hanterare innehåller tunga beräkningar, omfattande DOM-manipulation eller komplexa tillståndsuppdateringar kommer den här fasen att ta lång tid.
3. Presentation Delay (Presentationsfördröjning)
Tiden från att event-hanterarna har körts klart till dess att webbläsaren faktiskt visar den visuella uppdateringen på skärmen. Det inkluderar stilberäkningar, layoutarbete, målning och kompositionering.
INP-värdet är summan av dessa tre faser. För att optimera INP effektivt behöver du identifiera vilken fas som bidrar mest — och åtgärda just den.
Identifiera INP-problem: Verktyg och metoder
Innan du börjar optimera behöver du kunna identifiera och mäta INP-problem. Här är de viktigaste verktygen.
Chrome DevTools Performance Panel
Chrome DevTools Performance-panel är det mest kraftfulla verktyget du har till hands. Spela in en profil medan du interagerar med sidan och leta efter Long Tasks som blockerar huvudtråden. Du kan också använda PerformanceObserver för att mäta INP i fält:
// Använd PerformanceObserver för att mäta INP i fält
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.interactionId) {
const duration = entry.duration;
console.log(`Interaktion: ${entry.name}, Varaktighet: ${duration}ms`);
if (duration > 200) {
console.warn(`Långsam interaktion upptäckt: ${duration}ms`);
// Skicka till ditt analytics-system
sendToAnalytics({
metric: 'INP',
value: duration,
element: entry.target?.tagName,
type: entry.name
});
}
}
}
});
observer.observe({ type: 'event', buffered: true, durationThreshold: 16 });
Web Vitals JavaScript-bibliotek
Googles officiella web-vitals-bibliotek gör det enkelt att mäta INP i produktion. Och den bästa delen? Attributeringsinformationen som visar exakt vilken fas av interaktionen som tar längst tid:
import { onINP } from 'web-vitals';
onINP((metric) => {
console.log('INP:', metric.value, 'ms');
console.log('Element:', metric.attribution?.interactionTarget);
console.log('Typ:', metric.attribution?.interactionType);
console.log('Input Delay:', metric.attribution?.inputDelay, 'ms');
console.log('Processing Time:', metric.attribution?.processingDuration, 'ms');
console.log('Presentation Delay:', metric.attribution?.presentationDelay, 'ms');
}, { reportAllChanges: true });
Den där attributeringsinformationen är ovärderlig — den hjälper dig rikta dina optimeringsinsatser precis dit de gör störst skillnad.
Lighthouse och PageSpeed Insights
Lighthouse (version 12+) och PageSpeed Insights visar INP-data från CrUX-rapporten (fältdata) samt identifierar potentiella problem i laboratorietester. Fältdata ger dig en bild av hur riktiga användare upplever din sida, medan labbtester hjälper dig hitta specifika problem att åtgärda.
Optimera Long Tasks: Bryt upp tunga JavaScript-uppgifter
Okej, nu till det riktigt intressanta. Den vanligaste orsaken till dåligt INP är långa uppgifter (Long Tasks) som blockerar huvudtråden. En uppgift som tar mer än 50 millisekunder klassas som en Long Task, och under tiden den körs kan webbläsaren inte svara på användarinteraktioner. Det spelar ingen roll hur snabbt du klickar — webbläsaren sitter fast.
Identifiera Long Tasks
// Övervaka Long Tasks
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn(`Long Task upptäckt: ${entry.duration}ms`);
// Logga vilka skript som bidrog
if (entry.attribution) {
entry.attribution.forEach((attr) => {
console.log(` Källa: ${attr.containerType} - ${attr.containerSrc}`);
});
}
}
});
longTaskObserver.observe({ type: 'longtask', buffered: true });
Strategi 1: Använd scheduler.yield() för att yielda till huvudtråden
scheduler.yield() är ett relativt nytt API som låter dig ge kontroll tillbaka till huvudtråden mitt i en uppgift. Det ger webbläsaren chansen att hantera väntande användarinteraktioner. Till skillnad från äldre metoder som setTimeout prioriterar scheduler.yield() fortsättningen av din uppgift — den hamnar alltså inte sist i kön.
// Utan yield - en lång, blockerande uppgift
function processLargeArray(items) {
for (const item of items) {
heavyComputation(item); // Blockerar huvudtråden
}
}
// Med scheduler.yield() - uppdelad uppgift
async function processLargeArrayOptimized(items) {
for (let i = 0; i < items.length; i++) {
heavyComputation(items[i]);
// Yielda var 5:e iteration för att låta webbläsaren andas
if (i % 5 === 4 && 'scheduler' in window) {
await scheduler.yield();
}
}
}
// Med fallback för webbläsare utan stöd
async function yieldToMain() {
if ('scheduler' in window && 'yield' in scheduler) {
await scheduler.yield();
} else {
// Fallback till setTimeout
await new Promise((resolve) => setTimeout(resolve, 0));
}
}
Webbläsarstöd: I skrivande stund (tidigt 2026) stöds scheduler.yield() i Chrome 129+, Edge 129+ och Firefox. Safari saknar tyvärr fortfarande stöd, så du behöver en fallback-lösning.
Strategi 2: Använd requestAnimationFrame för visuella uppdateringar
När din interaktion involverar visuella uppdateringar kan du använda requestAnimationFrame för att skjuta upp icke-kritiskt arbete till nästa bildram. Det är ett enkelt men effektivt knep:
button.addEventListener('click', () => {
// Kritisk visuell feedback — kör direkt
button.classList.add('active');
showLoadingSpinner();
// Skjut upp tyngre arbete till nästa frame
requestAnimationFrame(() => {
// Icke-kritisk bearbetning
updateAnalytics();
requestAnimationFrame(() => {
// Ännu mer arbete kan skjutas upp ytterligare
recalculateLayout();
});
});
});
Strategi 3: Använd requestIdleCallback för lågprioritetsarbete
requestIdleCallback schemalägger arbete under webbläsarens lediga perioder. En varning dock — om huvudtråden är ständigt upptagen kan schemalägda uppgifter fördröjas rejält, så lita inte på det för tidskritiska saker.
function processNonCriticalWork(tasks) {
function workLoop(deadline) {
// Arbeta så länge det finns tid kvar i den lediga perioden
while (tasks.length > 0 && deadline.timeRemaining() > 1) {
const task = tasks.shift();
task();
}
// Om det finns fler uppgifter, schemalägg en ny ledig-period
if (tasks.length > 0) {
requestIdleCallback(workLoop, { timeout: 2000 });
}
}
requestIdleCallback(workLoop, { timeout: 2000 });
}
// Användning
button.addEventListener('click', () => {
// Omedelbar visuell feedback
updateUI();
// Icke-kritiskt arbete körs när webbläsaren är ledig
processNonCriticalWork([
() => sendAnalyticsEvent('button_click'),
() => prefetchNextPage(),
() => updateRecommendations()
]);
});
Optimera Event Handlers
Tunga event handlers är en av de vanligaste bovarna bakom högt INP. Här är strategier för att göra dem snabbare.
Debouncing och Throttling
För interaktioner som sker i snabb följd — scroll- och input-händelser till exempel — är debouncing och throttling dina bästa vänner:
// Throttle-funktion: kör max en gång per intervall
function throttle(fn, delay) {
let lastCall = 0;
return function (...args) {
const now = Date.now();
if (now - lastCall >= delay) {
lastCall = now;
fn.apply(this, args);
}
};
}
// Debounce-funktion: väntar tills användaren slutar interagera
function debounce(fn, delay) {
let timer;
return function (...args) {
clearTimeout(timer);
timer = setTimeout(() => fn.apply(this, args), delay);
};
}
// Användning med scroll
window.addEventListener('scroll', throttle(() => {
updateScrollProgress();
}, 100));
// Användning med sökinput
searchInput.addEventListener('input', debounce((e) => {
performSearch(e.target.value);
}, 300));
Delegera händelser istället för individuella lyssnare
Att fästa event listeners på hundratals individuella element kan faktiskt bli ganska dyrt. Det ökar minnesanvändningen och fördröjer event-hanteringen. Använd event delegation istället:
// Dåligt: individuella lyssnare på varje element
document.querySelectorAll('.item').forEach((item) => {
item.addEventListener('click', handleItemClick);
});
// Bra: en enda lyssnare på föräldraelementet
document.querySelector('.item-container').addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (item) {
handleItemClick(item);
}
});
Separera visuell feedback från databearbetning
Det här är kanske den viktigaste strategin av alla. Ge omedelbar visuell feedback och skjut upp tyngre arbete till efteråt:
addToCartButton.addEventListener('click', async () => {
// 1. Omedelbar visuell feedback (snabb)
addToCartButton.textContent = 'Tillagd!';
addToCartButton.disabled = true;
cartBadge.textContent = parseInt(cartBadge.textContent) + 1;
// 2. Yielda till huvudtråden
await yieldToMain();
// 3. Tyngre operationer (API-anrop, uppdateringar)
try {
await fetch('/api/cart', {
method: 'POST',
body: JSON.stringify({ productId: product.id })
});
await yieldToMain();
// 4. Uppdatera resten av UI:t
updateCartSummary();
showSuccessNotification();
} catch (error) {
// Återställ UI vid fel
addToCartButton.textContent = 'Lägg i varukorg';
addToCartButton.disabled = false;
cartBadge.textContent = parseInt(cartBadge.textContent) - 1;
showErrorNotification();
}
});
Hantera tredjepartsskript
Tredjepartsskript — analys, reklam, chat-widgets, A/B-testning — är en av de vanligaste orsakerna till dåligt INP. Jag har sett sajter där tredjepartsskript ensamma stod för 70 % av huvudtrådens blockeringstid. De körs ofta på huvudtråden och kan blockera användarinteraktioner utan att du ens märker det.
Strategier för tredjepartsskript
<!-- Dåligt: synkron laddning blockerar rendering -->
<script src="https://analytics.example.com/tracker.js"></script>
<!-- Bättre: async laddar parallellt men kör så snart som möjligt -->
<script async src="https://analytics.example.com/tracker.js"></script>
<!-- Bäst: defer laddar parallellt och kör efter parsing -->
<script defer src="https://analytics.example.com/tracker.js"></script>
Fördröj icke-kritiska skript med Intersection Observer
En smart teknik är att ladda icke-kritiska resurser först när användaren faktiskt behöver dem:
// Ladda chat-widget först när användaren scrollar nedåt
function loadChatWidget() {
const script = document.createElement('script');
script.src = 'https://chat.example.com/widget.js';
document.body.appendChild(script);
}
// Använd Intersection Observer för att trigga laddning
const footer = document.querySelector('footer');
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
loadChatWidget();
observer.disconnect();
}
}, { rootMargin: '200px' });
observer.observe(footer);
Använd Web Workers för tunga beräkningar
Web Workers kör JavaScript i en separat tråd, helt oberoende av huvudtråden. Idealiskt för tunga beräkningar som annars skulle blockera allt:
// main.js
const worker = new Worker('/worker.js');
button.addEventListener('click', () => {
// Omedelbar visuell feedback
showLoadingState();
// Skicka tung beräkning till Worker
worker.postMessage({ type: 'PROCESS_DATA', data: largeDataset });
});
worker.addEventListener('message', (e) => {
if (e.data.type === 'RESULT') {
// Uppdatera UI med resultatet
displayResults(e.data.results);
hideLoadingState();
}
});
// worker.js
self.addEventListener('message', (e) => {
if (e.data.type === 'PROCESS_DATA') {
// Tung beräkning körs utanför huvudtråden
const results = performHeavyCalculation(e.data.data);
self.postMessage({ type: 'RESULT', results });
}
});
Ramverksspecifika optimeringar
Moderna JavaScript-ramverk har inbyggda verktyg för att förbättra INP. Nyckelordet här är att använda dem — det är förvånansvärt vanligt att utvecklare inte utnyttjar dessa funktioner fullt ut.
React
React 18+ har useTransition och useDeferredValue, som är designade just för att hålla gränssnittet responsivt under tunga uppdateringar:
import { useTransition, useDeferredValue, memo } from 'react';
function SearchComponent() {
const [query, setQuery] = useState('');
const [isPending, startTransition] = useTransition();
// useDeferredValue fördröjer icke-kritiska uppdateringar
const deferredQuery = useDeferredValue(query);
const handleSearch = (e) => {
// Omedelbar uppdatering av input-fältet
setQuery(e.target.value);
// Fördröjd uppdatering av sökresultaten
startTransition(() => {
setSearchResults(filterResults(e.target.value));
});
};
return (
<div>
<input
type="text"
value={query}
onChange={handleSearch}
placeholder="Sök..."
/>
{isPending && <span>Söker...</span>}
<SearchResults query={deferredQuery} />
</div>
);
}
// Undvik onödiga omrenderingar med memo
const SearchResults = memo(function SearchResults({ query }) {
const results = useMemo(() => filterResults(query), [query]);
return (
<ul>
{results.map((result) => (
<li key={result.id}>{result.title}</li>
))}
</ul>
);
});
Vue 3
<template>
<div>
<input v-model="query" @input="handleSearch" placeholder="Sök..." />
<div v-if="isSearching">Söker...</div>
<!-- v-memo förhindrar omrendering om inget ändrats -->
<div v-for="item in results" :key="item.id" v-memo="[item.id, item.updated]">
{{ item.title }}
</div>
</div>
</template>
<script setup>
import { ref, computed, defineAsyncComponent } from 'vue';
const query = ref('');
const isSearching = ref(false);
// Asynkron komponent laddas vid behov
const HeavyChart = defineAsyncComponent(() =>
import('./HeavyChart.vue')
);
// Computed med caching
const results = computed(() => {
return filterResults(query.value);
});
const handleSearch = debounce((e) => {
isSearching.value = true;
// Bearbeta sökning
nextTick(() => {
isSearching.value = false;
});
}, 250);
</script>
Angular
Angular har flera inbyggda mekanismer. OnPush change detection strategy är en av de enklaste vinsterna du kan göra:
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-product-list',
// OnPush minskar antalet change detection-cykler
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div *ngFor="let product of products; trackBy: trackByProductId">
{{ product.name }}
</div>
<!-- @defer laddar komponenten lazy -->
@defer (on viewport) {
<app-reviews [productId]="selectedProductId" />
} @placeholder {
<div>Laddar recensioner...</div>
}
`
})
export class ProductListComponent {
// trackBy förhindrar onödiga DOM-uppdateringar
trackByProductId(index: number, product: Product) {
return product.id;
}
}
Optimera DOM-storlek och rendering
En stor DOM kan avsevärt försämra INP. Varje gång webbläsaren behöver göra stilberäkningar, layout eller målning måste den traversera DOM-trädet, och ju större det är desto längre tid tar det. Jag har sett sidor med 5 000+ DOM-noder där enbart stilberäkningar tog 40–50 ms.
content-visibility: auto
CSS-egenskapen content-visibility: auto är ett av de mest underskattade verktygen för INP-optimering. Den talar om för webbläsaren att den kan hoppa över rendering av element som inte syns i viewporten:
/* Hoppa över rendering av element utanför viewporten */
.article-section {
content-visibility: auto;
contain-intrinsic-size: auto 500px; /* Uppskattat storlek för layoutstabilitet */
}
/* Specifikt för långa listor */
.list-item {
content-visibility: auto;
contain-intrinsic-size: auto 80px;
}
/* Kombinera med CSS containment */
.card {
contain: layout style paint;
}
Virtualisering av långa listor
Om din sida visar hundratals eller tusentals element i en lista bör du bara rendera de som syns i viewporten. I produktion finns det utmärkta bibliotek som react-window eller @tanstack/virtual, men här är en enkel implementation som visar principen:
// Enkel virtualisering utan bibliotek
class VirtualList {
constructor(container, items, itemHeight) {
this.container = container;
this.items = items;
this.itemHeight = itemHeight;
this.visibleCount = Math.ceil(container.clientHeight / itemHeight) + 2;
this.container.style.overflow = 'auto';
this.container.style.position = 'relative';
// Skapa en spacer för total scrollhöjd
this.spacer = document.createElement('div');
this.spacer.style.height = `${items.length * itemHeight}px`;
this.container.appendChild(this.spacer);
this.container.addEventListener('scroll',
throttle(() => this.render(), 16)
);
this.render();
}
render() {
const scrollTop = this.container.scrollTop;
const startIndex = Math.floor(scrollTop / this.itemHeight);
const endIndex = Math.min(startIndex + this.visibleCount, this.items.length);
// Rensa befintliga element (utom spacer)
const existingItems = this.container.querySelectorAll('.virtual-item');
existingItems.forEach((el) => el.remove());
// Rendrera synliga element
for (let i = startIndex; i < endIndex; i++) {
const el = document.createElement('div');
el.className = 'virtual-item';
el.style.position = 'absolute';
el.style.top = `${i * this.itemHeight}px`;
el.style.height = `${this.itemHeight}px`;
el.textContent = this.items[i];
this.container.appendChild(el);
}
}
}
CSS-optimeringar som påverkar INP
CSS kan ha en överraskande stor påverkan på INP — det är inte bara JavaScript som är boven. Komplexa selektorer och layout-triggers kan vara minst lika problematiska.
Undvik layout thrashing
Layout thrashing uppstår när du blandar DOM-läsningar och -skrivningar i en loop. Webbläsaren tvingas räkna om layouten mellan varje steg, och det blir riktigt dyrt:
// Dåligt: Layout thrashing — läs-skriv-cykler i loop
function resizeElements(elements) {
elements.forEach((el) => {
const width = el.offsetWidth; // Framtvingar layout (läs)
el.style.width = width * 2 + 'px'; // Invaliderar layout (skriv)
});
}
// Bra: Batcha läsningar och skrivningar
function resizeElementsOptimized(elements) {
// Läs alla värden först
const widths = elements.map((el) => el.offsetWidth);
// Skriv alla värden sedan
elements.forEach((el, i) => {
el.style.width = widths[i] * 2 + 'px';
});
}
// Bäst: Använd requestAnimationFrame
function resizeElementsBest(elements) {
const widths = elements.map((el) => el.offsetWidth);
requestAnimationFrame(() => {
elements.forEach((el, i) => {
el.style.width = widths[i] * 2 + 'px';
});
});
}
Använd CSS-transforms istället för layout-egenskaper
Det här är ett klassiskt tips men fortfarande relevant. Animeringar med transform och opacity hanteras av GPU:n och triggar inte layout:
/* Dåligt: Trigger layout + paint */
.animate-bad {
transition: left 0.3s, top 0.3s, width 0.3s;
}
/* Bra: Enbart compositing — kör på GPU */
.animate-good {
transition: transform 0.3s, opacity 0.3s;
will-change: transform;
}
Mätning och övervakning i produktion
Att optimera INP är inte en engångsinsats. Du behöver kontinuerlig övervakning för att säkerställa att förbättringarna faktiskt håller och att nya regressioner fångas tidigt. Jag rekommenderar starkt att sätta upp någon form av Real User Monitoring (RUM) — det är den enda vägen att se vad riktiga användare upplever.
Implementera en komplett INP-övervakningslösning
import { onINP, onLCP, onCLS } from 'web-vitals';
class WebVitalsMonitor {
constructor(endpoint) {
this.endpoint = endpoint;
this.buffer = [];
this.flushInterval = 5000; // Skicka var 5:e sekund
this.init();
}
init() {
onINP((metric) => this.collectMetric('INP', metric));
onLCP((metric) => this.collectMetric('LCP', metric));
onCLS((metric) => this.collectMetric('CLS', metric));
// Periodisk sändning
setInterval(() => this.flush(), this.flushInterval);
// Skicka kvarvarande data vid sidstängning
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.flush();
}
});
}
collectMetric(name, metric) {
this.buffer.push({
name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
page: window.location.pathname,
userAgent: navigator.userAgent,
timestamp: Date.now(),
// INP-specifik attribution
...(name === 'INP' && metric.attribution ? {
interactionTarget: metric.attribution.interactionTarget,
interactionType: metric.attribution.interactionType,
inputDelay: metric.attribution.inputDelay,
processingDuration: metric.attribution.processingDuration,
presentationDelay: metric.attribution.presentationDelay
} : {})
});
}
flush() {
if (this.buffer.length === 0) return;
const data = [...this.buffer];
this.buffer = [];
// Använd sendBeacon för tillförlitlig leverans
if (navigator.sendBeacon) {
navigator.sendBeacon(this.endpoint, JSON.stringify(data));
} else {
fetch(this.endpoint, {
method: 'POST',
body: JSON.stringify(data),
keepalive: true
});
}
}
}
// Initialisera
const monitor = new WebVitalsMonitor('/api/vitals');
Mobilspecifika INP-utmaningar
Mobilanvändare utgör en särskild utmaning för INP, och ärligt talat är det på mobil som de flesta sajter verkligen kämpar. Processorer i mobiltelefoner är betydligt svagare än i stationära datorer, och användare har ofta sämre nätverksanslutning.
Varför mobil INP ofta är sämre
Det finns flera anledningar:
- Svagare processorer: En genomsnittlig mobiltelefon har ungefär 3–5 gånger svagare CPU-prestanda jämfört med en stationär dator. En JavaScript-uppgift som tar 30 ms på en dator kan ta 90–150 ms på en mobil — och plötsligt är den en Long Task.
- Begränsat minne: Mobilenheter har mindre RAM, vilket kan leda till att webbläsaren frigör minne oftare (garbage collection), vilket pausar JavaScript-exekveringen.
- Thermal throttling: När mobiltelefonen blir varm drosslas CPU:n. Det är lätt att glömma, men det kan ha enorm påverkan under längre sessioner.
- Touch-interaktioner: Touch-events genererar fler händelser än musklick, och webbläsaren måste vänta ~300 ms för att avgöra om det är ett dubbelklick (om inte
touch-actionär satt).
Optimeringsstrategier specifikt för mobil
/* Eliminera 300ms touch delay */
html {
touch-action: manipulation;
}
/* Reducera komplexitet för mobila skärmar */
@media (max-width: 768px) {
/* Förenkla animationer */
.complex-animation {
animation: none;
transition: opacity 0.15s ease;
}
/* Minska DOM-storlek med content-visibility */
.sidebar,
.related-articles,
.comments-section {
content-visibility: auto;
contain-intrinsic-size: auto 300px;
}
}
Du kan också anpassa bearbetningen baserat på enhetens kapacitet — ge svagare enheter mindre arbete åt gången:
// Anpassa bearbetning baserat på enhetens kapacitet
function getDeviceCapability() {
// Kontrollera antal logiska processorkärnor
const cores = navigator.hardwareConcurrency || 2;
// Kontrollera tillgängligt minne (Chrome-specifikt)
const memory = navigator.deviceMemory || 4;
if (cores <= 2 || memory <= 2) return 'low';
if (cores <= 4 || memory <= 4) return 'medium';
return 'high';
}
// Anpassa batchstorlek baserat på enhetens kapacitet
async function processItemsAdaptive(items) {
const capability = getDeviceCapability();
const batchSize = capability === 'low' ? 3 :
capability === 'medium' ? 8 : 20;
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
batch.forEach((item) => processItem(item));
// Yielda oftare på svagare enheter
if (i + batchSize < items.length) {
await yieldToMain();
}
}
}
Praktiskt exempel: Optimera en e-handelssida
Så, låt oss dyka ner i ett realistiskt scenario. Tänk dig en produktlistningssida i en e-handelsbutik som lider av högt INP. Det är ett vanligt problem — filterknappen blockerar huvudtråden i hundratals millisekunder varje gång den klickas. Vi identifierar problemen och åtgärdar dem steg för steg.
Innan optimering: Det problematiska tillståndet
// Problem: Allt körs synkront i event-hanteraren
filterButton.addEventListener('click', () => {
// 1. Hämta alla filterval (snabbt)
const filters = getSelectedFilters();
// 2. Filtrera 5000 produkter — LÅNGSAMT (~120ms)
const filtered = products.filter((product) => {
return matchesAllFilters(product, filters);
});
// 3. Sortera resultaten — LÅNGSAMT (~80ms)
filtered.sort((a, b) => {
return complexSortFunction(a, b, sortOrder);
});
// 4. Rendrera alla produkter — LÅNGSAMT (~200ms)
productContainer.innerHTML = '';
filtered.forEach((product) => {
productContainer.appendChild(createProductCard(product));
});
// 5. Uppdatera filterräknare
updateFilterCounts(filtered);
// 6. Skicka analytics
trackFilterEvent(filters, filtered.length);
});
// Total blockeringstid: ~400ms+ = DÅLIGT INP
Efter optimering: Den förbättrade versionen
// Lösning: Separera visuell feedback, yielda, och använd Web Worker
const filterWorker = new Worker('/filter-worker.js');
filterButton.addEventListener('click', async () => {
// 1. OMEDELBAR visuell feedback (< 5ms)
showFilterLoadingState();
filterButton.disabled = true;
// 2. Skicka filtrering och sortering till Web Worker
const filters = getSelectedFilters();
filterWorker.postMessage({
type: 'FILTER_AND_SORT',
products,
filters,
sortOrder
});
});
filterWorker.addEventListener('message', async (e) => {
const { filtered } = e.data;
// 3. Rendrera i batchar med yielding
productContainer.innerHTML = '';
const batchSize = 20;
for (let i = 0; i < filtered.length; i += batchSize) {
const batch = filtered.slice(i, i + batchSize);
const fragment = document.createDocumentFragment();
batch.forEach((product) => {
fragment.appendChild(createProductCard(product));
});
productContainer.appendChild(fragment);
// Yielda efter varje batch om det finns fler
if (i + batchSize < filtered.length) {
await yieldToMain();
}
}
// 4. Dölj laddningstillståndet
hideFilterLoadingState();
filterButton.disabled = false;
// 5. Icke-kritiskt arbete i idle-tid
requestIdleCallback(() => {
updateFilterCounts(filtered);
trackFilterEvent(getSelectedFilters(), filtered.length);
});
});
// filter-worker.js
self.addEventListener('message', (e) => {
const { products, filters, sortOrder } = e.data;
const filtered = products
.filter((product) => matchesAllFilters(product, filters))
.sort((a, b) => complexSortFunction(a, b, sortOrder));
self.postMessage({ filtered });
});
Resultatet? Istället för att blockera huvudtråden i 400+ millisekunder ger vi omedelbar visuell feedback (under 5 ms), flyttar den tunga beräkningen till en Web Worker, renderar resultaten i batchar, och schemalägger icke-kritiskt arbete under idle-tid. INP-värdet går från 400+ ms till under 50 ms. Det är en enorm skillnad för användarupplevelsen.
Felsökning av svåra INP-problem
Ibland är INP-problemen inte uppenbara. Kanske ser allt bra ut i labbet men fältdatan berättar en annan historia. Här är tekniker för att spåra svårfångade problem.
Använd Long Animation Frames (LoAF) API
Long Animation Frames (LoAF) är en nyare API som ger betydligt mer detaljerad information än Long Tasks API. Den visar inte bara att en lång bildram inträffade, utan också exakt vilka skript och funktioner som bidrog:
// LoAF ger djupare insikter än Long Tasks
const loafObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.duration > 50) {
console.group(`Long Animation Frame: ${entry.duration}ms`);
console.log('Blockering:', entry.blockingDuration, 'ms');
console.log('Render start:', entry.renderStart);
// Detaljerad skriptinformation
entry.scripts.forEach((script) => {
console.log(` Skript: ${script.invokerType}`);
console.log(` Källa: ${script.sourceURL}:${script.sourceFunctionName}`);
console.log(` Varaktighet: ${script.duration}ms`);
console.log(` Exekveringsstart: ${script.executionStart}`);
});
console.groupEnd();
}
}
});
loafObserver.observe({ type: 'long-animation-frame', buffered: true });
LoAF API stöds i Chrome 123+ och är ovärderligt för att hitta de specifika funktionerna och skripten som orsakar INP-problem. Genom att kombinera LoAF-data med web-vitals-bibliotekets attribution kan du bygga en komplett bild av vad som händer under varje interaktion — och exakt var i din kodbas problemet sitter.
Checklista: INP-optimering steg för steg
Här är en systematisk checklista du kan följa. Börja uppifrån — de första punkterna ger oftast störst effekt:
- Mät först: Använd
web-vitals-biblioteket och Chrome DevTools för att identifiera de långsammaste interaktionerna och vilka faser (input delay, processing, presentation) som bidrar mest. - Identifiera Long Tasks: Använd Performance-panelen i DevTools för att hitta JavaScript-uppgifter som tar mer än 50 ms.
- Bryt upp tunga uppgifter: Använd
scheduler.yield(),requestAnimationFrame, ellersetTimeoutför att dela upp långa uppgifter i mindre bitar. - Ge omedelbar visuell feedback: Visa laddningsindikatorer eller tillståndsbyte direkt vid interaktion, innan tung bearbetning börjar.
- Flytta tunga beräkningar till Web Workers: Stora datamängder, sortering och filtrering bör ske utanför huvudtråden.
- Hantera tredjepartsskript: Ladda icke-kritiska skript med
defereller dynamiskt, och överväg att fördröja dem tills användaren interagerat med sidan. - Optimera DOM-storlek: Använd
content-visibility: autooch virtualisering för långa listor. - Undvik layout thrashing: Batcha DOM-läsningar och -skrivningar, och använd CSS-transforms istället för layout-egenskaper för animationer.
- Använd ramverksspecifika verktyg: React
useTransition, Vuev-memo, AngularOnPush— de finns där av en anledning, använd dem. - Övervaka kontinuerligt: Sätt upp Real User Monitoring (RUM) i produktion för att fånga regressioner tidigt.
Sammanfattning och framtidsutsikter
INP har blivit det mest kritiska prestandamåttet för interaktivitet på webben 2026. Med bara hälften av alla webbplatser som klarar Core Web Vitals-tröskelvärdena finns det enormt utrymme för förbättring. Sajter som investerar i INP-optimering belönas med bättre sökrankning, högre konverteringsgrad och (kanske viktigast av allt) nöjdare användare.
De viktigaste strategierna att fokusera på:
- Bryt upp Long Tasks med
scheduler.yield()och andra yielding-tekniker - Ge omedelbar visuell feedback genom att separera UI-uppdateringar från tung bearbetning
- Flytta tunga beräkningar till Web Workers för att avlasta huvudtråden
- Hantera tredjepartsskript genom lazy loading och fördröjd laddning
- Mät och övervaka INP kontinuerligt med fältdata från riktiga användare
Framöver kan vi förvänta oss ytterligare förbättringar i webbläsar-API:er för schemaläggning. scheduler.yield() är bara början — framtida versioner av Prioritized Task Scheduling API kommer att ge utvecklare ännu finare kontroll över hur arbete fördelas på huvudtråden.
Men oavsett vilka verktyg och tekniker som kommer härnäst är grundprincipen densamma: respektera användarens tid, håll huvudtråden fri, och ge alltid omedelbar feedback på interaktioner. Det är nyckeln till en snabb och responsiv webbupplevelse.