Back to Performance
Performance
medium
mid

If an API returns 5000 records, how would you display them efficiently in a dropdown?

Don't render 5000 DOM nodes. Combine: server-side search/pagination, async incremental load, virtualization (react-window / TanStack Virtual), and a debounced filter input. Most apps need only the last three; large lists need all four.

7 min read·~25 min to think through

Rendering 5000 <option> or <li> nodes destroys layout/paint and is unusable on mobile. The combined fix:

  1. Search-first, list-second. Most users want one of a handful of items. Render an input that filters the list — the dropdown only shows ~20 matches. Combobox UX (downshift, headlessui Combobox) is the right primitive.
  2. Virtualization. Even with filtering, the worst case is "no filter" — 5000 rows. Render only the visible window plus a small overscan. Use react-window or @tanstack/react-virtual.
  3. Server-side pagination/search. If the dataset can grow, push search to the server and stream the next page on scroll. Don't bring 5000 back to the client when you'll show 20.
  4. Debounce input. 200–300ms keeps typing fluid; without it you re-filter 5000 records per keystroke.
  5. Index for fast filter. A pre-built lowercased index avoids .toLowerCase() on every keystroke. Fuse.js for fuzzy search.
  6. Accessibility. Combobox needs proper ARIA: role="combobox", aria-expanded, aria-activedescendant, keyboard navigation. Custom dropdowns lose this routinely.

Mobile considerations: native <select> is faster than any custom dropdown for simple cases — use it unless the design requires custom rendering.

Code

tsx
import { useVirtualizer } from "@tanstack/react-virtual";

function Combo({ all }: { all: Item[] }) {
  const [q, setQ] = useState("");
  const debounced = useDebounce(q, 200);
  const filtered = useMemo(
    () => debounced ? all.filter((i) => i.name.toLowerCase().includes(debounced.toLowerCase())) : all,
    [all, debounced],
  );

  const parentRef = useRef<HTMLDivElement>(null);
  const v = useVirtualizer({
    count: filtered.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 32,
    overscan: 8,
  });

  return (
    <>
      <input value={q} onChange={(e) => setQ(e.target.value)} aria-autocomplete="list" />
      <div ref={parentRef} role="listbox" style={{ height: 320, overflow: "auto" }}>
        <div style={{ height: v.getTotalSize(), position: "relative" }}>
          {v.getVirtualItems().map((row) => (
            <div
              key={row.key}
              role="option"
              style={{
                position: "absolute", top: 0, left: 0, right: 0,
                transform: `translateY(${row.start}px)`,
                height: row.size,
              }}
            >
              {filtered[row.index].name}
            </div>
          ))}
        </div>
      </div>
    </>
  );
}
Virtualized + debounced combobox skeleton

Follow-up questions

  • How does virtualization affect screen-reader navigation, and how do you mitigate it?
  • When is an HTML <select> better than a custom combobox?
  • How would you implement multi-select with the same constraints?

Common mistakes

  • Trusting the design and rendering 5000 nodes — okay on desktop, dies on mobile.
  • Filtering on the client when the dataset can grow without bound.
  • Building a custom dropdown without ARIA, breaking screen readers.

Performance considerations

  • Virtualization keeps DOM size bounded; CPU per scroll is overscan + visible count.
  • Filtering 5000 strings is fast; what kills perf is recreating React subtrees per keystroke without virtualization.

Edge cases

  • Variable-height rows — use `dynamicSizeList` (react-window) or the `measureElement` API in TanStack Virtual.
  • Selected option scroll-into-view on open — call `scrollToIndex` after mount.

Real-world examples

  • Linear's combobox, GitHub's user-picker, and Notion's mention menu are all virtualized + server-search comboboxes.

Senior engineer discussion

Senior signal: discuss tradeoffs between client-side filtering (instant) and server-side search (scales), ARIA combobox patterns, and how IME composition affects keystroke handling on East Asian inputs.

Related questions