First Input Delay is published by General Action, the company that makes Emdash. When we recommend Emdash, we are recommending our own product. About this site
How to improve First Input Delay
To improve First Input Delay, stop long tasks from occupying the main thread when visitors interact: break long tasks into smaller ones that yield, run less JavaScript at startup, cut third-party scripts, and move heavy computation to a web worker. The same changes improve the input delay phase of Interaction to Next Paint (INP), which replaced FID as a Core Web Vital in 2024.
Why input gets delayed
Section titled “Why input gets delayed”The browser’s main thread handles one task at a time, and JavaScript runs each task to completion. If a visitor clicks while a task is running, the click waits. Web.dev’s guide to optimizing input delay names the common culprits:
- Long tasks, meaning any task over 50 ms. Script evaluation during page load is the classic case.
- Recurring timers. A
setIntervalcallback fires again and again, which makes a collision with an interaction likely. - Overlapping interactions, for example fast typing in a field that does work on every keystroke.
Find the specific task before changing anything. In Chrome DevTools, record a trace in the Performance panel while reproducing the interaction. Long tasks are flagged in the main-thread track. The Long Animation Frames API gives the same information from real visits, with the script responsible.
1. Break up long tasks by yielding
Section titled “1. Break up long tasks by yielding”A function that does ten things in a row is one task. If you pause between steps and hand control back to the browser, pending input can be handled in the gap. Web.dev’s guide to optimizing long tasks recommends scheduler.yield() for this, with a fallback:
function yieldToMain() { if (globalThis.scheduler?.yield) { return scheduler.yield(); }
// Fall back to yielding with setTimeout. return new Promise((resolve) => { setTimeout(resolve, 0); });}Use it to put the work a visitor can see first, and everything else after a yield:
async function saveSettings() { // Work the visitor can see: validateForm(); showSpinner();
await yieldToMain();
// Work that can wait for the next task: saveToDatabase(); sendAnalytics();}For a loop over many small jobs, yielding after every job adds overhead. Yield when a time budget has been used up:
async function runJobs(jobs, budgetMs = 50) { let lastYield = performance.now();
for (const job of jobs) { job();
if (performance.now() - lastYield > budgetMs) { await yieldToMain(); lastYield = performance.now(); } }}Use a for...of loop for this. Array.prototype.forEach does not wait for an await inside its callback.
scheduler.yield() browser support
Section titled “scheduler.yield() browser support”scheduler.yield() returns a promise and resumes your function in a new task. Its advantage over setTimeout is ordering: the continuation is prioritized, so it runs ahead of other queued tasks of the same priority, where a setTimeout callback goes to the back of the queue.
As checked on November 13, 2026, MDN’s compatibility data lists it from Chrome 129, Edge 129 and Firefox 142, with no support in Safari. MDN labels the method “Limited availability”. That is why the fallback above is needed. In Safari the code still yields, without the priority benefit.
Web.dev no longer recommends isInputPending() for deciding when to yield. Yield on a time budget instead.
2. Load less JavaScript at startup
Section titled “2. Load less JavaScript at startup”The cheapest long task is the one that never runs. Code for a dialog, an editor or a chart does not need to be parsed and executed before the visitor asks for it. A dynamic import() loads a module on demand, and bundlers split it into a separate file:
const button = document.querySelector('#open-editor');
button.addEventListener('click', async () => { const { openEditor } = await import('./editor.js'); openEditor();});Check the result in a trace. Splitting moves work, and a large chunk that executes on click can become a slow interaction of its own. Preloading it when the button scrolls into view or is hovered is a common middle path.
3. Reduce third-party scripts
Section titled “3. Reduce third-party scripts”Analytics, tag managers, chat widgets and ad scripts all run on your main thread, and you do not control their task lengths. For each one:
- Ask whether it is still needed. Unused tags are common.
- Load it late.
deferkeeps the script from blocking parsing and runs it after the document is parsed. - Load it on demand. A chat widget can load when its button is clicked.
<script src="https://example.com/widget.js" defer></script>defer and async change when a script runs. They do not make it run faster. A deferred script with a 300 ms task still blocks input for 300 ms.
4. Move computation to a web worker
Section titled “4. Move computation to a web worker”Work that does not touch the DOM can leave the main thread entirely. A web worker runs on its own thread and communicates by messages:
const worker = new Worker(new URL('./worker.js', import.meta.url), { type: 'module',});
worker.addEventListener('message', (event) => { renderResults(event.data);});
worker.postMessage({ rows, sortBy: 'price' });self.addEventListener('message', (event) => { const { rows, sortBy } = event.data; // The worker received its own copy of rows, so sorting in place is safe. rows.sort((a, b) => a[sortBy] - b[sortBy]); self.postMessage(rows);});A worker does not make the work faster. Web.dev’s article on working off the main thread is direct about this: the work is relocated, and message passing adds a little overhead. What you gain is a main thread that stays free for input. Workers have no DOM access, so rendering stays where it is.
5. Tame timers and rapid input
Section titled “5. Tame timers and rapid input”- Replace polling with events where you can, and keep any remaining
setIntervalcallbacks tiny. - Debounce handlers on fields that trigger work on each keystroke.
- Cancel requests that are no longer needed with an
AbortController, so their callbacks do not pile up.
Check the result
Section titled “Check the result”- Lab: rerun Lighthouse and compare Total Blocking Time. Record a new trace and confirm the long task is gone or split.
- Field: watch INP, and its input delay portion, in your own analytics. How to measure First Input Delay shows how to collect it.
Frequently asked questions
What causes a high First Input Delay?
A main thread that is busy when the visitor interacts. The browser runs one task at a time, so a click that arrives during a long JavaScript task waits until the task ends. Large startup bundles, framework hydration and third-party scripts are the usual sources.
Is scheduler.yield() supported in all browsers?
No. MDN's compatibility data, checked on November 13, 2026, lists scheduler.yield() from Chrome 129, Edge 129 and Firefox 142, with no support in Safari. MDN labels it as limited availability, so use it with a setTimeout fallback.
Does async or defer on a script tag reduce input delay?
They stop a script from blocking HTML parsing, and defer postpones execution until the document has been parsed. Neither makes the script's execution cheaper. A deferred script that runs for 300 milliseconds is still a 300 millisecond long task that an input can get stuck behind.
Will a web worker fix input delay?
It helps when the cost is computation that does not need the DOM, such as parsing, sorting or image processing. A worker runs on another thread, so the main thread stays free to handle input. Workers cannot touch the DOM, and messages to and from them have a copying cost.
How do I know a fix worked?
In the lab, Total Blocking Time should fall and the long task should disappear from a DevTools performance trace. In the field, watch the input delay portion of Interaction to Next Paint over the following weeks, since field data is collected from real visits and reported over a rolling period.