Interaction to Next Paint (INP): Stop frustratie door trage knoppen
Gebruiker tikt op "Toevoegen aan winkelwagen". Er gebeurt niets. Nog een keer tikken. Nog steeds niets. Eindelijk, 800 ms later, registreren beide tikken en verschijnen er twee artikelen in de winkelwagen. Gefrustreerd verwijdert de gebruiker het dubbele en overweegt elders te winkelen. Je INP kost je sales.

Wat is INP (en waarom websites erdoor kapot lijken)?
INP meet de vertraging tussen het moment dat iemand klikt, tikt of typt en wanneer hij visuele feedback ziet. Als je knop 600 ms nodig heeft om te reageren, denkt de gebruiker dat hij stuk is.
Het harde deel: INP bijhoudt je slechtste interactie, niet je gemiddelde. Eén trage knop tijdens het afrekenen verpest de metric. Eén traag invoerveld trekt je score omlaag. Google meet het 75e percentiel. Dat betekent dat 25% van je gebruikers een slechte ervaring kan hebben en je nog steeds scoort. Maar die 25% vertrekt.
In tegenstelling tot de vroegere First Input Delay (FID), die alleen de eerste interactie mat, bewaakt INP elke klik, tik en toetsaanslag gedurende het hele bezoek. Het is een veel strenger (en nauwkeuriger) maatstaf voor responsiviteit.
INP-normen
Wat telt als interactie? Klikken, tikken en toetsaanslagen. Hover- en scroll-events worden niet meegenomen in INP-metingen.
De verborgen kosten van trage interacties
Slechte INP irriteert gebruikers niet alleen. Het breekt hun mentale model van hoe websites werken. Als een knop niet direct reageert, gaan ze ervan uit dat de website stuk is, hun internet hapert of dat ze niet goed hebben geklikt. Ze proberen het opnieuw, dubbelklikken en vernieuwen de pagina. Dat maakt de situatie alleen maar erger.
Dubbele bestellingen
Gebruiker klikt op "Bestelling plaatsen", er gebeurt 600 ms niets, hij klikt opnieuw. Nu heb je dubbele bestellingen, boze klanten en restitutieverzoeken.
Formulierabandonment
Typen in een zoekvak met 400 ms vertraging voelt kapot. Bezoekers denken dat het veld dood is, typen opnieuw en lopen tegen dezelfde vertraging aan. Elke poging telt weer mee in je 75e percentiel.
Mobiele gebruikers het hardst geraakt
Over de 1.552 e-commercesites in onze mobiele benchmark is de mediane INP 194 ms, bij een drempel van 200 ms. De helft van de markt zit een paar milliseconden van een onvoldoende af. Op je eigen laptop zie je dat nooit.
Hoe dat er in de praktijk uitziet. We brachten de mobiele INP van Simyo terug van 413 ms naar 150 ms op het 75e percentiel, en op desktop van 102 ms naar 52 ms. Hun UX Score op mobiel ging van 82 naar 100.
De oorzaak lag bij third-party tooling en tracking, niet bij hun eigen applicatiecode. Dat is meestal zo, en precies de reden dat je het niet ziet als je lokaal op een snelle laptop profileert. We vonden de trage interacties met attributiedata uit het veld.
Hetzelfde traject draaiden we bij drie merken van Frasers Group: flannels.com, sportsdirect.com en houseoffraser.co.uk. In drie maanden, zonder replatforming en zonder hun tooling te wijzigen.
INP meten
Velddata (echte gebruikers)
INP kan alleen worden gemeten met echte gebruikersdata. Labtools kunnen het niet vastleggen omdat ze geen echte interacties simuleren:
- Iron/Out gratis benchmark: haal je INP op uit het Chrome UX Report
- Google Search Console: het Core Web Vitals-rapport toont INP per paginagroep
- Real User Monitoring: volg INP voor elke bezoeker met onze observability-implementatie
INP debuggen in het lab
Hoewel je INP niet kunt meten met labtools, kun je wel de oorzaken opsporen:
// Chrome DevTools Performance tab
// 1. Start recording
// 2. Perform the slow interaction
// 3. Stop recording
// 4. Look for:
// - Long tasks (red flags in the timeline)
// - JavaScript execution blocking the main thread
// - Event handler duration
// The Performance tab shows an "Interactions" track
// that highlights slow interactions automaticallyINP programmatisch meten
Installeer de web-vitals library:
npm install web-vitalsTrack vervolgens INP:
import {onINP} from 'web-vitals';
onINP((metric) => {
// Send to analytics
console.log('INP:', metric.value);
// Get the specific interaction that caused this INP
const interaction = metric.entries[0];
console.log('Slow interaction:', {
type: interaction.name, // 'pointerdown', 'click', 'keydown'
target: interaction.target, // DOM element
duration: interaction.duration,
startTime: interaction.startTime
});
});Veelvoorkomende oorzaken van slechte INP
1. Zware JavaScript-uitvoering
Wanneer gebruikers interageren, draaien JavaScript-event handlers op de main thread. Als je handler 400 ms aan JavaScript uitvoert, zal INP minimaal 400 ms zijn.
Fix: Splits lange taken op, stel niet-kritiek werk uit, gebruik web workers voor zware berekeningen.
2. Lange taken die de main thread blokkeren
Als er een lange taak (meer dan 50 ms) loopt wanneer de gebruiker interageert, kan de browser pas reageren als die taak klaar is. Gebruikers wachten terwijl JavaScript draait.
Fix: Geef regelmatig controle terug aan de main thread, splits grote bundles, laad niet-kritieke features lazy.
3. Dure DOM-updates
Nadat je event handler klaar is, moet de browser stijlen, layout en paint herberekenen. Complexe DOM-wijzigingen kosten tijd.
Fix: Minimaliseer DOM-mutaties, batch updates, vermijd forced synchronous layouts en gebruik CSS transforms in plaats van layout-eigenschappen.
4. Scripts van derden
Analytics, advertenties, chatwidgets en A/B-testtools draaien allemaal JavaScript op de main thread. Ze concurreren om verwerkingstijd tijdens interacties.
Fix: Auditeer en verwijder ongebruikte scripts, stel niet-kritieke tags uit, gebruik facades voor zware widgets.
5. Inefficiënte event handlers
Event handlers die te veel werk doen, synchrone netwerkaanvragen maken of cascade-updates veroorzaken, kunnen honderden milliseconden blokkeren.
Fix: Optimaliseer de logica van handlers, debounce/throttle events met hoge frequentie, maak handlers waar mogelijk async.
INP-optimalisatietechnieken
1. Splits lange taken op
De browser kan alleen reageren op interacties tussen taken. Splits langlopende JavaScript op in kleinere stukken om de browser de kans te geven gebruikersinvoer te verwerken.
Geef controle terug aan de main thread
// Bad: Processes all items in one long task
function processItems(items) {
items.forEach(item => {
// Heavy processing
heavyWork(item);
});
}
// Good: Yields after each batch
async function processItems(items) {
for (let i = 0; i < items.length; i++) {
heavyWork(items[i]);
// Yield to browser every 5 items
if (i % 5 === 0) {
await new Promise(resolve => setTimeout(resolve, 0));
}
}
}
// Better: Use scheduler.yield() (experimental)
async function processItems(items) {
for (const item of items) {
heavyWork(item);
if (navigator.scheduling?.isInputPending()) {
await scheduler.yield();
}
}
}Impact: Kan INP met 100–300 ms verlagen op pagina's met veel JavaScript
2. Event handlers optimaliseren
Niet-kritiek werk uitstellen
// Bad: Everything runs immediately
button.addEventListener('click', () => {
updateUI(); // Critical
trackAnalytics(); // Not critical
updateRecommendations(); // Not critical
syncToServer(); // Not critical
});
// Good: Only critical work is synchronous
button.addEventListener('click', () => {
// Immediate visual feedback
updateUI();
// Defer everything else
setTimeout(() => {
trackAnalytics();
updateRecommendations();
syncToServer();
}, 0);
});
// Better: Use requestIdleCallback for non-urgent work
button.addEventListener('click', () => {
updateUI();
requestIdleCallback(() => {
trackAnalytics();
updateRecommendations();
});
// Still defer server sync but with higher priority
setTimeout(() => syncToServer(), 0);
});Dure handlers debouncen
// For search input, filtering, etc.
function debounce(fn, delay) {
let timeoutId;
return function(...args) {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => fn.apply(this, args), delay);
};
}
const searchInput = document.querySelector('#search');
const debouncedSearch = debounce((query) => {
// Expensive search operation
performSearch(query);
}, 300);
searchInput.addEventListener('input', (e) => {
debouncedSearch(e.target.value);
});3. DOM-manipulatie minimaliseren
DOM-updates batchen
// Bad: Multiple layout recalculations
items.forEach(item => {
const element = document.createElement('div');
element.textContent = item.title;
container.appendChild(element); // Layout recalc each time
});
// Good: Build fragment first
const fragment = document.createDocumentFragment();
items.forEach(item => {
const element = document.createElement('div');
element.textContent = item.title;
fragment.appendChild(element);
});
container.appendChild(fragment); // Single layout recalc
// Better: Use innerHTML for large updates (faster)
const html = items.map(item =>
`<div>${item.title}</div>`
).join('');
container.innerHTML = html;Vermijd forced synchronous layouts
// Bad: Reading layout properties forces immediate recalc
element.style.width = '100px';
const height = element.offsetHeight; // Forces layout
element.style.height = height + 'px'; // Another layout
// Good: Read all layout properties first, then write
const width = element.offsetWidth;
const height = element.offsetHeight;
element.style.width = width + 10 + 'px';
element.style.height = height + 10 + 'px';
// Better: Use CSS when possible
element.classList.add('expanded'); // No JavaScript layout thrashing4. Gebruik web workers voor zware berekeningen
// Main thread - stays responsive
const worker = new Worker('/data-processor.js');
button.addEventListener('click', () => {
// Immediate feedback
button.disabled = true;
button.textContent = 'Processing...';
// Heavy work happens off main thread
worker.postMessage({ data: largeDataset });
});
worker.onmessage = (e) => {
// Update UI with results
displayResults(e.data);
button.disabled = false;
button.textContent = 'Process';
};
// data-processor.js (runs in worker)
self.onmessage = (e) => {
const results = heavyComputation(e.data);
self.postMessage(results);
};5. Code splitting en lazy loading
// React/Next.js: Load heavy components only when needed
import dynamic from 'next/dynamic';
const HeavyChart = dynamic(() => import('./HeavyChart'), {
loading: () => <p>Loading chart...</p>
});
// Vanilla JS: Dynamic imports
button.addEventListener('click', async () => {
const { initChart } = await import('./chart-library.js');
initChart(data);
});
// Webpack/Vite: Lazy load routes
const routes = [
{
path: '/dashboard',
component: () => import('./Dashboard.jsx')
}
];Geef eerst visuele feedback, dan pas het werk
INP meet niet hoe lang je code erover doet. INP meet hoe lang de gebruiker naar een onveranderd scherm zit te kijken. De klok stopt bij het eerstvolgende frame dat de browser tekent, niet wanneer je handler klaar is.
Dat verschil is geld waard, want het betekent dat je INP kunt verbeteren zonder iets sneller te maken. Teken eerst iets, doe daarna het werk.
Een bezoeker tikt op "Toevoegen aan winkelwagen" en je handler roept een API aan die 600 ms duurt. Teken je niets tot het antwoord binnen is, dan is je INP 600 ms en tikt de bezoeker nog een keer. Zet je de knop meteen op disabled met een spinner, dan is je INP onder de 100 ms. Die API duurt nog steeds 600 ms. Wat je meet, en wat de bezoeker ervaart, verschillen volledig.
De ergste INP-problemen zitten bijna altijd bij interacties die op het netwerk wachten. Die vertraging kun je vaak niet wegnemen. De stilte eromheen wel.
Drie plekken waar dit direct loont:
- Alles wat het netwerk raakt. Toevoegen aan winkelwagen, filter toepassen, formulier versturen. Toon de wachtstatus vóór het verzoek, niet na het antwoord.
- Menu's en accordions die layoutwerk doen. Teken eerst de open staat, laad de inhoud daarna.
- Validatie in formulieren. Laat direct zien dat het veld gecontroleerd wordt, in plaats van te wachten tot de controle klaar is.
Het patroon is steeds hetzelfde. Knip je handler in tweeën: het deel dat verandert wat de bezoeker ziet, en het deel dat het werk doet. Teken het eerste, geef de browser de kans te renderen, doe dan pas het tweede.
button.addEventListener('click', async () => {
// 1. Meteen tekenen. Dit is wat INP meet.
button.disabled = true;
button.textContent = 'Bezig...';
// 2. Laat de browser eerst renderen.
await new Promise(requestAnimationFrame);
// 3. Nu pas het trage deel, buiten de meting.
await addToCart(sku);
button.textContent = 'Toegevoegd';
});Geavanceerde INP-optimalisatiestrategieën
Controle teruggeven aan de browser met scheduler.yield()
Bij het verwerken van grote datasets geef je periodiek controle terug aan de browser zodat hij gebruikersinteracties kan afhandelen. De scheduler.yield() API is slimmer dan setTimeout omdat het alleen yieldt wanneer nodig.
// Break up long tasks to allow interaction handling
async function processLargeDataset(data) {
for (let i = 0; i < data.length; i++) {
processItem(data[i]);
// Yield every 5 items to let browser handle interactions
if (i % 5 === 0) {
await scheduler.yield();
}
}
onComplete();
}
// Fallback for browsers without scheduler.yield()
async function yieldToMain() {
if ('scheduler' in window && 'yield' in scheduler) {
return scheduler.yield();
}
return new Promise(resolve => setTimeout(resolve, 0));
}
async function processLargeDataset(data) {
for (let i = 0; i < data.length; i++) {
processItem(data[i]);
if (i % 5 === 0) {
await yieldToMain();
}
}
onComplete();
}Waarom dit werkt: Lange taken (meer dan 50 ms) blokkeren de main thread. Door periodiek te yielden, splits je één lange taak op in meerdere kortere taken, waardoor de browser kansen krijgt om klikken, tikken en toetsaanslagen te verwerken tussen de stukjes werk door.
Gebruik content-visibility voor content buiten het scherm
/* Prevent browser from rendering off-screen content */
.article-section {
content-visibility: auto;
contain-intrinsic-size: 1000px; /* Estimated height */
}
/* Reduces layout work during interactions by 50-80%
on pages with lots of off-screen content */React-rendering optimaliseren
// Use React.memo to prevent unnecessary re-renders
const ExpensiveComponent = React.memo(({ data }) => {
return <div>{/* Complex render */}</div>;
});
// Use useCallback for event handlers
const MyComponent = () => {
const handleClick = useCallback(() => {
// Handler logic
}, []); // Dependencies
return <button onClick={handleClick}>Click</button>;
};
// Use useDeferredValue for non-urgent updates
const MyComponent = ({ searchTerm }) => {
const deferredSearchTerm = useDeferredValue(searchTerm);
const results = useMemo(() =>
expensiveSearch(deferredSearchTerm),
[deferredSearchTerm]
);
return <Results data={results} />;
};
// Use startTransition for non-urgent state updates
import { startTransition } from 'react';
const handleSearch = (value) => {
setInputValue(value); // Urgent: update input
startTransition(() => {
setSearchResults(search(value)); // Not urgent
});
};React 19.2+ Activity-component
Het nieuwe <Activity />-component handelt de zichtbaarheid van modals/tabbladen af zonder unmounting, waarbij state bewaard blijft en re-renders worden voorkomen. Zie de React-documentatie voor details.
INP verbeteren in React en Next.js
Concurrent rendering in React bestaat grotendeels om dit probleem op te lossen. Standaard is elke state-update urgent, dus een toetsaanslag die een zware re-render start blokkeert precies het frame dat die toetsaanslag had moeten tonen.
Markeer de zware update als niet-urgent. Met startTransition vertel je React dat de update onderbroken mag worden. Typen blijft dan soepel terwijl een zware gefilterde lijst op de achtergrond opnieuw rendert.
import {useState, useTransition} from 'react';
function ProductFilter({products}) {
const [query, setQuery] = useState('');
const [results, setResults] = useState(products);
const [isPending, startTransition] = useTransition();
function onChange(e) {
// Urgent: het invoerveld moet het teken nu tonen.
setQuery(e.target.value);
// Niet urgent: React mag dit onderbreken.
startTransition(() => {
setResults(filterProducts(products, e.target.value));
});
}
return <input value={query} onChange={onChange} aria-busy={isPending} />;
}useDeferredValue doet hetzelfde wanneer je een waarde binnenkrijgt in plaats van zelf zet. Binnen een componentbibliotheek is dat meestal het geval.
Schrap JavaScript in plaats van het te verschuiven. Concurrent rendering laat een grote bundel beter aanvoelen. Kleiner wordt hij er niet van. In de App Router van Next.js draaien Server Components op de server, zonder JavaScript in de browser. Dat haalt het werk weg in plaats van het uit te stellen. Voor componenten die echt interactief moeten zijn maar niet meteen, gebruik je next/dynamic.
Suspense helpt bij het opstarten. Met selective hydration worden de delen van de pagina waar iemand daadwerkelijk op klikt als eerste interactief, in plaats van te wachten tot de hele boom klaar is.
Deze API's helpen specifiek voor INP. Voor FID doen ze niets, want Google heeft die metric in maart 2024 vervangen. Komt een artikel over React-performance nog met FID-benchmarks, dan dateert het van vóór die wissel en meet het de verkeerde dingen.
Geen idee of dit op jouw site speelt? Onze gratis benchmark laat je INP zien op basis van echte Chrome-gebruikersdata, in ongeveer 30 seconden.
"INP issues detected on your sites" in Search Console
Heb je een mail van Google gehad met als onderwerp "Core Web Vitals INP issues detected on your sites", of zie je die melding in Search Console staan? Dit is wat het betekent, en wat niet.
INP verving FID in maart 2024 als Core Web Vital voor responsiviteit. De melding is dus geen waarschuwing voor iets wat eraan komt. Google zegt dat echte Chrome-gebruikers nu al trage interacties ervaren, op pagina's die al op deze metric beoordeeld worden.

