Fix INP in Remix
Interaction to Next Paint (INP) is a critical Core Web Vital that measures how quickly the main content of your page becomes visible. In Remix applications, common issues include unoptimized images, render-blocking resources, and inefficient data loading patterns that delay the largest visible element.
This guide walks through five targeted fixes for INP in Remix, with real code examples and before/after performance comparisons. Each step addresses a specific bottleneck in the Remix rendering pipeline.
Expected results
Following all five steps typically produces these improvements:
Before
340ms
INP score (Needs Improvement) -- slow form submissions, heavy event handlers, blocking transitions, large DOM operations
After
95ms
INP score (Good) -- optimistic UI, debounced handlers, non-blocking transitions, efficient DOM updates
Step-by-step fix
Use optimistic UI for instant feedback on form submissions
Remix's useFetcher and useNavigation hooks enable optimistic UI patterns where the interface updates immediately upon user interaction, before the server responds. This dramatically reduces perceived INP because the visual update happens synchronously while the actual mutation runs in the background.
// Blocking form -- user waits for server response
export default function LikeButton({ postId }: { postId: string }) {
return (
);
// Button does nothing until server responds (300-500ms)
}
import { useFetcher } from "@remix-run/react";
export default function LikeButton({ postId, liked }: {
postId: string;
liked: boolean;
}) {
const fetcher = useFetcher();
// Optimistic: assume success immediately
const isLiked = fetcher.formData
? fetcher.formData.get("liked") === "true"
: liked;
return (
);
// UI updates instantly on click -- INP measured at ~16ms
}
Debounce search inputs and rapid-fire events
Search boxes, auto-complete fields, and filter inputs that trigger fetches on every keystroke create rapid successive interactions that compete for main thread time. Debounce these inputs to batch updates and reduce the number of network requests and DOM updates.
import { useFetcher } from "@remix-run/react";
import { useRef, useCallback } from "react";
export function SearchInput() {
const fetcher = useFetcher();
const timeoutRef = useRef();
const handleInput = useCallback((e: React.ChangeEvent) => {
// Clear previous timer
clearTimeout(timeoutRef.current);
// Wait 300ms after last keystroke before fetching
timeoutRef.current = setTimeout(() => {
fetcher.submit(
{ q: e.target.value },
{ method: "get", action: "/api/search" }
);
}, 300);
}, [fetcher]);
return (
{fetcher.data && (
{fetcher.data.results.map((result: any) => (
- {result.title}
))}
)}
);
}
Break up long tasks with yield-to-main-thread patterns
Any JavaScript task running longer than 50ms blocks the main thread and delays the browser's ability to process user interactions. In Remix route components, break up expensive computations (data transformation, DOM manipulation, sorting large lists) by yielding to the main thread between chunks.
// Utility: yield control back to the browser
function yieldToMain(): Promise {
return new Promise((resolve) => {
if (typeof scheduler !== "undefined" && scheduler.yield) {
// Use scheduler.yield if available (Chrome 115+)
scheduler.yield().then(resolve);
} else {
setTimeout(resolve, 0);
}
});
}
// Process large list in chunks
async function processItems(items: Item[]) {
const CHUNK_SIZE = 50;
const results: ProcessedItem[] = [];
for (let i = 0; i < items.length; i += CHUNK_SIZE) {
const chunk = items.slice(i, i + CHUNK_SIZE);
results.push(...chunk.map(processItem));
// Yield after each chunk so interactions can be processed
if (i + CHUNK_SIZE < items.length) {
await yieldToMain();
}
}
return results;
}
Show navigation progress for route transitions
Remix route transitions can feel slow when navigating to pages with slow loaders. Use useNavigation to show a progress indicator immediately when the user clicks a link, providing visual feedback that reduces perceived latency. The interaction (click) gets a fast visual response even if the data takes time to load.
import { useNavigation } from "@remix-run/react";
export function GlobalProgress() {
const navigation = useNavigation();
const isNavigating = navigation.state !== "idle";
return (
);
}
/* CSS */
/*
.progress-bar {
position: fixed;
top: 0;
left: 0;
height: 3px;
background: var(--color-accent);
width: 0;
opacity: 0;
transition: width 0.5s ease, opacity 0.2s;
z-index: 9999;
}
.progress-bar.active {
opacity: 1;
width: 80%;
animation: progress 2s ease-in-out;
}
*/
Minimize client-side JavaScript and third-party scripts
Every kilobyte of JavaScript adds to main thread processing time, which directly impacts INP. Audit your client-side JavaScript: remove unused dependencies, defer analytics scripts, and lazy-load components that are not needed for initial interactivity. Use Remix's server-side capabilities to move computation off the client.
import { useEffect } from "react";
export function DeferredAnalytics() {
useEffect(() => {
// Load analytics after page is interactive
const timer = setTimeout(() => {
const script = document.createElement("script");
script.src = "https://analytics.example.com/tracker.js";
script.async = true;
document.body.appendChild(script);
}, 3000); // Delay 3 seconds after mount
return () => clearTimeout(timer);
}, []);
return null;
}
// In root.tsx:
//
Quick checklist
-
Form submissions use optimistic UI via
useFetcher - Search inputs debounced to 200-300ms
- Long tasks broken into chunks with yield-to-main-thread
-
Navigation progress indicator via
useNavigation - Third-party scripts deferred until after page load
- Heavy computation moved to server (loaders/actions)
-
No synchronous
localStorage/sessionStoragein event handlers
Frequently asked questions
A well-optimized Remix application should achieve INP under 150ms. Remix's server-first architecture means most data mutations happen on the server, reducing client-side JavaScript execution. With optimistic UI patterns and debounced inputs, INP under 100ms is achievable for most interactions.
Remix uses progressive enhancement with HTML forms as the primary interaction model. Form submissions are handled by server-side action functions, which means the heavy computation happens on the server rather than the client. Combined with optimistic UI via useFetcher, the browser can process the click event and update the UI in a single frame (~16ms).
Yes. Remix prefetches route data when a user hovers over a link (with intent prefetching), so by the time they click, the data may already be loaded. This makes navigation transitions faster. For routes with slow loaders, use the useNavigation hook to show a progress indicator immediately, ensuring the click interaction gets a fast visual response.
Use Chrome DevTools Performance panel to record interactions. Look for Long Tasks (marked with red corners) that overlap with your click/keypress events. The Interactions track shows the exact INP measurement. Filter by the interaction type (click, keypress) to identify which event handlers are slow. Check for expensive React re-renders using React DevTools Profiler.
Yes, this is one of Remix's core strengths. Move data processing, filtering, sorting, and validation to loader and action functions that run on the server. The client only receives the processed result. This reduces main thread work and directly improves INP. Use fetcher.submit to trigger server-side processing without full page navigation.
Use Chrome DevTools Performance panel with CPU throttling (4x slowdown) to simulate mid-range mobile devices. Interact with the page (click buttons, type in inputs, open menus) and look for long tasks in the flame chart. The Web Vitals Chrome Extension shows real-time INP scores as you interact. For Remix, pay attention to hydration-related interaction delays.
Continue learning
Fix LCP in Remix
Related performance optimization for the same framework.
GuideComplete INP Guide
Deep dive into INP -- thresholds, measurement, and optimization strategies.
FixFix INP in Next.js
Compare INP fixes across different frameworks.
ToolCWV Score Explainer
Enter your scores for personalized fix recommendations.
BenchmarkNext.js vs Remix 2026 Performance
See how Remix 3.1 INP compares to Next.js 15.2 in head-to-head benchmarks.