React Rendering Optimization
Patterns to reduce unnecessary computation and re-creation during React renders.
1. Don't Wrap Simple Expressions in useMemo
When an expression is simple and returns a primitive (boolean, number, string), useMemo overhead exceeds the expression cost.
Bad:
const isLoading = useMemo(() => {
return user.isLoading || notifications.isLoading;
}, [user.isLoading, notifications.isLoading]);
Good:
const isLoading = user.isLoading || notifications.isLoading;
2. Extract Default Non-Primitive Parameters to Constants
Default non-primitive values in memoized components create new instances every render, breaking memo().
Bad:
const UserAvatar = memo(function UserAvatar({ => {} }: { onClick?: () => void }) {
// ...
})
Good:
const NOOP = () => {};
const UserAvatar = memo(function UserAvatar({ }: { onClick?: () => void }) {
// ...
})
3. Hoist Static JSX Elements
Extract static JSX outside components to avoid re-creation on every render. Especially helpful for large static SVG nodes.
Bad:
function Container() {
return <div>{loading && <div className="animate-pulse h-20 bg-gray-200" />}</div>;
}
Good:
const loadingSkeleton = <div className="animate-pulse h-20 bg-gray-200" />;
function Container() {
return <div>{loading && loadingSkeleton}</div>;
}
If React Compiler is enabled, it handles this automatically.
4. Hoist RegExp Creation
Don't create RegExp inside render. Hoist to module scope or memoize.
Bad:
function Highlighter({ text, query }: Props) {
const regex = new RegExp(`(${query})`, 'gi')
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
}
Good:
const EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
function Highlighter({ text, query }: Props) {
const regex = useMemo(
() => new RegExp(`(${escapeRegex(query)})`, 'gi'),
[query]
)
const parts = text.split(regex)
return <>{parts.map((part, i) => ...)}</>
}
Warning: Global regex (
/g) has mutablelastIndexstate —regex.test('foo')returnstruethenfalsealternately.
5. Calculate Derived State During Rendering
If a value can be computed from current props/state, derive it during render instead of storing in state + syncing with useEffect.
Bad:
const [fullName, setFullName] = useState('');
useEffect(() => {
setFullName(firstName + ' ' + lastName);
}, [firstName, lastName]);
Good:
const fullName = firstName + ' ' + lastName;
Reference: You Might Not Need an Effect
6. Cache Repeated Function Calls
Use a module-level Map to memoize pure functions called repeatedly with the same inputs.
Bad:
{projects.map(project => {
const slug = slugify(project.name) // called 100+ times for same names
return <ProjectCard key={project.id} slug={slug} />
})}
Good:
const slugifyCache = new Map<string, string>()
function cachedSlugify(text: string): string {
if (slugifyCache.has(text)) return slugifyCache.get(text)!
const result = slugify(text)
slugifyCache.set(text, result)
return result
}
Use a Map (not a hook) so it works everywhere: utilities, event handlers, not just components.
7. Use Explicit Conditional Rendering
Use explicit ternary instead of && when the condition can be 0, NaN, or other falsy values that render visibly.
Bad (renders "0" when count is 0):
<div>{count && <span className="badge">{count}</span>}</div>
Good:
<div>{count > 0 ? <span className="badge">{count}</span> : null}</div>
Checklist
- No
useMemowrapping simple primitive expressions - Default non-primitive props extracted to module-level constants
- Static JSX hoisted outside components
- RegExp created at module scope or memoized
- Derived state computed during render, not in useEffect
- Repeated pure function calls cached with module-level Map
- Conditional rendering uses explicit checks for falsy values