TroubleshootingIntermediate

Reduce First Input Delay (FID)

FID measures interactivity and responsiveness. Reduce JavaScript execution time, optimize event handlers, eliminate long tasks, and deliver responsive user experiences.

13 min read
Atatus Team
Updated March 15, 2025
6 sections
01

Understanding First Input Delay and INP

FID and its successor INP measure how responsive a page is to user interactions.

First Input Delay (FID) measures the delay between when a user first interacts with a page (clicks a button, presses a key, taps a link) and when the browser is able to begin processing that interaction's event handler. FID is a real-user metric—it requires an actual user to interact with the page and cannot be measured in lab settings without simulated interactions. Google's threshold for Good FID is under 100ms, Needs Improvement is 100 to 300ms, and Poor is above 300ms. FID was replaced by Interaction to Next Paint (INP) as a Core Web Vitals metric in March 2024.

Interaction to Next Paint (INP) measures the latency of all interactions throughout the entire page lifetime, reporting the worst interaction (excluding outliers) as the page's INP score. This is more comprehensive than FID, which measures only the first interaction. A page might have excellent FID (the first click responds quickly) but poor INP (subsequent interactions during heavy JavaScript execution are slow). Good INP is under 200ms, Needs Improvement is 200 to 500ms, and Poor is above 500ms. Improving your FID score and the practices that drive it also improves INP.

Both FID and INP are caused by the same root issue: the browser's main thread is occupied with JavaScript execution when the user tries to interact. The main thread in the browser handles JavaScript execution, layout calculation, style calculation, painting, and event handler invocation. When a long task (JavaScript execution exceeding 50ms on the main thread) is running, user interactions are queued waiting for the long task to complete. The user experiences this as the UI not responding to their input—buttons that don't visually respond to clicks, text fields that don't show typed characters.

Long tasks are defined as JavaScript executions that take more than 50ms on the main thread. The browser's Long Tasks API reports these as PerformanceLongTaskTiming entries. A 300ms long task means any user interaction that starts during those 300ms will have at least 300ms of input delay. Heavy JavaScript execution during page load—bundle parsing, framework hydration, analytics initialization, third-party script execution—generates long tasks that block the main thread precisely when users are most likely to first interact with the page.

02

Track User Interaction Delays

Measuring interaction delays in production reveals the real-world impact on user experience.

Measure FID and INP from real users using the Web Vitals JavaScript library, which provides a onFID() and onINP() callback that fires with the metric value when it is finalized. FID is finalized on the first user interaction; INP is updated throughout the page session and finalized when the page is hidden. Send both metrics to your analytics or APM platform along with the event target (which element the user interacted with), the interaction type (click, keyboard, pointer), and the interaction time, which enables debugging specific interaction patterns that have high INP.

Track FID and INP distribution across page templates and device types, as mobile devices have significantly higher main thread blocking times than desktop computers. A desktop computer with a fast processor handles JavaScript-heavy frameworks with under 100ms FID, while the same page on a mid-range Android phone may have 400ms FID because the phone has 5 to 10 times less JavaScript execution speed. Optimizing for P75 FID and INP on mobile is the appropriate target, as it represents the majority of users and the worst-case hardware you need to support.

Long task monitoring through the PerformanceObserver API provides continuous visibility into main thread blocking. Observe entries of type 'longtask' and log any long task exceeding 50ms with the task duration and attribution (which script or frame caused it). Aggregate long task data by duration category (50-100ms, 100-250ms, 250ms+) to understand the severity of main thread blocking on your pages. A page with a 500ms long task during hydration will have 500ms FID for any user who happens to click during that 500ms window.

Correlate FID and INP degradation with specific JavaScript bundles, third-party scripts, and deployment events. When FID increases after a deployment, identifying which new JavaScript code introduced long tasks requires correlating the deployment timing with long task attribution data. APM tools that capture JavaScript execution profiling in production (with sampling to limit overhead) can identify specific function call chains that exceed the long task threshold and were introduced by recent code changes.

03

Reduce JavaScript Execution Time

Long JavaScript execution is the direct cause of poor FID—reducing it is the most impactful improvement.

