INP-optimering i 2026: Din komplette guide til Interaction to Next Paint

Alt du behøver at vide om Interaction to Next Paint (INP) i 2026. Fra de tre faser og diagnosticering med DevTools og CrUX til avancerede teknikker med scheduler.yield(), Web Workers og framework-specifikke løsninger.

INP-optimering i 2026: Din komplette guide til Interaction to Next Paint

Da Google officielt erstattede First Input Delay (FID) med Interaction to Next Paint (INP) som Core Web Vital tilbage i marts 2024, blev det en wake-up call for rigtig mange webudviklere. INP er nemlig en langt mere krævende metrik end sin forgænger — og i 2026 er det ikke længere nok bare at have en hurtig første interaktion. Hele brugeroplevelsen skal føles responsiv.

I denne guide gennemgår vi alt det vigtige om INP: fra grundlæggende teori til avancerede optimeringsteknikker med konkrete kodeeksempler, du kan bruge med det samme.

Hvad er INP, og hvorfor erstattede den FID?

Interaction to Next Paint (INP) er en Core Web Vital-metrik, der måler den samlede responsivitet af en webside gennem hele dens levetid. Hvor FID kun målte forsinkelsen på den allerførste brugerinteraktion, måler INP alle interaktioner — klik, tryk, tastaturinput — og rapporterer den værste (eller næstværste ved mange interaktioner) som sidens INP-score.

Ærligt talt havde FID nogle ret grundlæggende begrænsninger:

  • Kun første interaktion: FID ignorerede fuldstændigt alle efterfølgende interaktioner. En side kunne have en fantastisk FID-score, men stadig føles langsom, når man navigerede, scrollede eller udfyldte formularer.
  • Kun input delay: FID målte kun ventetiden, før browseren begyndte at behandle hændelsen. Den sagde intet om selve behandlingstiden eller den tid, det tog at male resultatet på skærmen.
  • Urepræsentativ for den virkelige oplevelse: De fleste brugere interagerer med en side mange gange. At kun måle den første interaktion giver et skævt billede.

INP løser alle disse problemer ved at observere latensen af hver eneste interaktion og vælge en repræsentativ høj værdi. For sider med færre end 50 interaktioner vælges den højeste latens. For sider med 50 eller flere interaktioner bruges typisk den 98. percentil — så en enkelt outlier ikke ødelægger det hele.

Sådan måles INP: De tre faser

For at forstå og optimere INP skal du kende de tre faser, der tilsammen udgør en interaktions samlede latens. Det her er grundlæggende viden, som virkelig er værd at have styr på.

1. Input Delay (Inputforsinkelse)

Input delay er tiden fra brugeren interagerer (f.eks. klikker på en knap), til browseren faktisk begynder at udføre de tilhørende event handlers. Forsinkelsen opstår typisk, fordi hovedtråden er optaget af andet arbejde — f.eks. en lang JavaScript-opgave, parsing af et stort script eller en igangværende layout-beregning.

Input delay er ofte den største synder i dårlige INP-scores. Tænk over det: Hvis browseren er midt i en 300ms lang task, når brugeren klikker, bliver klikket bare sat i kø og venter.

2. Processing Time (Behandlingstid)

Processing time er den tid, det tager at udføre selve event handler-koden. Har du registreret en click-handler, en pointerup-handler og en mouseup-handler på det samme element, kører de alle i denne fase. Kompleks logik, tung DOM-manipulation eller synkrone API-kald i handlers øger behandlingstiden direkte.

3. Presentation Delay (Præsentationsforsinkelse)

Presentation delay dækker tiden fra den sidste event handler er færdig, til browseren har malet den næste frame på skærmen. Det inkluderer style-beregninger, layout, paint og compositing. Har event handleren udløst omfattende DOM-ændringer, kan denne fase blive overraskende stor.

Den samlede INP-latens er summen af alle tre faser:

INP = Input Delay + Processing Time + Presentation Delay

For at opnå god INP skal alle tre faser optimeres. Du kan ikke nøjes med kun at fokusere på én af dem.

INP-tærskelværdier

Google definerer tre kategorier for INP-scores:

  • God (Good): Under 200 millisekunder. Interaktioner føles øjeblikkeligt responsive.
  • Behøver forbedring (Needs Improvement): Mellem 200 og 500 millisekunder. Brugeren mærker en forsinkelse, men det er ikke katastrofalt.
  • Dårlig (Poor): Over 500 millisekunder. Interaktioner føles tydeligt langsomme og frustrerende.

Disse tærskelværdier vurderes på 75. percentil af alle sidebesøg. Det vil sige, at 75% af dine brugeres oplevelser skal ligge under 200ms for at opnå en "god" score. Det er en bevidst høj standard.

I praksis bør du sigte mod en INP på under 150ms for at have lidt sikkerhedsmargin. Enheder i den lavere ende af markedet — og dem er der mange af globalt — vil typisk have højere INP-værdier end det, du ser på din egen udviklermaskine.

