From harness-claude
Measures and optimizes INP (Interaction to Next Paint) by decomposing interactions into input delay, processing time, and presentation delay. Use for sluggish UI, long tasks during interactions, or CrUX INP >200ms.
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Measure and optimize INP — the worst-case interaction latency across the entire page session — by decomposing each interaction into input delay, processing time, and presentation delay, then targeting each phase with yielding, scheduling, and rendering strategies.
Guides optimization of Core Web Vitals (LCP, INP, CLS) for improved page experience and search rankings. Covers diagnostics, thresholds, and fixes for loading, responsiveness, and visual stability.
Detects and eliminates long tasks (>50ms on main thread) using PerformanceObserver, scheduler.yield(), postTask(), Web Workers, and requestIdleCallback to ensure UI responsiveness.
Conducts web performance audits with Core Web Vitals (LCP, FID, CLS, INP), Lighthouse automation, bottleneck identification, and optimization recommendations for page load times and UX issues.
Share bugs, ideas, or general feedback.
Measure and optimize INP — the worst-case interaction latency across the entire page session — by decomposing each interaction into input delay, processing time, and presentation delay, then targeting each phase with yielding, scheduling, and rendering strategies.
scheduler.yield() or scheduler.postTask() could break up long processing in event handlersUnderstand how INP differs from FID. FID measured only the input delay of the first interaction. INP measures the full latency (input delay + processing time + presentation delay) of all interactions throughout the page session, and reports the worst interaction (approximately — it uses a high-percentile heuristic to avoid outliers).
Decompose interaction into 3 phases:
Reduce input delay by avoiding long tasks. If a 200ms task is running when the user clicks, the click handler cannot start until that task finishes:
// BAD — monolithic initialization blocks all interactions for 500ms
function initialize() {
processData(); // 200ms
buildIndex(); // 150ms
renderWidgets(); // 150ms
}
// GOOD — yield between chunks so user input can be processed
async function initialize() {
processData();
await scheduler.yield(); // let pending input events execute
buildIndex();
await scheduler.yield();
renderWidgets();
}
Reduce processing time by keeping event handlers lean. Move non-essential work out of the synchronous event handler:
// BAD — analytics + validation + DOM update all synchronous in click handler
button.addEventListener('click', () => {
trackAnalytics(event); // 50ms
validateForm(); // 100ms
updateDOM(); // 30ms
}); // Total: 180ms processing time
// GOOD — only essential work synchronous, defer the rest
button.addEventListener('click', () => {
updateDOM(); // 30ms — user sees immediate feedback
requestIdleCallback(() => {
trackAnalytics(event); // runs when idle
validateForm(); // runs when idle
});
}); // Processing time: 30ms
Reduce presentation delay. Minimize the work between event handler completion and the next paint:
content-visibility: auto to reduce rendering cost for off-screen contentUse event delegation to reduce event handler overhead. Instead of attaching listeners to each list item, delegate to the parent:
// BAD — 500 event listeners for 500 items
items.forEach((item) => item.addEventListener('click', handleClick));
// GOOD — 1 event listener, delegate based on target
list.addEventListener('click', (e) => {
const item = e.target.closest('.item');
if (item) handleClick(item);
});
Measure INP with the Performance API:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
// entry.duration = total interaction latency (input delay + processing + presentation)
// entry.processingStart - entry.startTime = input delay
// entry.processingEnd - entry.processingStart = processing time
// entry.startTime + entry.duration - entry.processingEnd = presentation delay
console.log('INP candidate:', entry.duration, 'Target:', entry.target);
}
});
observer.observe({ type: 'event', buffered: true, durationThreshold: 16 });
| Rating | INP (p75) | Perception |
|---|---|---|
| Good | <= 200ms | Interface feels responsive |
| Needs improvement | <= 500ms | Noticeable lag |
| Poor | > 500ms | Interface feels broken |
INP is measured at the 75th percentile of all interactions during a page session. On pages with fewer than 50 interactions, the worst interaction is used. On pages with 50+ interactions, the 98th percentile is used (approximately the worst 1-2 interactions).
Redbus improved INP from 657ms to 164ms by breaking a monolithic click handler into yielding chunks. The original handler executed a 400ms data processing function synchronously on bus route selection click. The fix:
await scheduler.yield() between each phaseTesco reduced INP by 50% by moving analytics event processing from synchronous click handlers to requestIdleCallback. Every product click triggered synchronous analytics calls that serialized product data, computed session metrics, and prepared beacon payloads — totaling 80ms of processing. Moving to requestIdleCallback reduced click handler processing to 5ms (just the DOM update), with analytics work deferred to idle periods.
INP does not simply report the single worst interaction (which could be an outlier). Instead:
This makes INP resilient to one-off anomalies while still capturing consistently slow interactions.
Synchronous DOM manipulation in event handlers. Large DOM updates in a click handler force the browser to perform style, layout, and paint synchronously before the next frame. Use requestAnimationFrame to batch visual updates.
Heavy computation in input handlers without yielding. A click handler that processes 10,000 data records without yielding blocks the main thread for the entire duration. No other interactions can be processed during this time.
Individual event listeners on hundreds of list items. Besides the memory cost of hundreds of closures, the browser must evaluate each listener binding during event dispatch. Event delegation on the parent element reduces both memory and dispatch overhead.
Running requestAnimationFrame work inside click handlers. requestAnimationFrame callbacks execute before the next paint but after the current task. Scheduling heavy work in rAF from a click handler delays the visual update to the frame after next, increasing presentation delay.
Ignoring input delay in favor of processing time. Many developers focus on optimizing their event handler code but ignore that a 300ms long task running before the user clicks creates 300ms of input delay before the handler even starts. Reducing background long tasks is equally important.