INP Optimization

A compact comparison panel for optimization techniques that lower Interaction to Next Paint. Pass a list of techniques ({ id, label, beforeMs, afterMs }); the widget surfaces a tablist, a side-by-side before / after comparison with proportional bars scaled against the larger of the two totals, and a summary line that quantifies the savings.

Preview

Pick an optimization

Each technique brings the same interaction under the web.dev INP good band.

Break the click handler with scheduler.yield() so the browser can paint between work chunks.

Before420ms
After180ms
Interaction latency dropped by 240ms (57%).After: 180ms · target ≤ 0.2s.
Customize
Yield on input
420ms
180ms
Defer to rAF
380ms
170ms
Isolate long task
620ms
160ms
Options

Installation

npx shadcn@latest add https://craftbits.dev/r/inp-optimization.json

Usage

import { InpOptimization } from "@craft-bits/core";
 
<InpOptimization
  title="Pick an optimization"
  techniques={[
    { id: "yield-on-input", label: "Yield on input", beforeMs: 420, afterMs: 180 },
    { id: "raf-paint", label: "Defer to rAF", beforeMs: 380, afterMs: 170 },
    { id: "isolate-loaf", label: "Isolate long task", beforeMs: 620, afterMs: 160 },
  ]}
/>

Controlled mode — drive the active technique from outside so the choice can feed an external lesson stepper:

const [active, setActive] = useState("yield-on-input");
 
<InpOptimization
  techniques={TECHNIQUES}
  activeTechnique={active}
  onActiveTechniqueChange={setActive}
/>

Anatomy

  • Header. Optional title and description.
  • Tablist. One pill per technique. The active pill picks up an inset accent shadow and data-cb-active.
  • Before / After columns. Two side-by-side cards with verdict-toned totals and proportional bars on a shared scale, so the saving is visible at a glance.
  • Summary. Absolute savings (ms), relative reduction (%), after-total, and the target ≤ line. Verdict tone tracks the after-total.

Understanding the component

  1. Bars share a scale. Both columns scale their bars against max(beforeMs, afterMs) so the after-bar reads as a fraction of the before-bar, not as a fraction of itself.
  2. Verdict bucket. Each total is graded against goodMs (default 200) and poorMs (default 500). good paints --cb-success, needs-improvement paints --cb-warning, poor paints --cb-error.
  3. Controlled + uncontrolled. Provide activeTechnique with onActiveTechniqueChange, or defaultActiveTechnique for the uncontrolled case. Omit both and the first technique is active.
  4. Motion. Bar fills animate with the snap spring on every technique change and short-circuit under prefers-reduced-motion.

Props

PropTypeDefaultDescription
techniquesInpOptimizationTechnique[]requiredThe techniques to compare.
activeTechniquestringControlled active-technique id.
defaultActiveTechniquestringfirst idInitial active id in uncontrolled mode.
onActiveTechniqueChange(next) => voidFires on tab change.
titleReactNodeOptional heading above the panel.
descriptionReactNodeOptional sub-headline under the title.
headingAs'h2' | 'h3' | 'h4''h3'Tag for the title element.
goodMsnumber200Threshold for the good verdict.
poorMsnumber500Threshold for the poor verdict.
beforeLabelReactNode'Before'Label on the "before" column.
afterLabelReactNode'After'Label on the "after" column.
hideSummarybooleanfalseHide the savings summary line.
tablistAriaLabelstring'INP optimization'ARIA label for the tablist.
classNamestringMerged onto the root via cn().

InpOptimizationTechnique

FieldTypeDescription
idstringStable identifier. Surfaces via data-cb-tech.
labelReactNodeDisplay label on the tab.
beforeMsnumberTotal interaction latency before the technique, in ms.
afterMsnumberTotal interaction latency after the technique, in ms.
descriptionReactNodeOptional one-line description shown under the tablist.

Accessibility

  • The wrapper is a <section> with data-cb-edu="inp-optimization" and data-cb-verdict="good" | "needs-improvement" | "poor" tracking the after-total.
  • The tablist uses native role="tablist" + role="tab"; the active tab carries aria-selected="true".
  • Each bar is a role="progressbar" with aria-valuenow / aria-valuemin / aria-valuemax and a descriptive aria-label.
  • The summary row is aria-live="polite" so screen readers announce the savings on tab change.
  • Bar fills short-circuit under prefers-reduced-motion.