Het rapport groepeert je URL's en geeft ze een van twee niveaus, gelijk aan de INP-drempelwaarden:
- Moet worden verbeterd, oftewel boven de 200 ms
- Slecht, oftewel boven de 500 ms
Mobiel wordt veel vaker gemeld dan desktop, en dat is geen meetfout. Telefoonprocessors zijn trager, dus dezelfde JavaScript blokkeert de main thread langer. Bij veelvoorkomende oorzaken van slechte INP zie je waar die tijd aan opgaat.
Het rapport vertelt je waar, niet waarom
Search Console laat zien welke groepen URL's problemen hebben. Welke interactie, welke handler of welk script de boosdoener is, staat er niet bij. Daarvoor heb je het Performance-paneel in DevTools nodig met throttling aan, of veldmetingen die vastleggen op welk element geklikt werd.

Heb je een fix uitgerold, dan kun je Google vragen die te valideren. Reken wel op geduld. Het rapport draait op CrUX, dat de afgelopen 28 dagen aan veldmetingen gebruikt, dus een fix van vandaag kan pas weken later schoon uitkomen. Precies daarom is het verstandig om daarnaast je eigen real user monitoring te draaien: die laat het effect van een release dezelfde dag zien, en wijst de vertraging toe aan een concreet element in plaats van aan een groep URL's.
INP monitoren over tijd
INP is het Core Web Vital dat het moeilijkst te monitoren is, omdat het varieert per gebruikersgedrag. Verschillende gebruikers starten verschillende interacties.
Problematische interacties tracken
import {onINP} from 'web-vitals';
onINP((metric) => {
// Only report poor INP (> 200ms)
if (metric.value > 200) {
const interaction = metric.entries[0];
// Send detailed context to analytics
analytics.track('poor_inp', {
inp_value: metric.value,
interaction_type: interaction.name,
target_element: interaction.target?.tagName,
target_id: interaction.target?.id,
target_class: interaction.target?.className,
page_url: location.pathname,
user_agent: navigator.userAgent
});
}
});Alerts instellen
- CrUX API: Bewaak je 75e-percentiel INP wekelijks
- RUM-drempelwaartealarmen: Ontvang een melding als INP boven de 200 ms uitkomt voor meer dan 10% van de gebruikers
- Regressiedetectie: Alert bij een INP-stijging van meer dan 50 ms na een deployment
Performance budgets
- INP: Onder 200 ms (75e percentiel)
- JavaScript-bundelgrootte: Onder 300 KB (gecomprimeerd)
- Lange taken: Geen taken boven 200 ms
- Scripts van derden: Maximaal 5 in totaal
INP-optimalisatiechecklist
Nog steeds slechte INP?
INP is het moeilijkst te optimaliseren Core Web Vital. Wij kunnen je interacties profileren, knelpunten identificeren en bewezen fixes implementeren. Neem contact op of voer een gratis benchmark uit om je huidige INP te zien.
Hulp nodig bij het verbeteren van je INP?
Wij identificeren JavaScript-knelpunten en implementeren optimalisaties die je website direct responsief laten aanvoelen.