Ana Sayfa / Blog / INP (Interaction to Next Paint) optimize etmek: JavaScript’in rolü

INP (Interaction to Next Paint) optimize etmek: JavaScript’in rolü

Core Web Vitals'a 2024'te eklenen INP, FID'den daha zorlu bir metrik. Gerçek projelerde INP'yi 500ms'den 200ms altına çekerken JavaScript'in etki mekanizmaları üzerine çalıştım.

Google Core Web Vitals’a 2024’te First Input Delay’i emekliye ayırıp Interaction to Next Paint’i ekledi. Bu değişiklik pek çok ürünü uykudan uyandırdı. FID sadece ilk input’u ölçüyordu, INP tüm oturum boyunca kullanıcının yaşadığı interaction gecikmesinin 75. yüzdelikini ölçüyor. Çok daha dürüst bir metrik, ama geçmesi daha zor.

“Good” eşiği INP için 200ms. Bir müşteri projesinde INP p75 değeri 580ms’den 190ms’ye indirdik, önümdeki 3 hafta yoğun JavaScript optimization geçti. Burada öğrendiklerim.

INP ne ölçüyor, nasıl değişiyor?

INP şu zinciri ölçüyor:

  1. Kullanıcı bir event tetikliyor (tap, click, key press)
  2. Browser event handler’ı çalıştırıyor (JS execution)
  3. Browser layout/paint yapıyor
  4. Next paint’e kadar geçen süre = INP değeri

Bu zincir nerede uzarsa INP o kadar kötü. 3 ana kaynak: event handler içindeki JS uzun, handler bir sürü side-effect tetikliyor (state update, re-render), browser frame budget’ını aşan layout çalışması var.

Long task’ı tespit edin

Chrome DevTools Performance panel INP’yi anlamak için en iyi yer. Record yaparken kullanıcı aksiyonları simüle edin, long task’ları görün.

“Long task” = main thread 50ms üzerinde bloke. Her long task INP’ye katkı yapar. Performance Observer API ile production’da yakalayabilirsiniz:

new PerformanceObserver((list) => {
    list.getEntries().forEach((entry) => {
        console.log('Long task:', entry.duration, entry.attribution);
    });
}).observe({entryTypes: ['longtask']});

Event handler’ları böl

En yaygın suçlu: handler içinde sync olarak büyük iş yapmak. Örnek:

button.addEventListener('click', () => {
    const filtered = bigList.filter(complexPredicate);
    const sorted = filtered.sort(expensiveComparator);
    renderList(sorted);
});

Bu handler 300ms sürüyor olabilir. INP 300+ alırsınız. Çözüm: iş parçalarını yield ile bölüp her parçanın arasında paint fırsatı vermek.

button.addEventListener('click', async () => {
    showLoadingState();
    await new Promise(r => setTimeout(r, 0)); // yield
    const filtered = bigList.filter(complexPredicate);
    await new Promise(r => setTimeout(r, 0));
    const sorted = filtered.sort(expensiveComparator);
    renderList(sorted);
});

Event sonrası ilk paint hemen çıkar (loading state’i), sonrası arka planda yürür.

Modern alternatif: scheduler.yield() API’si. Chrome 116+’da var, doğrudan Paint’e geri kontrol verir.

requestIdleCallback vs requestAnimationFrame

Farklı iş için farklı scheduling:

  • requestAnimationFrame: UI güncellemesi, animation frame’ine bağlı iş. Next paint’ten hemen önce çalışır.
  • requestIdleCallback: idle time’da yapılacak iş. Kullanıcı etkileşimi yokken arka plan hesapları.

INP için requestIdleCallback gold. Analytics push, prefetch, cache warming gibi “hemen değilse de yapılmalı” işleri buraya koyun.

Third-party script etkisi

3rd party script’ler INP’nin sessiz katili. Google Tag Manager, chat widget’ları, A/B testing SDK’ları main thread’i bloklar.

En etkili teknik: bu script’leri defer veya async yükleyin, iframe içine sandbox edin (GTM için Partytown projesi web worker’a taşıma çözümü sunuyor), user interaction sonrasına ertelenebilenleri erteleyin.

Bir projede chat widget’ı ilk 10 saniyeden sonra load etmeye başladık, INP p75 150ms iyileşti.

React: re-render tuzağı

React uygulamalarında INP’nin en büyük kaynağı gereksiz re-render. Her click state güncellemesi tetikliyor, component tree re-render oluyor, 60+ component render ediliyorsa 100ms kolay harcar.

Çözüm stratejileri:

  • React.memo: prop’u değişmeyen componentler re-render etmesin
  • useMemo / useCallback: expensive computation ve referential equality
  • useTransition: non-urgent state update’i low priority olarak işaretleyin
  • useDeferredValue: input değerini güncel tutun ama türetilmiş expensive hesaplama deferred olsun

React 18’in Concurrent Mode bunun için tasarlandı. Migrate ettiğim projelerde INP %40 iyileşme gördüm.

CSS selector’ların maliyeti

Büyük DOM’larda karmaşık CSS selector’lar style recalc’ı patlatıyor. [data-x] > * + *:not(.hidden) ~ .foo gibi selectorlar her click’te 20-50ms ekleyebilir.

Selector’ları basitleştirin, CSS layer kullanıp isolate edin, contain: layout paint property’si ile layout scope’u daraltın.

Web Worker: büyük iş için tek çözüm

Bazı işler fundamentally CPU-heavy: büyük dataset filtering, image processing, complex calculation. Main thread’de ne yaparsanız yapın INP kötü olur.

Web Worker’a taşıyın. Comlink gibi kütüphanelerle RPC şeklinde ergonomi iyileşiyor.

const worker = new Worker('/processor.js');
button.addEventListener('click', async () => {
    const result = await worker.postMessageAsync({type: 'filter', data});
    render(result);
});

Main thread free, INP optimize.

INP field data nasıl topluyorum

CrUX raporu aggregate data veriyor ama kendi kullanıcılarım için real-user monitoring (RUM) gerekli. web-vitals library (Google’ın official) INP dahil core vitals’ı topluyor:

import {onINP} from 'web-vitals';
onINP((metric) => {
    sendToAnalytics({
        name: metric.name,
        value: metric.value,
        attribution: metric.attribution,
    });
});

Metric.attribution alanı hangi element hangi süre ile INP’yi tetiklediğini söyler, optimize edilecek hotspot’ları gösterir.

Son hatırlatma

INP optimizasyonu LCP gibi tek seferlik iyileştirme değil. Yeni feature her eklendiğinde regresyon riski var. CI’da INP sentetik testi koymak zor ama mümkün: Chrome headless + Lighthouse + Puppeteer kombinasyonu ile critical user flow’larda INP ölçün, threshold altı build fail olsun.

Bu disiplin olmazsa INP 6 ay sonra tekrar patlar, bakım cycle’ına girer.

Bu konuda bir projeniz mi var?

Kısa bir özet bırakın, 24 saat içinde size dönüş yapayım.

İletişime Geç