Which React pattern is faster

Reilly,react

Saw a tweet today about which pattern was most performant:

genuine question for react heads, which of these is most performant? Consider that we may have thousands of them all receiving new x y props on each frame. pic.twitter.com/t2tc7Z9qBG

— Steve Ruiz (@steveruizok)

March 15, 2026

My intuition thought that the minimal effects / work would win. So in this case I presumed A would win bc of it’s doing the least amount of work - just object creation (fast in js)

But tbh I didn’t really know so I ran an experiment!

Table of Contents

The Experiment

This is a lot of code but just want to be transparent!

tldr on this - we’re preloading a shit ton of state ahead of time and then using raf (requestAnimationFrame) to update state every 16ms

import { useState, useRef, useLayoutEffect, useEffect, memo } from 'react' import './App.css' // ============================================ // Pattern A: Inline styles // ============================================ const PatternA = memo(function PatternA({ x, y }: { x: number; y: number }) { return <div className="dot" style={{ transform: `translate(${x}px, ${y}px)` }} /> }) // ============================================ // Pattern B: useRef + useLayoutEffect // ============================================ const PatternB = memo(function PatternB({ x, y }: { x: number; y: number }) { const elementRef = useRef<HTMLDivElement>(null) useLayoutEffect(() => { const elm = elementRef.current if (!elm) return elm.style.transform = `translate(${x}px, ${y}px)` }, [x, y]) return <div className="dot" ref={elementRef} /> }) // ============================================ // Pattern C: Ref callback // ============================================ const PatternC = memo(function PatternC({ x, y }: { x: number; y: number }) { return ( <div className="dot" ref={(elm) => { if (elm) elm.style.transform = `translate(${x}px, ${y}px)` }} /> ) }) // ============================================ // Benchmark component // ============================================ type PatternType = 'A' | 'B' | 'C' interface Position { x: number y: number } function App() { const [pattern, setPattern] = useState<PatternType>('A') const [componentCount, setComponentCount] = useState(1000) const [updateInterval, setUpdateInterval] = useState(16) const [isRunning, setIsRunning] = useState(false) const [positions, setPositions] = useState<Position[]>(() => { const initial: Position[] = [] for (let i = 0; i < 1000; i++) { initial.push({ x: Math.random() * 600, y: Math.random() * 400 }) } return initial }) // Reinitialize positions when count changes useEffect(() => { const newPositions: Position[] = [] for (let i = 0; i < componentCount; i++) { newPositions.push({ x: Math.random() * 600, y: Math.random() * 400, }) } setPositions(newPositions) }, [componentCount]) // Animation loop useEffect(() => { if (!isRunning) return let animationId: number let lastUpdate = performance.now() const animate = () => { const now = performance.now() if (now - lastUpdate >= updateInterval) { lastUpdate = now setPositions(prev => prev.map(pos => ({ x: pos.x + (Math.random() - 0.5) * 10, y: pos.y + (Math.random() - 0.5) * 10, })) ) } animationId = requestAnimationFrame(animate) } animationId = requestAnimationFrame(animate) return () => cancelAnimationFrame(animationId) }, [isRunning, updateInterval]) const PatternComponent = pattern === 'A' ? PatternA : pattern === 'B' ? PatternB : PatternC return ( <div className="app"> <header className="header"> <h1>React Transform Pattern Benchmark</h1> <p>{componentCount} components updating every {updateInterval}ms ({Math.round(1000 / updateInterval)} target FPS)</p> </header> <div className="controls"> <div className="control-group"> <label>Pattern:</label> <div className="button-group"> <button className={pattern === 'A' ? 'active' : ''} onClick={() => setPattern('A')} > A: Inline Style </button> <button className={pattern === 'B' ? 'active' : ''} onClick={() => setPattern('B')} > B: useLayoutEffect </button> <button className={pattern === 'C' ? 'active' : ''} onClick={() => setPattern('C')} > C: Ref Callback </button> </div> </div> <div className="control-group"> <label>Components: {componentCount}</label> <input type="range" min="100" max="5000" step="100" value={componentCount} onChange={(e) => setComponentCount(Number(e.target.value))} disabled={isRunning} /> </div> <div className="control-group"> <label>Update interval: {updateInterval}ms ({Math.round(1000 / updateInterval)} target FPS)</label> <input type="range" min="4" max="100" step="1" value={updateInterval} onChange={(e) => setUpdateInterval(Number(e.target.value))} disabled={isRunning} /> </div> <div className="control-group"> <button className="primary" onClick={() => setIsRunning(true)} disabled={isRunning}> Start </button> <button onClick={() => setIsRunning(false)} disabled={!isRunning}> Stop </button> </div> </div> <div className="benchmark-container"> {positions.map((pos, i) => ( <PatternComponent key={i} x={pos.x} y={pos.y} /> ))} </div> <div className="pattern-info"> <h2>Pattern Descriptions</h2> <div className="pattern-cards"> <div className={`pattern-card ${pattern === 'A' ? 'active' : ''}`}> <h3>A: Inline Style</h3> <pre>{`<div style={{ transform: \`translate(\${x}px, \${y}px)\` }} />`}</pre> <p>React handles the style object, creating new objects each render.</p> </div> <div className={`pattern-card ${pattern === 'B' ? 'active' : ''}`}> <h3>B: useLayoutEffect + Ref</h3> <pre>{`useLayoutEffect(() => { elm.style.transform = \`...\` }, [x, y])`}</pre> <p>Direct DOM manipulation after React commit phase.</p> </div> <div className={`pattern-card ${pattern === 'C' ? 'active' : ''}`}> <h3>C: Ref Callback</h3> <pre>{`ref={(elm) => { if (elm) elm.style.transform = \`...\` }}`}</pre> <p>Sets transform during ref attachment. Creates new function each render.</p> </div> </div> </div> </div> ) } export default App

