W marcu 2024 roku Google oficjalnie zastąpił First Input Delay (FID) nową metryką Interaction to Next Paint (INP) jako część Core Web Vitals. INP lepiej odzwierciedla rzeczywistą responsywność strony i jest trudniejsza do optymalizacji. Ten przewodnik wyjaśnia różnice i pokazuje jak poprawić wyniki.

Czym było FID?

First Input Delay mierzył czas od pierwszej interakcji użytkownika (kliknięcie, tap) do momentu, gdy przeglądarka mogła rozpocząć obsługę tego zdarzenia. Mierzył tylko pierwszą interakcję.

Ograniczenia FID

  1. Tylko pierwsza interakcja - ignorował wszystkie kolejne
  2. Tylko opóźnienie - nie mierzył czasu przetwarzania
  3. Łatwy do optymalizacji - wystarczyło szybkie pierwsze ładowanie
  4. Nie odzwierciedlał rzeczywistości - strona mogła być wolna po pierwszej interakcji

Czym jest INP?

Interaction to Next Paint mierzy responsywność strony przez cały czas jej użytkowania. Bierze pod uwagę wszystkie interakcje (kliknięcia, tappy, naciśnięcia klawiszy) i raportuje najgorszą z nich (technicznie: 98. percentyl).

Co mierzy INP?

INP mierzy czas od interakcji do momentu wyrenderowania następnej klatki:

INP = Input Delay + Processing Time + Presentation Delay
  1. Input Delay - czas oczekiwania na rozpoczęcie obsługi (jak FID)
  2. Processing Time - czas wykonania event handlerów
  3. Presentation Delay - czas renderowania zmian w DOM

Progi INP

WynikOcena
≤ 200msDobry (zielony)
200ms - 500msWymaga poprawy (pomarańczowy)
> 500msSłaby (czerwony)

Porównanie FID vs INP

AspektFIDINP
Co mierzyTylko opóźnienieOpóźnienie + przetwarzanie + renderowanie
Które interakcjeTylko pierwsząWszystkie (raportuje najgorszą)
Progi≤100ms dobry≤200ms dobry
Trudność optymalizacjiNiskaWysoka
StatusWycofany (marzec 2024)Aktualny Core Web Vital

Jak mierzyć INP?

Dane laboratoryjne

Lighthouse (Chrome DevTools):

  • INP nie jest bezpośrednio mierzony w Lighthouse
  • Użyj Total Blocking Time (TBT) jako proxy

Chrome DevTools - Performance:

  1. Otwórz Performance panel
  2. Nagrywaj podczas interakcji z stroną
  3. Szukaj długich tasków w Main thread

Dane rzeczywiste (Field data)

web-vitals library:

import { onINP } from 'web-vitals';

onINP((metric) => {
  console.log('INP:', metric.value);
  console.log('Rating:', metric.rating); // good, needs-improvement, poor
  console.log('Entries:', metric.entries); // Szczegóły interakcji
});

Google Search Console:

  • Core Web Vitals report pokazuje INP z danych CrUX

PageSpeed Insights:

  • Sekcja “Diagnoza problemów z wydajnością” pokazuje INP

Chrome DevTools - Web Vitals overlay

  1. Otwórz DevTools
  2. Naciśnij Ctrl+Shift+P
  3. Wpisz “Show Web Vitals”
  4. Interakcja ze stroną pokaże INP w czasie rzeczywistym

Przyczyny słabego INP

1. Długie taski JavaScript

JavaScript blokuje main thread. Każdy task >50ms to “long task” wpływający na INP.

// Źle - długi synchroniczny task
function processData(items) {
  items.forEach(item => {
    // Ciężkie obliczenia...
    heavyComputation(item);
  });
}

// Dobrze - podziel na mniejsze taski
async function processDataAsync(items) {
  for (const item of items) {
    heavyComputation(item);
    // Oddaj kontrolę przeglądarce
    await scheduler.yield();
  }
}

// Alternatywnie z setTimeout
function processDataChunked(items, chunkSize = 10) {
  let index = 0;

  function processChunk() {
    const chunk = items.slice(index, index + chunkSize);
    chunk.forEach(heavyComputation);
    index += chunkSize;

    if (index < items.length) {
      setTimeout(processChunk, 0);
    }
  }

  processChunk();
}

2. Ciężkie event handlery