Diagnosticering af INP-problemer

Før du kan optimere noget som helst, skal du finde ud af, hvor problemerne faktisk ligger. Her er de vigtigste værktøjer og metoder.

Chrome DevTools Performance Panel

Chrome DevTools er stadig det mest detaljerede værktøj til at analysere INP-problemer. Åbn Performance-panelet, aktiver "Web Vitals"-sporet, og optag en session, hvor du interagerer med siden. INP-relevante interaktioner markeres direkte i tidslinjen, og du kan se præcis, hvilke long tasks der forårsager input delay.

I 2026 har Chrome DevTools desuden fået et forbedret INP-spor under "Interactions"-sektionen, hvor du kan se alle tre faser for hver interaktion visualiseret som farvekodede segmenter. Det er ret intuitivt, når man først har prøvet det.

Lighthouse

Lighthouse kan identificere potentielle INP-problemer i et lab-miljø. Kør en Lighthouse-audit med kategorien "Performance", og kig efter anbefalinger relateret til "Total Blocking Time" (TBT), som korrelerer stærkt med INP. Husk dog, at Lighthouse simulerer interaktioner og ikke måler reel INP — det er et diagnostisk værktøj, ikke en feltdatarapport.

Chrome UX Report (CrUX) feltdata

CrUX giver dig rigtige brugerdata fra Chrome-brugere, der har accepteret at dele anonymiseret brugsdata. Du kan tilgå CrUX-data via PageSpeed Insights, BigQuery eller CrUX API. Det er disse data, der repræsenterer, hvad dine faktiske brugere oplever — inklusiv på de langsomme enheder og netværk, som du aldrig ville fange i et lab.

Performance Observer API

For at indsamle INP-data direkte fra dine egne brugere kan du bruge Performance Observer API'en sammen med Googles web-vitals-bibliotek:

import { onINP } from 'web-vitals';

onINP((metric) => {
  console.log('INP:', metric.value, 'ms');
  console.log('Interaktion:', metric.entries);

  // Send data til dit analytics-system
  const body = JSON.stringify({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    delta: metric.delta,
    id: metric.id,
    navigationType: metric.navigationType,
    entries: metric.entries.map(entry => ({
      name: entry.name,
      startTime: entry.startTime,
      duration: entry.duration,
      processingStart: entry.processingStart,
      processingEnd: entry.processingEnd,
      inputDelay: entry.processingStart - entry.startTime,
      processingTime: entry.processingEnd - entry.processingStart,
      presentationDelay: entry.startTime + entry.duration - entry.processingEnd
    }))
  });

  // Brug sendBeacon for pålidelig afsendelse
  navigator.sendBeacon('/analytics/inp', body);
}, { reportAllChanges: true });

Det giver dig granulær kontrol over, hvilke data du indsamler, og du kan nedbryde INP i de tre faser direkte i din analyticsplatform.

Du kan også bruge den rå PerformanceObserver til at overvåge alle interaktioner:

const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // Beregn de tre faser
    const inputDelay = entry.processingStart - entry.startTime;
    const processingTime = entry.processingEnd - entry.processingStart;
    const presentationDelay =
      (entry.startTime + entry.duration) - entry.processingEnd;

    console.table({
      interaktion: entry.name,
      samletLatens: entry.duration,
      inputDelay: inputDelay.toFixed(1),
      processingTime: processingTime.toFixed(1),
      presentationDelay: presentationDelay.toFixed(1)
    });
  }
});

observer.observe({ type: 'event', buffered: true, durationThreshold: 16 });

Optimeringsteknikker

Okay, nu hvor vi ved, hvad INP er, og hvordan vi finder problemerne — lad os dykke ned i de konkrete teknikker til at forbedre din INP-score.

Opdeling af lange opgaver med scheduler.yield()

Den vigtigste enkeltfaktor for dårlig INP er lange opgaver (long tasks) på hovedtråden. En long task er enhver opgave, der tager mere end 50ms. Mens den kører, kan browseren simpelthen ikke reagere på brugerinput.

I 2026 er scheduler.yield() bredt understøttet i moderne browsere, og det er det foretrukne værktøj til at bryde lange opgaver op. I modsætning til ældre teknikker som setTimeout(0) bevarer scheduler.yield() opgavens prioritet i browserkøen. Det betyder, at dit arbejde genoptages hurtigst muligt efter at have givet browseren mulighed for at behandle ventende input.

// Dårligt eksempel: Lang uafbrudt opgave
function processLargeDataset(data) {
  for (let i = 0; i < data.length; i++) {
    heavyComputation(data[i]);  // Blokerer hovedtråden
    updateDOM(data[i]);          // Hele loopet kører uden pauser
  }
}

