INP (Interaction to Next Paint) adalah metrik Core Web Vitals untuk responsivitas: waktu dari user klik, tap, atau menekan tombol keyboard, sampai layar menampilkan hasilnya di frame berikutnya. Batasnya sudah kamu kenal dari pelajaran Core Web Vitals. Di sini kita bongkar isinya. Bayangin pesan kopi di kafe yang ramai: kamu menunggu kasir selesai melayani orang sebelumnya, barista membuat kopimu, lalu kopinya diantar ke meja. Waktu yang kamu rasakan adalah ketiganya dijumlah, bukan cuma lamanya barista bekerja.
🧪 Tebakan cepat: waktu user klik "Tambah ke keranjang", ada script analytics 180 ms yang baru mulai jalan. Event handler-nya butuh 250 ms, dan menggambar hasilnya 100 ms. Berapa INP interaksi ini: 250, 430, atau 530 ms? Jawabannya ada di bagian "Tiga bagian sebuah interaksi".
Tiga bagian sebuah interaksi
Semua JavaScript halaman, juga style, layout, dan paint, dikerjakan di satu jalur yang sama: main thread. Dia cuma bisa mengerjakan satu hal dalam satu waktu. Satu interaksi melewati tiga bagian:
- Input delay: dari klik sampai event handler mulai jalan. Panjang kalau main thread lagi sibuk mengerjakan task lain.
- Processing time: lamanya event handler berjalan. Ini kode kamu.
- Presentation delay: dari handler selesai sampai frame berikutnya selesai digambar: browser menghitung style, layout, dan paint untuk perubahannya.
| Bagian | Penyebab di contoh | Lama |
|---|---|---|
| Input delay | script analytics yang lagi jalan | 180 ms |
| Processing time | handler "Tambah ke keranjang" | 250 ms |
| Presentation delay | 1.500 baris digambar ulang | 100 ms |
| INP interaksi ini | 530 ms |
Itu jawaban tebakannya: 530 ms, dan itu poor. Batasnya: good maksimal 200 ms, needs-improvement sampai 500 ms, poor di atas 500 ms. Angka yang tepat 200 masih good. INP halaman diambil dari interaksi yang paling lambat selama kunjungan (kalau interaksinya banyak sekali, beberapa yang paling ekstrem diabaikan), lalu Google menilainya di persentil ke-75 kunjungan nyata.
INP menggantikan FID (First Input Delay) sejak Maret 2024. FID cuma mengukur input delay, dan cuma untuk interaksi pertama. Di contoh tadi FID-nya 180 ms dan terlihat good, padahal user menunggu 530 ms.
Long task: kenapa main thread macet
Long task adalah task yang jalan lebih dari 50 ms di main thread. Selama long task jalan, browser gak bisa menjalankan handler klik dan gak bisa menggambar. Script analytics 180 ms dan handler 250 ms tadi dua-duanya long task.
function classifyInp(milliseconds) {
if (milliseconds <= 200) return "good";
if (milliseconds <= 500) return "needs-improvement";
return "poor";
}
function identifyBlockingTasks(tasks) {
return tasks.filter((task) => task.duration > 50); // long task: lebih dari 50 ms
}
console.log(classifyInp(530)); // "poor"
const tasks = [
{ name: "parseHTML", duration: 30 },
{ name: "evalScript", duration: 120 },
{ name: "styleRecalc", duration: 20 },
{ name: "fetchData", duration: 80 },
];
console.log(identifyBlockingTasks(tasks));
// [{ name: "evalScript", duration: 120 }, { name: "fetchData", duration: 80 }]
<=di kedua batas:classifyInp(200)itu"good",classifyInp(500)itu"needs-improvement", danclassifyInp(501)itu"poor".task.duration > 50, bukan>=: task 50 ms belum long task, task 51 ms sudah.filtermengembalikan array baru berisi object task yang lolos, dengan urutan aslinya. Kalau gak ada yang lolos, hasilnya[].
Yield: kasih main thread kesempatan
Cara utama mengecilkan INP adalah yield (mengalah): pecah kerja panjang jadi potongan pendek, dan di antara potongan, kembalikan main thread ke browser supaya dia sempat menggambar dan menerima klik.
function yieldToMain() {
if (globalThis.scheduler?.yield) return scheduler.yield();
return new Promise((resolve) => setTimeout(resolve, 0)); // cadangan
}
button.addEventListener("click", async () => {
updateCartBadge(); // yang dilihat user dikerjakan duluan
for (const chunk of chunks) {
await yieldToMain(); // browser boleh menggambar dan menerima klik dulu
processChunk(chunk); // tiap potongan di bawah 50 ms
}
});
updateCartBadge()jalan sebelum yield pertama, jadi frame berikutnya sudah menampilkan angka keranjang yang baru. INP interaksi ini berhenti dihitung di frame itu.scheduler.yield()mengembalikan Promise. Sesudahawait, sisanya lanjut di task baru, dan didahulukan dari task lain yang lagi antre. Belum semua browser punya, jadi ada cadangansetTimeout(resolve, 0), yang menaruh sisanya di belakang antrean.- Yield-nya ada di dalam loop. Kalau cuma sekali, sisa kerjanya tetap satu long task yang bisa jadi input delay untuk klik berikutnya.
Di contoh tadi, kalau script analytics juga dipecah per 50 ms dan handler-nya yield setelah 12 ms, INP-nya jadi 50 + 12 + 100 = 162 ms: good.
| Bagian yang besar | Cara mengecilkannya |
|---|---|
| Input delay | pecah long task lain dengan yield, tunda script pihak ketiga |
| Processing time | kerjakan yang dilihat user dulu lalu yield, debounce handler berat, pindahkan hitungan berat ke Web Worker |
| Presentation delay | ubah DOM sesedikit mungkin: batch, virtual scrolling, hindari layout thrashing |
⚠️ Jebakan yang sering ketemu
- Cuma mengukur handler. Handler 30 ms terasa cepat, tapi kalau input delay-nya 300 ms, INP-nya tetap jelek.
- Yield cuma sekali. Sisa 238 ms yang jalan sekaligus tetap long task. Yield di setiap potongan.
<di batas. Di latihan,classifyInp(200)harus"good"danclassifyInp(500)harus"needs-improvement".>= 50untuk long task. Task 50 ms belum long task. Yang dihitung lebih dari 50.
TL;DR: INP adalah waktu dari interaksi sampai frame berikutnya digambar, yaitu input delay + processing time + presentation delay. Good maksimal 200 ms, needs-improvement sampai 500 ms, poor di atas 500 ms, dan INP halaman diambil dari interaksi yang paling lambat. INP menggantikan FID sejak Maret 2024, yang cuma mengukur input delay interaksi pertama. Long task adalah task lebih dari 50 ms di main thread. Kecilkan INP dengan yield: kerjakan yang dilihat user dulu, lalu await scheduler.yield() (cadangan setTimeout) di antara potongan pendek.