Performance
doodleui-react draws borders with rough.js, which is heavier than a CSS border. On a single button that cost is invisible. On a dashboard with dozens of sketchy rows, badges, and cards, it adds up — especially if components re-render for unrelated state.
This page covers patterns that keep dense UIs responsive.
Prefer fixed seeds in lists
Path generation is a pure function of size, roughness, seed, bowing, fill, and stroke. The library memoizes that work when those inputs are stable.
If you omit seed, each instance picks a random seed after mount. That forces an extra generation and defeats memoization across re-renders.
// Good — stable sketch across re-renders
{rows.map((row) => (
<Badge key={row.id} seed={row.id} roughness={1}>
{row.status}
</Badge>
))}
// Also good — shared deterministic seed for a whole panel
<Card seed={42} title="Summary">…</Card>
Use SketchSeedProvider when you want Shuffle-style redraws without scattering seeds.
Disable animation for dense tables
Draw-in animations are on by default. A 50-row table with staggered rules means many simultaneous stroke-dashoffset animations and temporary MutationObservers watching for path paint.
<DoodleUIProvider animate={false}>
<Table>{/* dense data */}</Table>
</DoodleUIProvider>
// Or per component
<Table animate={false}>…</Table>
<Badge animate={false}>Queued</Badge>
Keep animation on for sparse chrome (primary buttons, dialogs, alerts). Turn it off for bulk list chrome.
See Animation for reduced-motion behavior.
Lower roughness for dense UIs
Higher roughness (and patterned fills like hachure / cross-hatch) produce more SVG path data. For tables and badge grids, 0.75–1 usually looks sketchy enough and is cheaper than the default 1.5.
<DoodleUIProvider roughness={0.9} animate={false}>
<Table seed={7}>…</Table>
</DoodleUIProvider>
Solid fills are cheaper than hachure when you do not need the wash.
Keep list rows memo-friendly
Badge, Button, Card, Table row parts, and the underlying RoughSvg layer are wrapped in React.memo. They skip work when their props are shallow-equal.
That only helps if your list code does not defeat memoization:
// Prefer a stable sketch config
const sketch = useMemo(
() => ({ seed: 42, roughness: 1, animate: false as const }),
[],
);
{rows.map((row) => (
<TableRow key={row.id}>
<TableCell>
<Badge variant="accent" {...sketch}>
{row.status}
</Badge>
</TableCell>
</TableRow>
))}
Avoid putting unrelated state (search text, ticks, unrelated counters) inside the same component that maps hundreds of sketchy children without memoizing the row. Lift filters up, or extract a memoized OrderRow that only receives the row’s own data.
What the library already does
- rough.js drawables are memoized on visual inputs inside
RoughSvg - Unrelated parent re-renders do not regenerate paths when seed/size/roughness/etc. are unchanged
- Theme palette CSS reads are cached
- Draw-in observers disconnect after a short settle window
- Table sketch context values are referentially stable when sketch props are unchanged
Profiling tip
The docs app ships a heavy dashboard at /benchmark with React Profiler telemetry, unrelated re-render / single-row update buttons, and toggles for roughness, animation, and fixed vs random seeds. Use it with React DevTools Profiler when investigating a regression.