// Godt eksempel: Opdelt med scheduler.yield()
async function processLargeDataset(data) {
  for (let i = 0; i < data.length; i++) {
    heavyComputation(data[i]);
    updateDOM(data[i]);

    // Giv browseren mulighed for at reagere på input
    // efter hver N-te iteration
    if (i % 10 === 0) {
      await scheduler.yield();
    }
  }
}

Skal du understøtte ældre browsere, kan du bruge en polyfill-strategi:

// Yield-funktion med fallback
async function yieldToMain() {
  if ('scheduler' in globalThis && 'yield' in scheduler) {
    return scheduler.yield();
  }

  // Fallback til setTimeout — bemærk at denne
  // ikke bevarer opgaveprioritet
  return new Promise((resolve) => setTimeout(resolve, 0));
}

// Brug i praksis
async function processItems(items) {
  const BATCH_SIZE = 5;

  for (let i = 0; i < items.length; i++) {
    processItem(items[i]);

    if (i % BATCH_SIZE === 0 && i > 0) {
      await yieldToMain();
    }
  }
}

Optimering af event handlers

Event handlers er den kode, der kører i INP's "processing time"-fase. Jo hurtigere dine handlers er, jo bedre bliver din INP. Her er nogle nøgleteknikker:

Undgå unødvendigt arbejde i handlers:

// Dårligt: Al logik kører synkront i handleren
button.addEventListener('click', () => {
  // Tung beregning direkte i handleren
  const result = analyzeData(largeDataset);
  const html = generateComplexHTML(result);
  container.innerHTML = html;
  trackAnalyticsEvent('button_click', result);
  syncWithServer(result);
});

// Godt: Kun kritisk UI-opdatering i handleren
button.addEventListener('click', async () => {
  // Vis øjeblikkeligt feedback
  button.textContent = 'Behandler...';
  button.disabled = true;

  // Vent på næste frame, så browseren kan male feedback
  await scheduler.yield();

  // Kør beregning efter yield
  const result = analyzeData(largeDataset);
  const html = generateComplexHTML(result);
  container.innerHTML = html;

  // Ikke-kritisk arbejde kan vente
  requestIdleCallback(() => {
    trackAnalyticsEvent('button_click', result);
    syncWithServer(result);
  });
});

Brug debouncing og throttling korrekt:

// Debounce for input-felter (vent til brugeren stopper med at skrive)
function debounce(fn, delay) {
  let timeoutId;
  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn.apply(this, args), delay);
  };
}

// Throttle for scroll/resize (kør højst én gang per interval)
function throttle(fn, interval) {
  let lastTime = 0;
  return function (...args) {
    const now = Date.now();
    if (now - lastTime >= interval) {
      lastTime = now;
      fn.apply(this, args);
    }
  };
}

// Anvendelse
searchInput.addEventListener('input', debounce((e) => {
  performSearch(e.target.value);
}, 300));

document.addEventListener('scroll', throttle(() => {
  updateScrollIndicator();
}, 100));

requestIdleCallback for ikke-kritisk arbejde

requestIdleCallback lader dig planlægge arbejde, der kun udføres, når browseren har ledig tid. Det er perfekt til analytics, præ-caching, ikke-synlige DOM-opdateringer og anden lavprioritetslogik.

// Planlæg ikke-kritisk arbejde i ledige perioder
function scheduleNonCriticalWork(tasks) {
  function processTask(deadline) {
    // Fortsæt så længe der er tid og opgaver
    while (deadline.timeRemaining() > 5 && tasks.length > 0) {
      const task = tasks.shift();
      task();
    }

    // Planlæg resterende opgaver til næste ledige periode
    if (tasks.length > 0) {
      requestIdleCallback(processTask, { timeout: 2000 });
    }
  }

  requestIdleCallback(processTask, { timeout: 2000 });
}

// Brug
scheduleNonCriticalWork([
  () => prefetchNextPageData(),
  () => initializeAnalytics(),
  () => loadNonCriticalImages(),
  () => setupServiceWorker()
]);

Framework-specifikke tilgange

Moderne JavaScript-frameworks har heldigvis deres egne mekanismer til at hjælpe med INP. Lad os se på de vigtigste.

React: useTransition og useDeferredValue

React 18+ introducerede concurrent features, som er direkte relevante for INP-optimering. Og de gør faktisk en mærkbar forskel i praksis:

import { useState, useTransition, useDeferredValue } from 'react';

function SearchComponent() {
  const [query, setQuery] = useState('');
  const [isPending, startTransition] = useTransition();

  function handleSearch(e) {
    const value = e.target.value;

    // Opdater input-feltet øjeblikkeligt (høj prioritet)
    setQuery(value);

    // Marker søgeresultater som en transition (lav prioritet)
    // React kan afbryde denne opdatering for at reagere på input
    startTransition(() => {
      setSearchResults(filterResults(value));
    });
  }

  return (
    <div>
      <input value={query} onChange={handleSearch} />
      {isPending && <span>Søger...</span>}
      <SearchResults />
    </div>
  );
}

