tldr; go to the DEMO
Framework or vanilla JS, the browser plays by its own rules. Understanding them is the difference between a janky mess and a smooth experience.
Many articles cover the theory of the rendering pipeline. This one shows it. Let’s quickly cover some facts and then get our hands dirty.
A Frame in 16.67 Milliseconds
JavaScript runs on a single thread and must yield control to the browser to get anything drawn.
Here is what happens during one frame:
- Scripting - The JavaScript engine runs your code.
- Style Calculation - The browser figures out which CSS rules apply and computes the final styles, resolving cascades, inheritance, and computed values.
- Reflow (also called layout) – Calculates geometry such as width, height, and position. A reflow can ripple through parent and child elements, making it costly.
- Repaint (also called paint) – Draws pixels for backgrounds, borders, text, and shadows. Complex visuals like gradients or shadows slow this step.
- Composite – Takes painted layers and draws them to the screen. It is much cheaper than reflow or repaint.
Frame budget math: There are 1000 milliseconds in one second. Dividing by 60 frames per second gives 1000/60 = 16.6666666667 milliseconds per frame, which we round to 16.67 milliseconds. If your work takes longer than that, you will drop frames.
What matters in practice
Modern browsers handle simple pages easily, but once you add animations or many elements, performance limits appear. Knowing which properties trigger which steps can help you keep things fast.
- Scripting time is the time your JavaScript holds the main thread. Heavy work delays reflow and repaint.
- Changing properties like top, left, width, height, or margin triggers reflow.
- Changing paint only properties background-color, box-shadow, border-radius triggers repaint but not reflow.
- Changing transform or opacity is usually handled by the GPU, skipping reflow and repaint.
The Use Case (tested on Chrome)
To demonstrate this, we will build a small demo with squares that can move and shuffle.
First, the unoptimized version. It uses top and left, which cause reflows on every frame. A box-shadow transition adds paint work.
Then, the optimized version. It uses transform and opacity, which stay on the GPU for smoother motion.
You can switch between Unoptimized and Optimized below and press shuffle a few times. The difference is clear, especially as you add more squares.
Unoptimized code Optimized Code
Unoptimized Implementation
function BadSquaresComponent() { const [positions, setPositions] = useState(initialPositions); const [isAnimating, setIsAnimating] = useState(false); const shuffleSquares = () => { setIsAnimating(true); // Placeholder that generates random positions const newPositions = computeNewPositions(); setPositions(newPositions); setTimeout(() => setIsAnimating(false), 500); }; return ( <div> <div onClick={shuffleSquares}> shuffle squares </div> {positions.map((pos, index) => ( <div key={index} style={{ position: 'absolute', top: `${pos.top}px`, // Triggers reflow left: `${pos.left}px`, // Triggers reflow boxShadow: isAnimating ? '0 14px 28px rgba(239,68,68,0.45)' : 'none' // Triggers Repaint }} > {index + 1} </div> ))} </div> ); }Optimized Implementation
function GoodSquaresComponent() { const [positions, setPositions] = useState(initialPositions); const [isAnimating, setIsAnimating] = useState(false); const shuffleSquares = () => { setIsAnimating(true); // Placeholder that generates random positions const newPositions = computeNewPositions(); setPositions(newPositions); setTimeout(() => setIsAnimating(false), 500); }; return ( <div> <div onClick={shuffleSquares}> shuffle squares </div> {positions.map((pos, index) => ( <div key={index} style={{ position: 'absolute', // GPU accelerated transform: `translate(${pos.left}px, ${pos.top}px)`, opacity: isAnimating ? 0.7 : 1 // GPU accelerated }} > {index + 1} </div> ))} </div> ); }Monitoring performance
You can watch the animation and also use the FPS meter in DevTools (Cmd+Shift+P → “FPS meter”). When running the demo, the meter shows the live frame rate. Below are screenshots of both versions:


The unoptimized code barely reached 21 FPS when shuffling fast. The optimized version stayed at 60 FPS without breaking a sweat.
Conclusion
Smooth performance is not magic. It comes from understanding what the browser is doing and working with it, not against it. Animating layout or paint properties makes the browser recalculate and repaint every frame. Using properties like transform and opacity sends that work to the GPU, freeing the main thread and keeping motion fluid. Knowing these details can be critical in animation heavy applications.
.png)
