Ever wondered why a perfectly designed component library feels 'heavy' once it hits production? We've all been there: the buttons look great, the accessibility scores are 100, but the moment a user clicks a dropdown or searches a list, the UI stutters. This is where Interaction to Next Paint (INP) comes into play.
Since it replaced First Input Delay (FID) as a Core Web Vital in early 2024, INP has changed the game for those of us building design systems. We can no longer just worry about the first click; we have to care about every click, tap, and keypress across the entire lifespan of a user's visit.
What is INP, anyway?
According to Google's official documentation, INP assesses a page's overall responsiveness by observing the latency of all interactions. It doesn't just look at the average; it reports the longest interaction observed (minus a few outliers).
The budget is tight. To get a 'Good' rating, your interactions need to result in a new frame in under 200ms. If you hit 500ms, you're officially in the 'Poor' territory. For those of us building shared components, this means our code needs to be incredibly lean.
The Design System Trap
The biggest mistake I see senior engineers making is treating INP as a page-level problem. If you wait until a page is built to fix responsiveness, you're playing whack-a-mole. The real solution is to bake performance patterns into the design system itself.
Design systems often bloat INP by packing too much into event handlers. We add analytics, state management updates, prop drilling, and DOM queries all into a single onClick. This blocks the main thread and delays the 'next paint'—which is exactly what INP measures.
Strategy 1: The 'Yield to Main Thread' Pattern
One of the most effective ways to keep your components snappy is to yield to the main thread. If a component has to do something heavy (like processing a large list), don't do it all at once. Break it up so the browser has a chance to paint the UI.
async function handleSearch(query) {
// 1. Immediate visual feedback (the 'Next Paint')
setLoading(true);
// 2. Yield so the browser can actually show the loading state
await new Promise(resolve => setTimeout(resolve, 0));
// 3. Perform the heavy filtering logic
const results = performHeavySearch(query);
setResults(results);
}
By using a simple setTimeout or requestAnimationFrame, you ensure the user sees the 'pressed' or 'loading' state immediately. This keeps your INP low because the 'next paint' happens quickly, even if the data takes a bit longer to process.
Strategy 2: Component-Level INP Monitoring
I'm a big fan of including performance monitoring directly in the design system's 'provider' or base components. You can use the web-vitals library to track which components are causing delays in the real world.
import { onINP } from 'web-vitals';
export function PerformanceMonitor() {
onINP((metric) => {
console.log(`Interaction: ${metric.name}, Value: ${metric.value}ms`);
// Send to your analytics dashboard
sendToAnalytics({
component: metric.entries[0].target.getAttribute('data-ds-component'),
latency: metric.value
});
});
return null;
}
If every button in your system has a data-ds-component="Button" attribute, you'll quickly see which components are the biggest offenders across your entire site.
Common Pitfalls to Avoid
- Synchronous Dialogs: Avoid
alert()orconfirm(). They block the main thread entirely and destroy your INP score. - Heavy ResizeObservers: If your layout components use
ResizeObserver, keep the callbacks light. Complex math inside these can delay the paint of the entire page. - Over-reliance on rAF:
requestAnimationFrameis great for animations, but if you put heavy logic inside it, you're still blocking the frame from rendering.
Wrapping Up
- Treat the 200ms INP threshold as a hard budget for all component interactions.
- Use yielding patterns like
setTimeout(0)to separate visual feedback from heavy logic. - Tag your components with data attributes to track real-world performance via RUM tools.
I've found that when we stop thinking about performance as a 'final polish' step and start building it into our Button and Input components, the entire user experience transforms. It's about being proactive rather than reactive.
If you want to go deeper and learn how to build real, production-ready CSS design systems step by step, check out my full course here: CSS Design Systems Course
The full story including the testing layers and governance lives in Ship Your Design System on Amazon.
Let's chat more about performance on Twitter or connect with me on LinkedIn. I'd love to hear how you're tackling INP in your own projects!