// useDeferredValue: Udskyd tunge genrenderinger
function ProductList({ filter }) {
  // React bruger den gamle værdi, mens ny beregning kører
  const deferredFilter = useDeferredValue(filter);

  // Denne liste genrenderes kun, når browseren har tid
  const filteredProducts = useMemo(
    () => products.filter(p => matchesFilter(p, deferredFilter)),
    [deferredFilter]
  );

  return (
    <ul>
      {filteredProducts.map(product => (
        <ProductCard key={product.id} product={product} />
      ))}
    </ul>
  );
}

Vue: v-memo-direktivet

Vue 3's v-memo direktiv forhindrer unødvendige genrenderinger af listeelementer, hvilket direkte reducerer processing time:

<template>
  <div class="product-grid">
    <!-- v-memo sikrer, at hvert element kun genrenderes
         når dets afhængigheder ændres -->
    <div
      v-for="product in products"
      :key="product.id"
      v-memo="[product.id, product.price, selectedId === product.id]"
      class="product-card"
      @click="selectProduct(product.id)"
    >
      <h3>{{ product.name }}</h3>
      <p>{{ product.price }} kr.</p>
      <span v-if="selectedId === product.id">Valgt</span>
    </div>
  </div>
</template>

<script setup>
import { ref, shallowRef } from 'vue';

// Brug shallowRef for store objekter der ikke
// behøver dyb reaktivitet
const products = shallowRef([]);
const selectedId = ref(null);

function selectProduct(id) {
  selectedId.value = id;
}
</script>

Angular: OnPush og @defer

Angular tilbyder flere kraftfulde mekanismer til at skære ned på unødvendigt change detection-arbejde:

// OnPush change detection — komponenten opdateres kun
// når inputs ændres eller events fires i komponenten
import { Component, ChangeDetectionStrategy } from '@angular/core';

