Alex Vipond
SHIFT + D

The hardest part of web dev

Published about 2 years ago

Proper effect timing is the hardest part of web dev.

Tweet from Devon Govett saying it took 5 hours to solve a React effect timing bug with useLayoutEffect and useEffect

Effect timing problems can take hours, if not days, to debug, and the solution is often just a line or two of code. Devon Govett's React problem, for example, was fixed just by changing useLayoutEffect to useEffect.

I once debugged for several hours with six other devs on a late Friday evening just to find that a 10ms setTimeout was throwing off effect timing and code execution, and only when certain browser extensions were installed.

Proper effect timing is hard!

There are four steps you can follow to get better at fixing these problems:

  1. Understand the browser event loop
  2. Learn React and Vue timing tools that let you run your code at exactly the right time
  3. Learn plain JavaScript timing tools that let you run your code at exactly the right time

Understand the browser event loop

Understanding the browser event loop can be a huge help when dealing with these problems. My two favorite primers on how the event loop works are:

  1. "In the Loop", a talk by Jake Archibald
  2. "Deeper JavaScript: What the Duce is a Microtask", a podcast by Caleb Porzio. Probably worth listening to part 1 and part 2 of the Deeper JavaScript series too.

Learn React and Vue timing tools

When you're working with React and Vue, it's also super handy to know how these tools interact with the browser event loop.

It's critical to understand that updating the DOM, recalculating page layout, and repainting the page are three separate actions in the loop. Both frameworks allow you to run code before and after each of these actions.

In React:

  • Your render function runs before React updates the DOM
  • useInsertionEffect callbacks run after React updates the DOM, but before the browser recalculates page layout.
  • useLayoutEffect callbacks run after tbe browser recalculates page layout (taking into account any CSS injected by a useInsertionEffect callback), but before the browser repaints.
  • useEffect callbacks run after the browser has finished repainting

In Vue:

  • Your setup function runs once, during component initialization, before any DOM updates
  • watchEffect callbacks run before Vue updates the DOM. You can also use an onBeforeUpdate callback to run code before every update, even if reactive data referenced inside the callback hasn't changed.
  • watchPostEffect callbacks run after the browser has finished repainting. You can also get this same effect timing using watch and setting the flush option to post.

Vue doesn't currently have an equivalent of React's useInsertionEffect, but it does have a bunch more options for running code at other points in the component and reactivity lifecycle. See the Reactivity: Core docs and Lifecycle Hooks docs for more info.

Vue also has nextTick, which is great when you need to run some code after watchPostEffect.

Learn plain JavaScript timing tools

When I'm working with React or Vue, I tend to avoid plain JavaScript timing tools as often as possible, and instead use the built-in framework tools, which are nicely wired up to the reactivity system.

But when I'm outside of those frameworks, I fall back to these tools:

These tools time your code with subtle differences across use cases and across browsers, so it's tough to say exactly which one can run code at any specific time. Complicating the issue, you can also nest requestAnimationFrame to create different timing effects:

requestAnimationFrame(
  () => requestAnimationFrame(
    () => doSomethingTwoFramesLater()
  )
)

My best advice is to keep an eye out for all of these tools in your code and in third party code. If you see a problem that you suspect might be an effect timing problem, try different tools, randomly if necessary, to find a combination that works 🧙‍♂️

ON THIS PAGE

The hardest part of web devUnderstand the browser event loopLearn React and Vue timing toolsLearn plain JavaScript timing tools