JavaScript bundle size reduction directly reduces parse and compile time, which is pure main thread blocking time before any code executes. A 1MB JavaScript bundle on a mid-range mobile device takes 3 to 5 seconds to parse and compile—all on the main thread. Reducing this to 300KB through code splitting, tree shaking, and removing unused dependencies can cut parse/compile time to under 1 second. Target a total JavaScript budget of 300KB gzipped for the initial page load and lazy-load all other JavaScript when needed for specific interactions.

JavaScript execution patterns after parsing also contribute to FID and INP. Component hydration in React, Vue, and Angular SPA frameworks executes JavaScript to attach event listeners and initialize component state for all server-rendered HTML elements. In a complex application with hundreds of components, this hydration work can take 500ms to 3 seconds on mobile devices—creating a long window where the page looks loaded but is not interactive. React 18's concurrent hydration (createRoot) spreads hydration work across multiple frames, reducing the longest single blocking task.

Third-party scripts that initialize synchronously during page load are a major source of long tasks that are often overlooked because they are not application code. Analytics scripts, tag managers, A/B testing frameworks, chat widgets, and advertising scripts execute JavaScript that can monopolize the main thread for 100 to 500ms each. Audit every third-party script on your pages using Chrome DevTools Coverage panel to measure JavaScript execution time per script. Consider deferring third-party script initialization until after the page is interactive (postMessage after first interaction or after a timeout).

Polyfills for older browser features add execution overhead for all users, including modern browser users who do not need them. A polyfill bundle that includes Array.prototype.includes, Promise, fetch, and ResizeObserver adds 100 to 200KB and significant execution overhead, even on browsers that natively support all these features. Use differential serving to deliver minimal polyfill bundles to modern browsers (using module/nomodule technique) and full polyfill bundles only to legacy browsers. This can reduce JavaScript execution time by 20 to 40% for modern browser users.

04

Optimize Main Thread Blocking

Reducing main thread blocking directly reduces input delay for all user interactions.

Break up long synchronous tasks into smaller chunks that yield control back to the browser's event loop, allowing input processing between chunks. A data processing function that takes 500ms synchronously can be broken into 10 chunks of 50ms each, with a 0ms setTimeout between each chunk. While this extends total processing time slightly, it ensures that user inputs during the processing are handled within 50ms of being received rather than waiting up to 500ms. Use scheduler.postTask() (Chrome 94+) or requestIdleCallback() for low-priority work that should not interfere with user interactions.

Web Workers allow JavaScript execution in background threads that do not block the main thread. Compute-intensive operations—data parsing, image processing, encryption, compression, and algorithmic computations—can be moved to Web Workers, freeing the main thread for rendering and event handling. Communication between the main thread and Web Workers uses message passing, which requires structuring your code to separate computation from UI manipulation (which must stay on the main thread). Libraries like Comlink simplify the worker communication API, making Web Worker adoption more accessible.

React's useDeferredValue and useTransition hooks allow you to mark certain state updates as non-urgent, allowing the browser to process urgent updates (like typing in an input field) with priority while deferring the less urgent rendering updates (like updating a search results list) for when the main thread has capacity. Without these hooks, every state update triggers a synchronous re-render that can block the main thread. With useTransition, a search input remains responsive even when each keystroke triggers a large list re-render, because the browser processes the input event before re-rendering the list.

Optimize event handler performance for frequently-triggered events: scroll, resize, mousemove, and touchmove events can fire at 60fps or higher, meaning your event handlers execute dozens of times per second. Even a 10ms event handler executing 60 times per second consumes 600ms of main thread time per second. Use passive event listeners (addEventListener(..., { passive: true })) for scroll and touch events that do not call preventDefault(), debounce expensive handler logic so it executes only once after a series of events, and move scroll-triggered animations to CSS or use the IntersectionObserver API instead of scroll event listeners.

05

Implement Code Splitting and Lazy Loading

Loading less JavaScript upfront reduces the main thread blocking that delays interactivity.

Route-based code splitting ensures that users only download and parse JavaScript for the current route on page load. A single-page application with 20 routes should not require all 20 routes' JavaScript to be downloaded and parsed on the initial load. Using dynamic import() with React.lazy(), Vue's async components, or Angular's lazy-loaded modules splits route bundles automatically. The JavaScript bundle for route A is not downloaded until the user navigates to route A, keeping the initial bundle lean and reducing main thread blocking during page load.

