A page that is slow to respond is also hard to use with a keyboard or a screen reader, because both problems come from the same place: a main thread too busy to answer. Fix the long tasks that hurt Interaction to Next Paint, and you also fix late focus moves, missing announcements, and key presses that land in the wrong place.
INP replaced First Input Delay as a Core Web Vital on March 12, 2024, and a good score is 200 milliseconds or less at the 75th percentile. It measures clicks, taps, and key presses, which makes it one of the few performance metrics that measures keyboard users directly. Pages keep getting heavier, too. The 2026 WebAIM Million, published at the end of March, found that the average home page now carries 1,437 elements, and that home pages using ARIA averaged 59.1 detectable errors, versus 42 on pages without it. That's a correlation, not proof that ARIA causes errors, but it fits a pattern you can check on your own site: complexity collects in the same components that break.
What a busy main thread feels like without a mouse
A long task is any stretch of main-thread work over 50 milliseconds. While one runs, the browser can't respond to input or paint the screen. Everyone notices, but not equally.
| During a long task | Mouse user | Keyboard user | Screen reader user |
|---|---|---|---|
| Input waits | Clicks feel sluggish | Tab presses queue, then land all at once | Commands queue and speech goes quiet |
| Focus moves late | Rarely noticed | Focus indicator lags or skips several stops | Nothing is announced, so the user presses again |
| The DOM re-renders | A brief flicker | The focused element is replaced and focus drops to the page | Reading position can be lost |
| A live region updates | Text appears late | Text appears late | The announcement is late, merged, or missed |
The re-render row deserves attention. When a framework replaces the element that has focus, the browser moves focus to the document body. A mouse user never notices. A keyboard user's next Tab starts from the top of the page. A screen reader user hears nothing and has to work out where they are.
The "press again" problem is the cruelest. A screen reader user activates a disclosure button, hears nothing because the main thread is busy, and presses again. Both presses are processed when the task ends, so the menu opens and closes. The user concludes the button is broken, and for practical purposes it is.
There's a hidden cost as well. When assistive technology is running, the browser has to keep the accessibility tree in sync with the DOM, and in Chromium much of that work happens on the renderer's main thread. Lab tests rarely run with a screen reader on, so your measurements probably understate what screen reader users experience on a large, busy page.
Measure INP by input type
Aggregate INP hides keyboard problems, because most interactions on most sites are taps and clicks. The attribution build of the web-vitals library reports the interaction type, so segment by it:
import { onINP } from 'web-vitals/attribution';
onINP(({ value, attribution }) => {
navigator.sendBeacon('/rum/inp', JSON.stringify({
value,
type: attribution.interactionType, // 'pointer' or 'keyboard'
target: attribution.interactionTarget, // a CSS selector for the element
inputDelay: attribution.inputDelay,
processing: attribution.processingDuration,
presentation: attribution.presentationDelay,
}));
});
If keyboard INP is worse than pointer INP, look at keydown handlers: search-as-you-type, roving tabindex implementations that re-render a whole list on every arrow key, and global shortcut listeners. The three phases point in different directions. A long input delay means other work was already running, often third-party scripts. Long processing means your handler. A long presentation delay means rendering and layout.
In the lab, record a Performance panel trace while you Tab through the page and operate its widgets. Then record it again with NVDA running, and compare.
Give feedback first, then yield
The single most useful pattern is to do the small part of the work the user perceives, yield so the browser can paint and update the accessibility tree, and then do the heavy part in chunks.
function yieldToMain() {
if (globalThis.scheduler?.yield) return scheduler.yield();
return new Promise((resolve) => setTimeout(resolve, 0));
}
filterButton.addEventListener('click', async () => {
filterButton.setAttribute('aria-pressed', 'true'); // state first
status.textContent = 'Updating results'; // a role="status" element
await yieldToMain(); // paint, and let assistive tech catch up
const items = applyFilter(allItems);
resultsList.replaceChildren();
for (let i = 0; i < items.length; i += 50) {
renderRows(resultsList, items.slice(i, i + 50));
await yieldToMain();
}
status.textContent = `${items.length} results`;
});
scheduler.yield() resumes your work ahead of other queued tasks, while the setTimeout fallback sends it to the back of the line. Feature-detect as above, because support isn't universal yet. The ordering rule matters as much as the yielding: state changes and focus moves first, rendering second.
Stop forcing layout, and shrink the DOM
Layout thrashing happens when code alternates between writing styles and reading geometry, which forces the browser to recalculate layout on every read:
// Before: every read after a write forces a fresh layout
rows.forEach((row) => {
row.style.height = `${row.querySelector('.cell').offsetHeight}px`;
});
// After: read everything, then write everything
const heights = rows.map((row) => row.querySelector('.cell').offsetHeight);
rows.forEach((row, i) => { row.style.height = `${heights[i]}px`; });
Focus-management code is a frequent offender, because it measures positions to scroll a focused item into view. Batch those reads too.
DOM size multiplies everything. More elements mean more style and layout work per interaction, and a bigger accessibility tree to keep in sync. The usual sources are duplicate navigation (a desktop menu and a mobile menu, both always rendered), mega menus with hundreds of links on every page, the contents of closed dialogs, repeated inline SVG icons, and wrapper elements from page builders. Render menus and dialogs when they first open, and ship one responsive navigation instead of two.
For long pages that must stay long, content-visibility skips rendering work for off-screen sections:
.article-section,
.product-grid > li {
content-visibility: auto;
contain-intrinsic-size: auto 600px;
}
Unlike display: none or list virtualization, content skipped this way stays in the DOM and in the accessibility tree, so find-in-page and screen reader navigation still reach it. That's also the reason to be wary of virtualized lists. When only the visible rows exist, a screen reader user's browse mode can't reach the rest. Pagination or content-visibility is usually the kinder choice.
Finally, prefer native elements. A <button>, <details>, or <select> needs no JavaScript to handle its keyboard behavior, so it costs nothing on the main thread and can't drift out of sync with its ARIA.
Debounce input, and move heavy work off the main thread
Search-as-you-type is where keyboard INP and the screen reader experience fail together. Every keystroke triggers filtering, rendering, and often an announcement. Debounce the work, and announce once the results settle, not on every key:
let timer;
searchInput.addEventListener('input', () => {
clearTimeout(timer);
timer = setTimeout(async () => {
const results = await searchInWorker(searchInput.value);
renderResults(results);
searchStatus.textContent = `${results.length} results`; // role="status"
}, 250);
});
Here searchInWorker posts the query to a Web Worker and resolves with its reply. Filtering, sorting, and parsing large JSON payloads don't need the DOM, so they don't need the main thread. Moving them to a worker leaves the main thread free for the work only it can do: handling input, moving focus, and painting.
One backlog, two audiences
When you triage INP, tag each slow interaction with the users it affects. You'll often find the same handful of components (filters, menus, search, and carts) at the top of both the performance list and the accessibility list. Fixing them once pays twice. Our Core Web Vitals engineering sprints start from your field data and fix the worst INP templates, and source-code remediation covers the focus, naming, and announcement issues that ride along with them.