@Component({
  selector: 'app-product-list',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="products">
      @for (product of products(); track product.id) {
        <app-product-card [product]="product" />
      }
    </div>

    <!-- @defer udskyder indlæsning af tunge komponenter -->
    @defer (on viewport) {
      <app-product-reviews [productId]="selectedProductId()" />
    } @placeholder {
      <div class="skeleton">Indlæser anmeldelser...</div>
    }
  `
})
export class ProductListComponent {
  products = signal<Product[]>([]);
  selectedProductId = signal<string | null>(null);
}

Angulars @defer-blok (introduceret i Angular 17) er særligt kraftfuld for INP. Den lazy-loader automatisk komponenter og deres afhængigheder baseret på triggers som viewport-synlighed, interaktion eller tomgang. Det er en af de funktioner, jeg synes virkelig gør en forskel i større Angular-applikationer.

Undgå tvungne synkrone layouts (layout thrashing)

Layout thrashing opstår, når du gentagne gange læser og skriver til DOM'en i samme synkrone flow. Det tvinger browseren til at genberegne layout flere gange — og det er dyrt:

// Dårligt: Layout thrashing — tvinger layout-genberegning
// i hver iteration
function resizeElements(elements) {
  elements.forEach(el => {
    // LÆS: Tvinger browser til at beregne layout
    const width = el.offsetWidth;
    // SKRIV: Invaliderer layout
    el.style.width = (width * 1.1) + 'px';
    // Næste iteration: LÆS tvinger endnu en layout-beregning!
  });
}

// Godt: Batch reads og writes separat
function resizeElements(elements) {
  // Fase 1: LÆS alle værdier (én layout-beregning)
  const widths = elements.map(el => el.offsetWidth);

  // Fase 2: SKRIV alle ændringer (én layout-invalidering)
  elements.forEach((el, i) => {
    el.style.width = (widths[i] * 1.1) + 'px';
  });
}

// Endnu bedre: Brug requestAnimationFrame
function resizeElements(elements) {
  const widths = elements.map(el => el.offsetWidth);

  requestAnimationFrame(() => {
    elements.forEach((el, i) => {
      el.style.width = (widths[i] * 1.1) + 'px';
    });
  });
}

Egenskaber der udløser tvungen layout-genberegning inkluderer: offsetWidth, offsetHeight, offsetTop, offsetLeft, clientWidth, clientHeight, scrollWidth, scrollHeight, getComputedStyle() og getBoundingClientRect(). Vær ekstra opmærksom, når du bruger disse i loops eller event handlers.

Code splitting og lazy loading

Jo mindre JavaScript der er indlæst og parsed på hovedtråden, jo mindre er risikoen for at en long task blokerer brugerinteraktion. Code splitting opdeler din applikation i mindre chunks, der indlæses efter behov:

// Dynamisk import — indlæs kun modulet, når det er nødvendigt
async function openSettingsModal() {
  // Vis en loading-indikator øjeblikkeligt
  showLoadingSpinner();

  // Indlæs modulet on-demand
  const { SettingsModal } = await import('./components/SettingsModal.js');

  hideLoadingSpinner();

  const modal = new SettingsModal();
  modal.open();
}

// Route-baseret code splitting i en SPA
const routes = {
  '/': () => import('./pages/Home.js'),
  '/products': () => import('./pages/Products.js'),
  '/checkout': () => import('./pages/Checkout.js'),
  '/account': () => import('./pages/Account.js')
};

// Intersection Observer til lazy loading af komponenter
function lazyLoadComponent(selector, importFn) {
  const target = document.querySelector(selector);
  if (!target) return;

  const observer = new IntersectionObserver(
    async (entries) => {
      if (entries[0].isIntersecting) {
        observer.disconnect();

        const module = await importFn();
        module.init(target);
      }
    },
    { rootMargin: '200px' } // Start indlæsning 200px før synlighed
  );

  observer.observe(target);
}

// Brug
lazyLoadComponent(
  '#product-reviews',
  () => import('./components/ProductReviews.js')
);
lazyLoadComponent(
  '#recommendation-engine',
  () => import('./components/Recommendations.js')
);

Web Workers til tunge beregninger

Web Workers kører JavaScript i en separat tråd, og det eliminerer fuldstændigt blokeringen af hovedtråden. Det er ideelt til databehandling, billedmanipulation, sortering af store datasæt og andre CPU-intensive opgaver:

// main.js — Hovedtråden
class WorkerPool {
  constructor(workerScript, poolSize = navigator.hardwareConcurrency || 4) {
    this.workers = [];
    this.queue = [];
    this.available = [];

    for (let i = 0; i < poolSize; i++) {
      const worker = new Worker(workerScript, { type: 'module' });
      this.workers.push(worker);
      this.available.push(worker);
    }
  }

  async execute(taskData) {
    return new Promise((resolve, reject) => {
      const processTask = (worker) => {
        worker.onmessage = (e) => {
          this.available.push(worker);
          this.processQueue();
          resolve(e.data);
        };
        worker.onerror = (e) => {
          this.available.push(worker);
          this.processQueue();
          reject(e);
        };
        worker.postMessage(taskData);
      };

      if (this.available.length > 0) {
        processTask(this.available.shift());
      } else {
        this.queue.push(processTask);
      }
    });
  }

  processQueue() {
    if (this.queue.length > 0 && this.available.length > 0) {
      const task = this.queue.shift();
      task(this.available.shift());
    }
  }
}

// Brug worker pool til at holde hovedtråden fri
const pool = new WorkerPool('/workers/data-processor.js');

button.addEventListener('click', async () => {
  // Vis feedback øjeblikkeligt
  resultDiv.textContent = 'Beregner...';

  // Kør tung beregning i en worker
  const result = await pool.execute({
    type: 'ANALYZE_SALES_DATA',
    data: salesData
  });

  // Opdater UI med resultatet
  resultDiv.textContent = `Resultat: ${result.summary}`;
});
// workers/data-processor.js — Worker-tråden
self.addEventListener('message', (e) => {
  const { type, data } = e.data;

  switch (type) {
    case 'ANALYZE_SALES_DATA': {
      // Denne tunge beregning blokerer IKKE hovedtråden
      const summary = performHeavyAnalysis(data);
      self.postMessage({ summary });
      break;
    }
  }
});

function performHeavyAnalysis(data) {
  // Kompleks beregning som ville blokere UI
  // hvis den kørte på hovedtråden
  let total = 0;
  let categories = {};

  for (const item of data) {
    total += item.amount;
    categories[item.category] =
      (categories[item.category] || 0) + item.amount;
  }

  return {
    total,
    categories,
    average: total / data.length,
    topCategory: Object.entries(categories)
      .sort(([,a], [,b]) => b - a)[0]?.[0]
  };
}

Feltdata vs. laboratoriedata: Hvorfor feltdata er vigtigst

En af de mest udbredte fejl i INP-optimering er at stole udelukkende på laboratoriedata. Jeg har set det mange gange — teams der optimerer deres Lighthouse-scores, men stadig har dårlig INP i feltet. Der er en fundamental forskel mellem de to tilgange.

Laboratoriedata

Laboratoriedata indsamles i kontrollerede miljøer — typisk via Lighthouse, WebPageTest eller Chrome DevTools på din egen maskine. Fordelene er reproducerbarhed og detaljeret diagnostik. Men ulemperne er ikke til at ignorere:

  • Din udviklermaskine er typisk langt hurtigere end gennemsnittet af dine brugeres enheder.
  • Lab-tests simulerer begrænsede interaktionsmønstre. Reelle brugere interagerer på uforudsigelige måder.
  • Netværksforhold, samtidige browsertabs, enhedens termiske tilstand og andre virkelige faktorer mangler.
  • Lighthouse måler TBT (Total Blocking Time), ikke INP direkte. TBT korrelerer med INP, men de er ikke identiske.

Feltdata (Real User Monitoring — RUM)

Feltdata kommer fra rigtige brugere under virkelige forhold. Og det er disse data, Google bruger til at vurdere Core Web Vitals i søgerangeringen. Det er kort sagt de tal, der tæller.

  • Feltdata repræsenterer den faktiske brugeroplevelse — på tværs af enheder, netværk og geografier.
  • INP i feltet afspejler alle interaktioner, ikke kun simulerede scenarier.
  • Du kan segmentere data efter enhedstype, browser, geografi og sidetype.
  • Feltdata fanger edge cases, som du aldrig ville teste i et lab.

Den bedste strategi er at bruge begge tilgange: Feltdata til at identificere hvad problemet er og dets omfang, laboratoriedata til at diagnosticere hvorfor det opstår og verificere dine løsninger.

Implementer altid en RUM-løsning — hvad enten det er Googles web-vitals-bibliotek med din egen analytics-backend, eller en tredjepartsløsning som DebugBear eller SpeedCurve. Uden feltdata flyver du reelt set blindt.

Praktisk case study: Optimering af en e-commerce produktside

Lad os se på et konkret eksempel. En dansk webshop havde en produktside med en INP på 620ms (p75) — altså langt over den "dårlige" grænse på 500ms. Her er, hvad vi fandt, og hvordan det blev løst.

Problemanalyse

Ved at analysere feltdata fra web-vitals-biblioteket kunne vi se, at de langsomste interaktioner var:

  1. "Tilføj til kurv"-knappen (380ms input delay): Et tredjeparts analytics-script kørte en 400ms long task i baggrunden, der blokerede hovedtråden.
  2. Farve/størrelse-vælgeren (290ms processing time): Når brugeren valgte en ny variant, blev hele produktbilledgalleriet genrenderet synkront — inklusive et dyrt billedkarrusel-bibliotek.
  3. Søgefeltets autocomplete (180ms presentation delay): Autocomplete-dropdown'en udløste layout thrashing ved at læse og skrive DOM-positioner i et loop.

Løsning 1: Eliminering af input delay

// FØR: Tredjeparts script blokerede med en lang task
// analytics-bundle.js kørte 400ms på hovedtråden ved sideload

// EFTER: Defer analytics og kør i idle time
// I <head>: Fjernet sync <script> tag

// I stedet: Indlæs analytics efter interaktivitet
if ('requestIdleCallback' in window) {
  requestIdleCallback(() => {
    const script = document.createElement('script');
    script.src = '/analytics-bundle.js';
    script.async = true;
    document.head.appendChild(script);
  }, { timeout: 5000 });
}

Resultat: Input delay på "Tilføj til kurv" faldt fra 380ms til 42ms.

Løsning 2: Optimering af processing time

// FØR: Synkron genrendering af hele galleriet
function onVariantChange(variantId) {
  const variant = getVariant(variantId);
  const images = variant.images;

  // Dyrt: Destruerer og genopbygger hele karrusellen
  imageCarousel.destroy();
  imageGallery.innerHTML = '';

  images.forEach(img => {
    const el = createImageElement(img);
    imageGallery.appendChild(el);
  });

  imageCarousel.init(imageGallery);
  updatePrice(variant.price);
  updateAvailability(variant.stock);
  updateBreadcrumb(variant.name);
}

// EFTER: Inkrementel opdatering med prioritering
async function onVariantChange(variantId) {
  const variant = getVariant(variantId);

  // Kritisk: Opdater synlige elementer øjeblikkeligt
  updatePrice(variant.price);
  updateAvailability(variant.stock);

  // Giv browseren mulighed for at male prisen
  await scheduler.yield();

  // Opdater billeder inkrementelt i stedet for at
  // destruere hele karrusellen
  imageCarousel.transitionTo(variant.images);

  // Lav-prioritet opdateringer
  await scheduler.yield();
  updateBreadcrumb(variant.name);
  updateRelatedProducts(variant.id);
}

Resultat: Processing time for farve/størrelse-vælger faldt fra 290ms til 65ms.

Løsning 3: Eliminering af layout thrashing

// FØR: Layout thrashing i autocomplete
function positionDropdown(items) {
  items.forEach(item => {
    // LÆS - tvinger layout
    const rect = item.getBoundingClientRect();
    // SKRIV - invaliderer layout
    item.style.left = rect.left + 'px';
    item.style.top = rect.bottom + 'px';
  });
}

// EFTER: Batch reads og writes, brug CSS til positionering
function positionDropdown(inputElement) {
  // Én enkelt læsning
  const inputRect = inputElement.getBoundingClientRect();

  // Brug CSS custom properties i stedet for inline styles
  requestAnimationFrame(() => {
    dropdown.style.setProperty('--dropdown-top',
      `${inputRect.bottom}px`);
    dropdown.style.setProperty('--dropdown-left',
      `${inputRect.left}px`);
    dropdown.style.setProperty('--dropdown-width',
      `${inputRect.width}px`);
  });
}

Resultat: Presentation delay for autocomplete faldt fra 180ms til 28ms.

Samlet resultat

Efter alle optimeringer faldt sidens INP fra 620ms til 135ms (p75) — en forbedring på hele 78%. Siden gik fra "dårlig" til "god" i CrUX-data. Brugerengagementet steg med 12%, og bounce rate faldt med 8% inden for de første 28 dage. Det er tal, der er svære at argumentere imod.

Værktøjer til INP-monitorering

For at holde styr på din INP-score og opdage regressioner tidligt bør du bruge en kombination af værktøjer. Her er en gennemgang af de vigtigste.

DebugBear

DebugBear tilbyder både syntetisk monitorering og RUM med fokus på Core Web Vitals. Platformen viser INP-trends over tid, giver detaljerede breakdowns af de tre faser og kan alertere, når INP overskrider dine tærskelværdier. DebugBear integrerer også CrUX-data, så du kan sammenligne dine egne RUM-data med Googles feltdata.

SpeedCurve

SpeedCurve er et avanceret performance-monitoreringsværktøj, der kombinerer syntetisk testing med RUM. Det giver filmstrimmel-visualiseringer af, hvordan sider loader og reagerer på interaktion. SpeedCurves LUX RUM-script indsamler detaljerede INP-data og kan vise, hvilke specifikke elementer og event handlers der bidrager mest til dårlig INP. Dashboards herfra er i øvrigt særligt gode til at kommunikere performance-data til ikke-tekniske stakeholders.

PageSpeed Insights

Googles PageSpeed Insights (PSI) er nok det nemmeste sted at starte. PSI viser CrUX-feltdata for din URL (hvis tilgængeligt) sammen med en Lighthouse lab-audit. INP vises direkte i feltdatasektionen med den klassiske grøn/gul/rød indikator. PSI er gratis og kræver ingen opsætning — du taster bare URL'en ind. Begrænsningen er dog, at du kun ser aggregerede data og ikke kan dykke ned i individuelle interaktioner.

Google Search Console

Search Console viser Core Web Vitals-status for hele dit websted, grupperet efter URL-grupper med lignende performance. INP-data her er baseret på CrUX og opdateres dagligt. Det er særligt nyttigt til at identificere, hvilke sidetyper (produktsider, kategorisider, blogindlæg) der har INP-problemer, og til at spore forbedringer over tid. Husk bare, at der er en forsinkelse på 28 dage, da CrUX bruger et rullende 28-dages vindue.

Sammenligning og anbefalinger

Her er en hurtig oversigt over, hvornår du bør bruge hvad:

  • Daglig udvikling og debugging: Chrome DevTools Performance Panel + web-vitals-biblioteket i konsollen.
  • Kontinuerlig monitorering og alertering: DebugBear eller SpeedCurve med RUM aktiveret.
  • Hurtig status-check: PageSpeed Insights for en individuel URL.
  • Overordnet site-health og SEO-impact: Google Search Console Core Web Vitals-rapport.
  • CI/CD-integration: Lighthouse CI med budgetter for TBT (som proxy for INP) i din build-pipeline.

Avancerede INP-mønstre for 2026

Ud over de grundlæggende teknikker er der nogle avancerede mønstre, der er blevet særligt relevante her i 2026. Lad os tage dem én for én.

Prioriteret opgaveplanlægning med Scheduler API

Scheduler API'en, som nu er bredt understøttet, giver dig finkornet kontrol over, hvornår opgaver kører:

// Prioriteret opgavehåndtering
button.addEventListener('click', async () => {
  // Synlig feedback — højeste prioritet
  await scheduler.postTask(() => {
    button.classList.add('active');
    showLoadingState();
  }, { priority: 'user-blocking' });

  // Datahentning — middel prioritet
  const data = await scheduler.postTask(() => {
    return fetchProductData(productId);
  }, { priority: 'user-visible' });

  // Analytics — laveste prioritet
  scheduler.postTask(() => {
    trackInteraction('add_to_cart', { productId });
  }, { priority: 'background' });

  // Opdater UI med data
  await scheduler.postTask(() => {
    renderProductDetails(data);
    hideLoadingState();
  }, { priority: 'user-blocking' });
});

Virtualisering af store lister

Hvis din side viser store lister eller tabeller, kan scrolling og klik i listen give høj INP. Virtualisering løser det ved kun at rendere de synlige elementer:

// Simpel virtualiseringsstrategi
class VirtualList {
  constructor(container, items, itemHeight) {
    this.container = container;
    this.items = items;
    this.itemHeight = itemHeight;
    this.visibleCount = Math.ceil(container.clientHeight / itemHeight) + 5;

    // Opret en scrollbar-container med fuld højde
    this.scrollContainer = document.createElement('div');
    this.scrollContainer.style.height =
      `${items.length * itemHeight}px`;
    this.scrollContainer.style.position = 'relative';
    container.appendChild(this.scrollContainer);

    // Lyt efter scroll med throttling
    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
    );

    // Genbrug eksisterende DOM-elementer hvor muligt
    requestAnimationFrame(() => {
      // Opdater kun synlige elementer
      this.updateVisibleItems(startIndex, endIndex);
    });
  }

  updateVisibleItems(start, end) {
    const fragment = document.createDocumentFragment();

    for (let i = start; i < end; i++) {
      const el = this.createItemElement(this.items[i]);
      el.style.position = 'absolute';
      el.style.top = `${i * this.itemHeight}px`;
      fragment.appendChild(el);
    }

    this.scrollContainer.innerHTML = '';
    this.scrollContainer.appendChild(fragment);
  }
}

View Transitions API for jævne overgange

View Transitions API'en kan forbedre den oplevede responsivitet ved at skabe jævne animationer mellem tilstande, mens tung beregning kører i baggrunden:

// Brug View Transitions til at skjule INP-latens
async function navigateToProduct(productId) {
  if (!document.startViewTransition) {
    // Fallback til direkte opdatering
    await updateProductPage(productId);
    return;
  }

  // Start en view transition — browseren fryser den
  // aktuelle visning og animerer til den nye
  const transition = document.startViewTransition(async () => {
    await updateProductPage(productId);
  });

  // Vent på at transitionen er færdig
  await transition.finished;
}

INP-tjekliste: Opsummering af best practices

Til slut er her en samlet tjekliste, du kan bruge som reference, når du optimerer INP på dine sider:

  1. Mål INP i feltet: Implementer web-vitals-biblioteket eller en RUM-løsning. Stol aldrig udelukkende på lab-data.
  2. Identificer de langsomste interaktioner: Brug feltdata til at finde de specifikke interaktioner med højest latens, og nedbryd dem i de tre faser.
  3. Reducer input delay: Eliminer eller udskyd tredjeparts scripts, brug code splitting til at reducere JavaScript på hovedtråden, og flyt tung initialisering til idle time.
  4. Optimer processing time: Hold event handlers lette, brug scheduler.yield() til at bryde lange opgaver op, og flyt tungt arbejde til Web Workers.
  5. Minimer presentation delay: Undgå layout thrashing, batch DOM-opdateringer, og brug CSS-containment og content-visibility til at begrænse rendering-scope.
  6. Brug framework-features: Udnyt React useTransition, Vue v-memo, Angular OnPush og @defer til at reducere unødvendigt render-arbejde.
  7. Test på rigtige enheder: Test på enheder der repræsenterer dine brugere — ikke kun din hurtige udviklermaskine. Brug Chrome DevTools' CPU-throttling til at simulere langsommere hardware.
  8. Monitorér kontinuerligt: Opsæt alerts for INP-regressioner, og gør performance til en del af din CI/CD-pipeline.
  9. Prioritér visuel feedback: Giv brugeren øjeblikkelig feedback ved interaktion (hover-states, loading-indikatorer, disabled-states), selv når det bagvedliggende arbejde tager tid.
  10. Optimer tredjepartskode: Audit alle tredjeparts scripts regelmæssigt. Brug facade-patterns til at lazy-loade tredjeparts widgets, og overvej at flytte tag managers og analytics til Web Workers.

Konklusion

INP er ganske enkelt den mest retvisende metrik for interaktiv responsivitet, vi har haft i web performance-verdenen. I modsætning til FID fanger INP den fulde brugeroplevelse — fra det første klik til den sidste scroll-interaktion. I 2026 er god INP ikke bare et teknisk mål, men en direkte faktor i SEO-rangering, brugerengagement og konverteringsrater.

De vigtigste takeaways er:

  • Forstå de tre faser af INP (input delay, processing time, presentation delay) og optimer dem alle.
  • Brug feltdata som din primære kilde til sandhed — lab-data er til diagnostik, ikke evaluering.
  • Udnyt moderne browser-API'er som scheduler.yield(), Scheduler API og Web Workers til at holde hovedtråden fri.
  • Giv altid øjeblikkelig visuel feedback, selv når bagvedliggende processer tager tid.
  • Gør INP-monitorering til en fast del af din udviklings- og deploymentproces.

God responsivitet sker ikke af sig selv — det kræver bevidst design, omhyggelig implementering og kontinuerlig monitorering. Men med de teknikker og værktøjer, vi har gennemgået her, har du alt, hvad du behøver for at opnå og fastholde en stærk INP-score. Og dine brugere? De vil kunne mærke forskellen.

Om Forfatteren Editorial Team

Our team of expert writers and editors.