// Źle - ciężki handler blokuje rendering
button.addEventListener('click', () => {
  // 500ms obliczeń...
  processLargeDataset();
  updateUI();
});

// Dobrze - oddziel obliczenia od UI
button.addEventListener('click', async () => {
  // Natychmiast pokaż feedback
  button.disabled = true;
  showSpinner();

  // Obliczenia w następnym tasku
  await scheduler.yield();
  const result = processLargeDataset();

  // Update UI
  await scheduler.yield();
  updateUI(result);
  hideSpinner();
  button.disabled = false;
});

3. Zbyt wiele re-renderów

// Źle - każda zmiana triggeruje reflow
items.forEach(item => {
  const div = document.createElement('div');
  div.textContent = item.name;
  container.appendChild(div); // Reflow!
});

// Dobrze - batch DOM operations
const fragment = document.createDocumentFragment();
items.forEach(item => {
  const div = document.createElement('div');
  div.textContent = item.name;
  fragment.appendChild(div);
});
container.appendChild(fragment); // Jeden reflow

4. Layout thrashing

// Źle - wielokrotne odczyty i zapisy
elements.forEach(el => {
  const height = el.offsetHeight; // Odczyt (force layout)
  el.style.height = height + 10 + 'px'; // Zapis (invalidate layout)
});

// Dobrze - najpierw odczytaj, potem zapisz
const heights = elements.map(el => el.offsetHeight);
elements.forEach((el, i) => {
  el.style.height = heights[i] + 10 + 'px';
});

5. Third-party scripts

Skrypty firm trzecich (analytics, ads, chat widgets) często mają ciężkie handlery.

Techniki optymalizacji INP

1. Scheduler API (scheduler.yield)

Nowe API pozwalające oddać kontrolę przeglądarce:

async function handleClick() {
  // Krok 1
  await scheduler.yield();

  // Krok 2
  await scheduler.yield();

  // Krok 3
}

// Polyfill dla starszych przeglądarek
if (!('scheduler' in window)) {
  window.scheduler = {
    yield: () => new Promise(resolve => setTimeout(resolve, 0))
  };
}

2. requestIdleCallback

Wykonuj mniej pilne taski gdy przeglądarka jest idle:

// Przetwarzanie w tle
function processInBackground(items) {
  let index = 0;

  function processNext(deadline) {
    while (index < items.length && deadline.timeRemaining() > 0) {
      processItem(items[index]);
      index++;
    }

    if (index < items.length) {
      requestIdleCallback(processNext);
    }
  }

  requestIdleCallback(processNext);
}

3. requestAnimationFrame

Synchronizuj zmiany wizualne z frame rate:

// Źle - może trafić w środek frame
button.addEventListener('click', () => {
  element.style.transform = 'translateX(100px)';
});

// Dobrze - synchronizacja z frame
button.addEventListener('click', () => {
  requestAnimationFrame(() => {
    element.style.transform = 'translateX(100px)';
  });
});

4. Web Workers

Przenieś ciężkie obliczenia poza main thread:

// main.js
const worker = new Worker('worker.js');

button.addEventListener('click', () => {
  showSpinner();
  worker.postMessage({ data: largeDataset });
});

worker.onmessage = (e) => {
  hideSpinner();
  displayResults(e.data);
};

// worker.js
self.onmessage = (e) => {
  const result = heavyComputation(e.data);
  self.postMessage(result);
};

5. Debouncing i throttling

// Debounce - wykonaj po zakończeniu sekwencji
function debounce(fn, delay) {
  let timeout;
  return (...args) => {
    clearTimeout(timeout);
    timeout = setTimeout(() => fn(...args), delay);
  };
}

