How to Fix Cumulative Layout Shift (CLS) on WordPress
Content jumps as the page loads and Search Console reports a CLS issue. Find the element that moves and why, then reserve space for images, embeds, ads, fonts and banners.
Search Console shows INP in red and buttons feel sticky. Learn what INP measures, find the long tasks behind it in Chrome devtools, and fix the scripts causing them.
Search Console emails you about “INP issue: longer than 200ms”, or PageSpeed Insights shows Interaction to Next Paint in orange or red. In real life it feels like this: you tap the menu button and nothing happens for half a second, you click “Add to cart” and the button seems frozen, or typing into a search box stutters.
What is happening is that the browser’s main thread is busy. The browser can only run one piece of JavaScript at a time on that thread, and it also uses it to draw the screen. If a script is in the middle of a long job when you click, your click waits in line, then your click’s own handlers run, and only after all that can the browser paint the result. INP measures that whole wait. Site owners usually see it after adding marketing tags, a chat widget or a heavy theme, and it often fails on mobile while desktop passes.
INP looks at the clicks, taps and key presses a visitor makes during their whole visit and reports one of the slowest (on pages with many interactions, a few extreme outliers are ignored). Scrolling and hovering do not count. Each interaction has three phases:
| Phase | What it is | Usual cause when it is long |
|---|---|---|
| Input delay | Time from the tap until event handlers can start | Other scripts already running a long task: tag managers, page load scripts, timers |
| Processing duration | Time the event handlers themselves take | Heavy click handlers, many listeners on the same element, analytics fired synchronously |
| Presentation delay | Time from handlers finishing to the next frame painted | Large DOM, expensive style and layout recalculation, big re-renders |
The thresholds, judged at the 75th percentile of real visits:
| INP | Rating |
|---|---|
| 200 ms or less | Good |
| Over 200 ms up to 500 ms | Needs improvement |
| Over 500 ms | Poor |
A long task is any single chunk of main-thread work longer than 50 ms. The browser cannot respond to input during it. One 400 ms task from a script that happens to run when the user taps gives you a 400 ms input delay before your own code even starts.
INP replaced First Input Delay as a Core Web Vital in March 2024. It is stricter, because it covers every interaction and all three phases, not just the delay before the first one. Loading speed (LCP), image weight and font choices are covered in the design-side speed guide (FG-11); this guide is about responsiveness after the page has loaded.
wp-content/mu-plugins/ or your child theme’s functions.php.INP is a field metric: it comes from real Chrome users, collected in the Chrome UX Report (CrUX). Lab tools cannot fully reproduce it.
Important: because field data is a 28-day rolling window, fixes take weeks to show up. Use the lab methods below to confirm improvement the same day.
Reading it: if input delay dominates, something else was running when you clicked (Step 3). If processing dominates, the element’s own handlers are slow (Step 5). If presentation delay dominates, rendering the result is expensive (the Advanced section).
Now record a trace. Click the record button, perform the slow interaction once or twice, then stop.
Script URLs map to WordPress plugins easily: a path such as /wp-content/plugins/PLUGIN_SLUG/assets/js/... names the plugin folder. Third-party domains such as googletagmanager.com name the service.
On most WordPress sites with failing INP, the biggest long tasks belong to scripts you did not write. The common offenders, and what to do about each:
A tag manager container is a small script that loads many others. Each tag, and every click trigger that listens to all clicks, adds work. In your tag manager, list tags by when they fire. Remove tags for tools no one uses. Prefer tags that fire on page load once rather than on every click. Avoid custom HTML tags that inject more scripts on interaction. Test by temporarily blocking the container: in devtools, Network panel, right-click the container request, choose “Block request URL”, reload and repeat your interaction test. If INP drops from 450 ms to 120 ms, you have found where to work.
Live chat scripts are often large and run work on load and on every route change. Use a facade: a lightweight button that looks like the chat launcher and loads the real widget only when clicked. Several chat vendors offer a delayed or lazy load option; otherwise many optimization plugins can delay the script until first interaction (see Step 6 for the trade-off).
These tools watch every change to the page so they can replay it, which costs main-thread time during exactly the interactions you care about. Run them only on the pages and for the period you are studying, or sample a fraction of visitors in the tool’s settings.
The accept button on a cookie banner is often the single worst interaction on a site, because clicking it releases every blocked tag at once and the banner’s own handler waits for them. Look in your consent tool for options to load tags asynchronously after consent, and check that it does not re-scan the whole page on click.
If processing duration is the big phase, the element’s handlers are doing too much. To see what is attached, select the element in the Elements panel and open the Event Listeners tab. You will often find several plugins listening to the same click on document.
The fix is to do the visible part first and defer the rest. A handler should update what the user sees, let the browser paint, and only then do analytics, network calls or heavy computation. The standard pattern, in your theme or plugin JavaScript:
function yieldToMain() {
if (globalThis.scheduler && typeof scheduler.yield === 'function') {
return scheduler.yield();
}
return new Promise(resolve => setTimeout(resolve, 0));
}
const button = document.querySelector('BUTTON_SELECTOR');
const menu = document.querySelector('MENU_SELECTOR');
button.addEventListener('click', async () => {
menu.classList.toggle('is-open'); // visible change first
await yieldToMain(); // let the browser paint
window.dataLayer = window.dataLayer || []; // non-urgent work after
window.dataLayer.push({ event: 'EVENT_NAME' });
});
Placeholders: BUTTON_SELECTOR and MENU_SELECTOR are CSS selectors for your toggle button and menu (right-click the element, Inspect, and read its class or id). EVENT_NAME is whatever your analytics expects. What it does: scheduler.yield() is supported in recent Chromium browsers and hands control back to the browser while keeping your work near the front of the queue; elsewhere the setTimeout fallback does a similar job. Good result: in a new trace the interaction’s processing time shrinks to a few milliseconds and the analytics work appears as a separate task after the paint.
Other handler fixes: debounce input handlers on search boxes so they do not run on every keystroke; avoid reading layout values (offsetHeight, getBoundingClientRect()) straight after changing styles in a loop, which forces repeated layout; and replace dozens of per-item listeners with one delegated listener.
| Method | When it downloads | When it runs | Best for |
|---|---|---|---|
| Normal script | Blocks parsing | Immediately | Almost nothing |
async |
In parallel | As soon as it arrives, in any order | Independent scripts such as analytics |
defer |
In parallel | After the HTML is parsed, in order | Theme and plugin scripts with dependencies |
| Delay until interaction | Not until first scroll, tap or key press | On that first interaction, or after a timeout | Chat, heatmaps, social embeds |
Since WordPress 6.3 you can register scripts with a loading strategy in PHP:
wp_register_script( 'SCRIPT_HANDLE', get_stylesheet_directory_uri() . '/js/FILE_NAME.js', array(), '1.0', array(
'strategy' => 'defer',
'in_footer' => true,
) );
Placeholders: SCRIPT_HANDLE is a unique name you choose, FILE_NAME your script file in the child theme’s js folder. WordPress automatically falls back to normal loading if a dependency requires it.
Optimization plugins such as WP Rocket, Perfmatters, LiteSpeed Cache and Flying Scripts offer a “delay JavaScript” feature. Use it with care: delayed scripts all execute on the visitor’s first interaction, so that first tap can become the slowest interaction of the visit and make INP worse. Delay only scripts that are not needed for the first click (chat, heatmaps, social widgets), and exclude menus, sliders, forms, cart and consent scripts. Test the very first click after a fresh load with throttling on.
Devtools tells you what happens on your machine. The web-vitals library’s attribution build tells you what happens to real visitors. To try it on one page view, paste this into the browser console:
const s = document.createElement('script');
s.type = 'module';
s.textContent = `
import {onINP} from 'https://unpkg.com/web-vitals@5/dist/web-vitals.attribution.js?module';
onINP(({value, rating, attribution}) => {
console.log('INP', Math.round(value), rating, attribution.interactionTarget);
console.log('input', Math.round(attribution.inputDelay),
'processing', Math.round(attribution.processingDuration),
'presentation', Math.round(attribution.presentationDelay));
if (attribution.longestScript) {
console.log('longest script', attribution.longestScript.entry.sourceURL,
attribution.longestScript.subpart);
}
}, {reportAllChanges: true});`;
document.head.appendChild(s);
Where: browser console, on your page. Then click around. reportAllChanges: true logs every time the page’s INP worsens instead of waiting until you leave. Reading the output: interactionTarget is a CSS selector for the element clicked; the three numbers are the phases in milliseconds; longestScript names the script file that ran longest during the interaction and which phase it fell in. If your site has a strict Content Security Policy the import may be blocked; that is expected.
For ongoing monitoring, a developer can bundle the library and send these values to your analytics as events, so you can see which elements and scripts cause slow interactions across thousands of visits, not just yours.
When the handler is quick but the paint is late, the browser is spending time on style recalculation and layout. In the trace, purple “Recalculate style” and “Layout” blocks after your handler confirm it.
document.querySelectorAll('*').length in the browser console to see the element count. Lighthouse flags an excessive DOM size; simplifying sections and removing duplicated mobile and desktop markup helps directly.content-visibility: auto on long below-the-fold sections lets the browser skip their layout until needed. Pair it with contain-intrinsic-size so the scrollbar does not jump.For script-heavy tasks you cannot remove, the Long Animation Frames data in your trace (and in the web-vitals attribution as longAnimationFrameEntries) shows exactly which script, function and invoker ran in each slow frame, which is the evidence a plugin developer needs to fix their code.
INP is almost entirely a browser-side problem, so your host can rarely fix it directly. Contact them if your optimization plugin’s settings are managed at the server level, or if you need a staging site to test script changes. Contact the plugin or service vendor instead when attribution points to their script; send the page URL, the slow interaction, the throttling used, a saved trace file (Performance panel, download icon), and the longestScript URL. Note the date and UTC time of your tests.
For quick outside checks, see our diagnostic tools. If the page is not just laggy but frozen or crashing the browser, start at the Emergency Room.
Did this fix it?
Still stuck? Start again from what you can see in the Emergency Room.
Still stuck?
Print a free one-page checklist to keep, or open the Emergency Room if the symptom does not match.
Get the free checklistsKeep reading
Search the field manual
Or jump straight to a symptom
Press / to search from any page, Esc to close