INP Optimization for WordPress: Fixing Interaction to Next Paint

INP (Interaction to Next Paint) replaced FID as a Core Web Vital in March 2024. It measures the time from a user interaction (click, keypress, tap) to when the browser paints the next frame in response. Poor INP is most commonly caused by long JavaScript tasks blocking the main thread during WordPress page interactions.

Problem: A WordPress or WooCommerce page scores poorly on INP (Interaction to Next Paint) — clicks on filters, tabs, or cart buttons feel sluggish even though the page loads quickly, and the cause is not obvious from Lighthouse alone.

Solution: Profile INP with Chrome DevTools Performance panel — look for long tasks (>50ms) in the main thread after a user interaction. The most common WordPress INP offenders are synchronous WooCommerce cart JavaScript, undeferred third-party scripts, and large layout recalculations triggered by DOM manipulation. Fix with defer/async script attributes, debouncing, and breaking long tasks with scheduler.yield().

The examples below measure INP with the web-vitals library, identify long tasks with PerformanceObserver, and show the most effective patterns for breaking up main-thread work in WordPress JavaScript.

// Measure INP using the web-vitals library (from Google)
// npm install web-vitals  or  use the CDN version
import { onINP } from 'web-vitals';

onINP( ( metric ) => {
    // metric.value in milliseconds
    // Good: < 200ms  Needs improvement: 200–500ms  Poor: > 500ms
    console.log( `INP: ${metric.value}ms (rating: ${metric.rating})` );

    // Send to your analytics endpoint
    if ( typeof wp !== 'undefined' && wp.apiFetch ) {
        wp.apiFetch( {
            path: '/myplugin/v1/vitals',
            method: 'POST',
            data: {
                metric: 'INP',
                value: metric.value,
                url: location.href,
                attribution: metric.attribution,
            },
        } );
    }
} );

// Observe long tasks — anything over 50ms blocks INP
const observer = new PerformanceObserver( ( list ) => {
    for ( const entry of list.getEntries() ) {
        if ( entry.duration > 50 ) {
            console.warn( `Long task: ${entry.duration.toFixed(0)}ms`, entry );
        }
    }
} );
observer.observe( { type: 'longtask', buffered: true } );

Break up long tasks to improve INP:

// Pattern 1: yield to the main thread between chunks using scheduler.yield()
// (Chrome 115+ / Safari 18+) or setTimeout fallback
async function processLargeList( items ) {
    const CHUNK_SIZE = 50;
    const results = [];

    for ( let i = 0; i < items.length; i += CHUNK_SIZE ) {
        const chunk = items.slice( i, i + CHUNK_SIZE );
        results.push( ...chunk.map( processItem ) );

        // Yield — lets the browser handle clicks/input between chunks
        if ( 'scheduler' in globalThis && 'yield' in scheduler ) {
            await scheduler.yield();
        } else {
            await new Promise( resolve => setTimeout( resolve, 0 ) );
        }
    }
    return results;
}

// Pattern 2: Move heavy computation off the main thread with a Web Worker
// worker.js
self.addEventListener( 'message', ( e ) => {
    const result = heavyComputation( e.data );
    self.postMessage( result );
} );

// main.js
const worker = new Worker( '/wp-content/plugins/myplugin/worker.js' );
button.addEventListener( 'click', () => {
    worker.postMessage( largeDataset );
} );
worker.addEventListener( 'message', ( e ) => {
    updateUI( e.data ); // back on main thread — fast DOM update only
} );

NOTE: The most common INP offenders on WordPress sites are: WooCommerce cart drawer JavaScript, third-party chat widgets, and heavy click handlers that run synchronous DOM queries. Profile interactions in Chrome DevTools → Performance panel by clicking "Record" and then performing the slow interaction.

Leave Comment

Your email address will not be published. Required fields are marked *