Back to JavaScript
JavaScript
medium
mid

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.

6 min read·~5 min to think through

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

jsx
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:

jsx
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:

jsx
function handleClick() { doStuff(); }
function Parent() {
  return <Child onClick={handleClick} />;     // stable reference
}

2. useCallback when you do need closure:

jsx
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:

jsx
function Child() {
  return <button onClick={() => doStuff()}>X</button>;
}

When the difference actually matters

useCallback / hoisting only matters if:

  • The child is wrapped in React.memo or 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 this correct 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 target settings, they compile to native arrows with no overhead.

Where you might see a real difference

  • this binding: arrows capture lexical this; 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.

Senior engineer discussion

Seniors don't take the bait when interviewers frame this as a syntax question. They redirect to reference identity, useCallback, and memo — the actual perf surface — and note that React 19's compiler is about to make most of this irrelevant. The deeper signal is whether the candidate profiles before optimizing and whether they understand React's render model.

Related questions