Visually it looks like:

4x cpu slowdown using Chrome Performance tab


Results Summary

Here’s a summary of the results from Claude:

Key Findings

Winner: inline

MetricinlineuseLayoutEffectrefCallbackWinner
TBT % of Duration41.8%44.0%45.8%inline
GC % of Duration15.6%17.1%18.7%inline
Scripting % of Duration87.9%86.6%85.3%refCallback
Frame Drop Rate76.7%76.2%75.5%refCallback
Long Tasks >200ms/sec0.941.761.75inline

Conclusion: inline wins on the metrics that matter most for interactivity:

The frame drop rates and scripting percentages are essentially a wash (~1-2% difference) - all three approaches are CPU-bound with similar frame delivery struggles


Detailed Analysis

Analysis Date: 2026-03-15 | Test Conditions: 4x CPU Throttling

Since trace durations varied (5,338ms to 5,699ms), metrics are normalized for fair comparison.

At a Glance

MetricinlineuseLayoutEffectrefCallbackWinner
TBT % of Duration41.8%44.0%45.8%inline
GC % of Duration15.6%17.1%18.7%inline
Scripting % of Duration87.9%86.6%85.3%refCallback
Frame Drop Rate76.7%76.2%75.5%refCallback
Long Tasks >200ms/sec0.941.761.75inline

Winner: inline - Wins on blocking time (TBT), GC pressure, and severe long task frequency. refCallback technically has the lowest frame drop rate, but the difference is marginal (~1-2%).


The Data

Normalized Metrics (Per Second / Percentage)

MetricinlineuseLayoutEffectrefCallback
Trace Duration5,338ms5,670ms5,699ms
Dropped Frames/sec92.391.992.1
Frame Drop Rate76.7%76.2%75.5%
TBT/sec (ms)418440458
TBT % of Duration41.8%44.0%45.8%
Scripting % of Duration87.9%86.6%85.3%
GC % of Duration15.6%17.1%18.7%
Long Tasks (>50ms)/sec6.185.825.44
Long Tasks (>200ms)/sec0.941.761.75

Raw Metrics (Absolute Values)

MetricinlineuseLayoutEffectrefCallback
Trace Duration5,338ms5,670ms5,699ms
Total Frames643684695
Dropped Frames493521525
Total Blocking Time2,233ms2,495ms2,607ms
Scripting Time4,691ms4,907ms4,861ms
GC Time832ms971ms1,066ms
Tasks >50ms333331
Tasks >100ms161615
Tasks >200ms51010

Metric Deep Dives

Total Blocking Time (TBT)

TBT measures the sum of time beyond 50ms for all long tasks - a key interactivity metric.

ApproachTBTTBT % of Durationvs inline
inline2,233ms41.8%baseline
useLayoutEffect2,495ms44.0%+5.3%
refCallback2,607ms45.8%+9.6%

Interpretation: inline spends proportionally less time blocking the main thread, leading to better potential interactivity.

Garbage Collection

ApproachGC TimeGC % of Durationvs inline
inline832ms15.6%baseline
useLayoutEffect971ms17.1%+9.6%
refCallback1,066ms18.7%+19.9%

Interpretation: inline creates less memory pressure. refCallback triggers ~20% more GC relative to its trace duration.

Severe Long Tasks (>200ms)

ApproachCountPer Secondvs inline
inline50.94/secbaseline
useLayoutEffect101.76/sec+87%
refCallback101.75/sec+86%

Interpretation: inline has half the rate of severe blocking tasks, meaning smoother execution with fewer major jank events.

Frame Performance

ApproachDrop RateDropped/sec
inline76.7%92.3
useLayoutEffect76.2%91.9
refCallback75.5%92.1

Interpretation: Frame drop rates are essentially equivalent (~1% difference). All approaches struggle equally with frame delivery due to heavy CPU work.


Long Task Distribution (Top 5)

View task duration breakdown by approach

inline: 266ms, 229ms, 222ms, 204ms, 202ms

useLayoutEffect: 256ms, 246ms, 223ms, 218ms, 216ms

refCallback: 255ms, 240ms, 240ms, 229ms, 228ms

Analysis: inline has one outlier (266ms) but its other long tasks are shorter. useLayoutEffect and refCallback have more consistently severe long tasks.


Recommendations

Use inline when:

Consider useLayoutEffect when:

Consider refCallback when:


Technical Notes

Trace File Information

FileCompressedUncompressedEventsDuration
inline.json.gz14.5 MB230 MB1,200,2965,338ms
useLayoutEffect.json.gz15.7 MB257 MB1,351,5185,670ms
refCallback.json.gz16.2 MB268 MB1,422,2755,699ms
2026 © Reilly O'Donnell.RSS