// Throttle - wykonuj maksymalnie raz na X ms
function throttle(fn, limit) {
  let inThrottle;
  return (...args) => {
    if (!inThrottle) {
      fn(...args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// Użycie
input.addEventListener('input', debounce(handleSearch, 300));
window.addEventListener('scroll', throttle(handleScroll, 100));

6. Passive event listeners

// Źle - może blokować scrollowanie
element.addEventListener('touchstart', handler);

// Dobrze - deklaracja że nie będzie preventDefault
element.addEventListener('touchstart', handler, { passive: true });

7. CSS contain

Ogranicz obszar, który musi być przeliczony:

.widget {
  contain: content; /* Izoluje layout i paint */
}

.modal {
  contain: strict; /* Pełna izolacja */
}

8. content-visibility

Opóźnij renderowanie elementów poza viewport:

.card {
  content-visibility: auto;
  contain-intrinsic-size: 0 500px; /* Szacowana wysokość */
}

Optymalizacja frameworków

React

// Użyj startTransition dla mniej pilnych aktualizacji
import { startTransition } from 'react';

function handleClick() {
  // Pilna aktualizacja - natychmiast
  setIsLoading(true);

  // Mniej pilna - może być odroczona
  startTransition(() => {
    setResults(computeExpensiveResults());
  });
}

// useDeferredValue dla drogich renderów
import { useDeferredValue } from 'react';

function SearchResults({ query }) {
  const deferredQuery = useDeferredValue(query);

  // Renderuj z opóźnionym query
  return <Results query={deferredQuery} />;
}

// memo dla unikania zbędnych re-renderów
const ExpensiveComponent = React.memo(({ data }) => {
  // ...
});

Vue

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

// shallowRef dla dużych obiektów
const largeData = shallowRef(null);

// Lazy computed
const expensiveComputed = computed(() => {
  // Obliczenia tylko gdy potrzebne
});
</script>

<template>
  <!-- v-once dla statycznej treści -->
  <div v-once>{{ staticContent }}</div>

  <!-- v-memo dla cachowania -->
  <div v-for="item in list" :key="item.id" v-memo="[item.id]">
    {{ item.name }}
  </div>
</template>

Checklist optymalizacji INP

JavaScript

  • Podziel długie taski na mniejsze (poniżej 50ms)
  • Użyj scheduler.yield() lub setTimeout(0)
  • Przenieś ciężkie obliczenia do Web Workers
  • Debounce/throttle event handlerów

DOM

  • Batch DOM operations (DocumentFragment)
  • Unikaj layout thrashing
  • Użyj requestAnimationFrame dla animacji
  • Zastosuj CSS contain dla izolacji

Third-party

  • Defer nieistotne skrypty
  • Lazy-load widgety (chat, social)
  • Rozważ server-side GTM

Framework

  • React: startTransition, useDeferredValue
  • Vue: v-once, v-memo, shallowRef
  • Virtualizacja długich list

Monitorowanie INP

Real User Monitoring (RUM)

import { onINP } from 'web-vitals';

onINP((metric) => {
  // Wyślij do analytics
  gtag('event', 'web_vitals', {
    metric_name: metric.name,
    metric_value: metric.value,
    metric_rating: metric.rating,
    metric_delta: metric.delta
  });

  // Lub do własnego endpointa
  navigator.sendBeacon('/analytics', JSON.stringify({
    name: 'INP',
    value: metric.value,
    page: location.pathname
  }));
});

Alerty na słabe INP

Ustaw alerty w Google Search Console lub własnym systemie monitoringu gdy INP przekroczy próg.

Podsumowanie

INP jest trudniejszą metryką niż FID, ale lepiej odzwierciedla rzeczywiste doświadczenie użytkownika. Kluczowe punkty:

  1. INP mierzy całą interakcję - nie tylko opóźnienie
  2. Raportuje najgorszą interakcję - musisz optymalizować wszystko
  3. Podziel długie taski - żaden task nie powinien trwać >50ms
  4. Oddawaj kontrolę przeglądarce - scheduler.yield(), requestIdleCallback
  5. Web Workers - dla ciężkich obliczeń
  6. Monitoruj w RUM - dane laboratoryjne nie wystarczą

Cel to INP poniżej 200ms dla 75% użytkowników.

Więcej o tym, jak INP i pozostałe metryki wydajności wpływają na ranking w Google, znajdziesz w przewodniku po technologiach web i SEO.

Źródła

  1. web.dev - Interaction to Next Paint (INP) https://web.dev/articles/inp

  2. web.dev - Optimize INP https://web.dev/articles/optimize-inp

  3. Chrome Developers - INP announcement https://developer.chrome.com/blog/inp-cwv

  4. web.dev - Long tasks https://web.dev/articles/optimize-long-tasks

  5. MDN - Scheduler API https://developer.mozilla.org/en-US/docs/Web/API/Scheduler

  6. web.dev - First Input Delay (FID) - archived https://web.dev/articles/fid