Interaction-based code splitting defers loading JavaScript for features that users only access occasionally. A modal dialog, an advanced search filter panel, a data export function, or a rich text editor are all features that users may never access during a session. Rather than loading their JavaScript with the initial page bundle, load them dynamically when the user first triggers the feature. A dynamic import(() => import('./RichTextEditor')) on first click of the 'Edit' button adds a small loading delay on first interaction but saves 100 to 300KB from the initial bundle for all users, most of whom will not use the editor.

Critical CSS extraction and defer loading of non-critical CSS reduces render-blocking CSS that itself can block interactivity during initial parse. While CSS blocking is primarily a LCP concern, eliminating render-blocking resources from the critical path also allows JavaScript to execute sooner. The faster your initial HTML and critical CSS are parsed, the sooner the browser can begin executing the JavaScript that powers interactivity, and the sooner the page becomes available to respond to user input.

Preloading critical JavaScript resources that are needed for interactivity reduces the time from page start to interactive by downloading JavaScript with the browser's highest priority. The preload link tag with as=script tells the browser to download a specific JavaScript file at high priority during the initial page load, even before the browser discovers it through script tags in the HTML. Preloading your main application bundle and any bundles needed for immediately-interactive features ensures that interactivity is available as quickly as possible after the page starts loading.

06

Monitor Interaction Responsiveness Continuously

Ongoing monitoring ensures FID and INP improvements are maintained as the application evolves.

Include FID and INP in your Core Web Vitals monitoring dashboard alongside LCP and CLS. Track trends over time to detect gradual degradation as new JavaScript features are added, dependencies are updated, and third-party scripts change their behavior. Long-term INP trends are particularly important because INP measures interaction responsiveness throughout the page session, not just on first load, and can degrade as more interactive features are added to existing pages without optimization consideration.

Performance audits after each release should include FID and INP measurements from synthetic testing with simulated CPU throttling to represent mid-range mobile devices. Chrome DevTools' Performance panel with 4x CPU throttle simulates a mid-range Android phone's JavaScript execution speed. Record a performance profile during a typical user session (clicking common buttons, navigating between sections, submitting forms) and identify all long tasks and their attribution. This reveals interaction responsiveness issues with production code before they are confirmed in real user data.

Real user interaction data provides ground truth for optimization validation. After implementing a JavaScript performance optimization, compare pre-optimization and post-optimization INP distributions from real users over equivalent time periods. A change that reduces a long task from 400ms to 150ms in a synthetic benchmark may reduce real-world INP more or less than expected, depending on whether the optimization affects the interactions users actually perform most frequently. Real user data validates the magnitude and direction of improvement.

Establish INP budgets for each page type and track SLO compliance as a product health metric. Set targets such as P75 INP under 200ms for all pages, and P99 INP under 500ms for critical interactive pages like checkout and search. Report INP SLO compliance in engineering reviews alongside error rates and availability metrics. When INP SLO compliance decreases—meaning more users are experiencing slow interactions—it triggers investigation of what code or infrastructure change caused the regression, with the same urgency as a declining availability SLO.

Key Takeaways

  • FID and INP are caused by JavaScript blocking the main thread during user interactions—long tasks (JavaScript exceeding 50ms) are the direct root cause to eliminate
  • INP replaced FID as a Core Web Vitals metric in March 2024—INP measures all interactions throughout the page session, not just the first, making it a more comprehensive interactivity metric
  • Code splitting (route-based and interaction-based) reduces initial JavaScript bundle size, directly cutting parse/compile time that blocks the main thread and prevents early interactions
  • Web Workers offload CPU-intensive operations to background threads—parsing large datasets, cryptography, and compression are good candidates that currently block the main thread unnecessarily
  • Third-party scripts are often the dominant source of long tasks on page load—audit all third-party scripts and defer their initialization until after the page is interactive
  • React useTransition and useDeferredValue hooks enable prioritized rendering that keeps input fields responsive even when state updates trigger expensive re-renders of large components
Get started today

Monitor your applications with Atatus

Put the concepts from this guide into practice. Set up full-stack observability in minutes with no credit card required.

No credit card required14-day free trialSetup in minutes

Related guides