When do function declarations versus arrow functions actually impact performance?
The function form itself (declaration vs arrow) is essentially zero perf impact. What matters is identity stability across renders in React: a new arrow function created inside a component on every render creates a new reference, which busts React.memo on child components and adds churn for useEffect deps. The fix is useCallback, hoisting, or stable references — not changing function syntax. For non-React code, the difference is negligible.
Short answer: declarations vs arrows have no measurable runtime perf difference in modern V8/SpiderMonkey. JIT compiles both equally well. The "performance" question is really about reference identity in React components.
What people actually mean
function Parent() {
return <Child onClick={() => doStuff()} />; // new arrow every render
}() => doStuff() is a new function reference every time Parent re-renders. If Child is wrapped in React.memo, the shallow compare sees a "new prop" and re-renders Child unnecessarily.
This isn't about arrow-vs-declaration; it's about creating a new closure on every render. The same problem happens with a function declaration inside the component:
function Parent() {
function handleClick() { doStuff(); } // also new on each render
return <Child onClick={handleClick} />;
}Fixes (in order of preference)
1. Hoist if you don't need closure over render values:
function handleClick() { doStuff(); }
function Parent() {
return <Child onClick={handleClick} />; // stable reference
}2. useCallback when you do need closure:
function Parent({ id }) {
const handleClick = useCallback(() => doStuffWith(id), [id]);
return <Child onClick={handleClick} />;
}3. Move the handler into the child so the parent doesn't pass anything:
function Child() {
return <button onClick={() => doStuff()}>X</button>;
}When the difference actually matters
useCallback / hoisting only matters if:
- The child is wrapped in
React.memoor is a pure component. - The child is expensive to render.
- The function is in a dep array (
useEffect,useMemo) and causes effects to over-fire.
If the child re-renders cheaply anyway, useCallback is overhead with no payoff.
The misconception
"Arrow functions are slow" or "declarations are faster" — both false in modern engines. The microbenchmarks that suggested otherwise were from old JIT versions and were noise even then.
What's true:
- Class methods bound via arrow class properties keep
thiscorrect without.bind. That's an ergonomic win, not a perf one. - Arrow functions are slightly smaller in source (potentially affects gzipped bundle by a few bytes).
- Some old transpilers polyfill arrows; with modern
targetsettings, they compile to native arrows with no overhead.
Where you might see a real difference
thisbinding: arrows capture lexicalthis; declarations don't. In old class-based React, methods had to be bound (this.x = this.x.bind(this)) — arrow class properties avoided that.- Hot loops: creating millions of small closures in a tight loop has GC cost. Hoist or reuse the function. This is real but rare in app code.
- Hidden classes / inline caches: passing the same function reference allows V8's IC to optimize the call site. Repeatedly passing fresh references prevents that optimization. Usually invisible in app perf.
Mental model
The "use declarations for perf" advice is folklore from a decade ago. Today, pick the form that reads best. In React, focus on reference stability (hoist or useCallback) when the receiving component is memoized. Don't refactor for the syntax itself.
React 19 compiler
The upcoming React Compiler automatically memoizes values and callbacks, removing most useCallback boilerplate. With it, the inline-arrow concern largely goes away. Until then, the manual rule still applies for memoized children.
Follow-up questions
- •When does useCallback actually help?
- •How does React 19's compiler change the need for useCallback?
- •What's the difference between useCallback and useMemo?
- •When do you reach for hoisting vs useCallback?
Common mistakes
- •Wrapping every callback in useCallback by default — overhead without payoff.
- •Believing arrows are slower than declarations — they're not.
- •Using useCallback on a function passed to a non-memoized child — wasted work.
- •Forgetting that inline JSX object/array literals create new references too — same problem, different shape.
- •Refactoring for syntax instead of profiling — measure first.
- •Bound class methods in modern React — class components themselves are largely legacy.
Performance considerations
- •In raw CPU terms, declarations vs arrows is indistinguishable. In React, reference identity matters for memoized subtrees and effect dep arrays, but the syntax is the wrong lever — useCallback or hoisting is. With React 19's compiler, most of these manual optimizations become unnecessary.
Edge cases
- •Inline arrows are unavoidable in some patterns (event handlers with closure over loop variables) — accept the re-render or restructure.
- •useEvent / experimental hooks aim to fix the 'stable callback' problem more elegantly.
- •Generator functions and async functions can't be arrows (function* / async function*) — syntax forces declaration.
- •Arrow functions can't be used as constructors (no new), don't have arguments, don't have prototype.
- •Stacktrace readability sometimes favors named declarations over anonymous arrows in DevTools.
Real-world examples
- •React docs: useCallback is for stable references passed to memoized children, not for general perf.
- •Most production React code mixes arrows (for terse handlers) and declarations (for named helpers) — readability driven, not perf driven.
- •React Compiler RFC and upcoming React 19 effectively remove the useCallback boilerplate.