# Keep Keystroke Cost Low

**SHOULD** · **ID:** `performance-keystroke-cost` · **Category:** performance
**Source:** [Vercel](https://github.com/vercel-labs/agent-skills/blob/main/skills/web-design-guidelines/SKILL.md)
**Interactive version:** https://ui-guides-agent-rules.netlify.app/principles/performance-keystroke-cost

> SHOULD: Prefer uncontrolled inputs; make controlled loops cheap (keystroke cost)

Prefer uncontrolled inputs, and keep every controlled input cheap per keystroke

A controlled input turns every keypress into a React render of its owning component and everything below it. If that subtree is expensive — a chart, an editor, a formatted list — the input visibly lags behind the user's fingers. Uncontrolled inputs (defaultValue + a ref) let the DOM own the value, so typing costs zero renders. When you do need controlled state, keep the subtree it feeds trivial: hoist the state down to the input, memoize expensive children, or defer the derived work with useDeferredValue.

## Bad — do not do this

`performance-keystroke-cost-bad`

```tsx
import { useRef, useState } from 'react';

/** Deliberately expensive subtree: re-rendering it costs real milliseconds. */
function ExpensivePreview({ text }: { text: string }) {
  const renders = useRef(0);
  renders.current += 1;

  // Busy work — stands in for a chart, editor, or big formatted list.
  let sum = 0;
  for (let i = 0; i < 2_000_000; i++) sum += i % 7;
  void sum;

  return (
    <div className="mt-3 rounded-md bg-muted p-2">
      <div className="text-xs text-muted-foreground">Preview: {text || 'empty'}</div>
      <div className="mt-1 text-xs font-medium tabular-nums text-error">
        expensive renders: {renders.current}
      </div>
    </div>
  );
}

export function KeystrokeCostBad() {
  const [text, setText] = useState('');
  const keystrokes = useRef(0);

  return (
    <div className="w-full max-w-sm space-y-4">
      <div className="rounded-lg border border-border bg-card p-4">
        <label htmlFor="kc-bad" className="mb-1 block text-xs font-medium text-foreground">
          Search
        </label>
        <input
          id="kc-bad"
          value={text}
          onChange={(e) => {
            keystrokes.current += 1;
            setText(e.target.value);
          }}
          placeholder="Type quickly here..."
          className="w-full rounded-lg border border-border bg-background px-3 py-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
        />
        <div className="mt-2 text-xs tabular-nums text-muted-foreground">
          keystrokes: {keystrokes.current}
        </div>
        <ExpensivePreview text={text} />
      </div>
      <p className="text-xs text-error">
        Controlled input wired straight into an expensive subtree: one expensive render per keystroke
        (1:1). Type fast and the field lags behind your fingers.
      </p>
    </div>
  );
}
```

## Good — do this

`performance-keystroke-cost-good`

```tsx
import { useRef, useState } from 'react';

/** Same expensive subtree as the bad example — the difference is how often it runs. */
function ExpensivePreview({ text }: { text: string }) {
  const renders = useRef(0);
  renders.current += 1;

  let sum = 0;
  for (let i = 0; i < 2_000_000; i++) sum += i % 7;
  void sum;

  return (
    <div className="mt-3 rounded-md bg-muted p-2">
      <div className="text-xs text-muted-foreground">Preview: {text || 'empty'}</div>
      <div className="mt-1 text-xs font-medium tabular-nums text-success">
        expensive renders: {renders.current}
      </div>
    </div>
  );
}

export function KeystrokeCostGood() {
  // The input owns its own value (uncontrolled). React never re-renders while typing.
  const inputRef = useRef<HTMLInputElement>(null);
  const keystrokeRef = useRef(0);
  const keystrokeDisplayRef = useRef<HTMLDivElement>(null);

  // Only this commits a render — and only when the user asks for a result.
  const [submitted, setSubmitted] = useState('');

  return (
    <form
      className="w-full max-w-sm space-y-4"
      onSubmit={(e) => {
        e.preventDefault();
        setSubmitted(inputRef.current?.value ?? '');
      }}
    >
      <div className="rounded-lg border border-border bg-card p-4">
        <label htmlFor="kc-good" className="mb-1 block text-xs font-medium text-foreground">
          Search
        </label>
        <input
          id="kc-good"
          ref={inputRef}
          defaultValue=""
          onInput={() => {
            keystrokeRef.current += 1;
            // Direct DOM write — no state, no render.
            if (keystrokeDisplayRef.current) {
              keystrokeDisplayRef.current.textContent = `keystrokes: ${keystrokeRef.current}`;
            }
          }}
          placeholder="Type quickly here..."
          className="w-full rounded-lg border border-border bg-background px-3 py-2 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
        />
        <div ref={keystrokeDisplayRef} className="mt-2 text-xs tabular-nums text-muted-foreground">
          keystrokes: 0
        </div>
        <button
          type="submit"
          className="mt-2 rounded-lg bg-primary px-3 py-1.5 text-xs text-primary-foreground hover:bg-primary/90 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
        >
          Preview
        </button>
        <ExpensivePreview text={submitted} />
      </div>
      <p className="text-xs text-success">
        Uncontrolled input via <code>defaultValue</code> + ref: typing costs zero renders. The
        expensive subtree renders once per submit, not once per keystroke — the field keeps up with
        your fingers.
      </p>
    </form>
  );
}
```

## References

- [Uncontrolled inputs (React)](https://react.dev/reference/react-dom/components/input)
- [useDeferredValue](https://react.dev/reference/react/useDeferredValue)
