When DOM Updates Appear to Be Asynchronous
Tags: javascript, animation, performance
Published: 2020-10-06 22:18:22

---
title: When DOM Updates Appear to Be Asynchronous
published: true
date: 
tags: javascript, animation, performance
canonical_url: https://macarthur.me/posts/when-dom-updates-appear-to-be-asynchronous
---

Imagine we have some JavaScript on a page that updates an element’s contents and immediately logs those contents out:

```
<span id="element">
  original text
</span>

<script>
  document.getElementById('element').innerHTML = "updated text";
  console.log(element.innerHTML);
</script>
```

After loading the page, you’d correctly expect that “updated text” will be displayed on the screen. And when it’s exposed via that `console.log()`, you’ll see the same value there too.

The fact that `console.log()` yields this result isn’t surprising because a DOM update is a _synchronous_ event. When the properties of the DOM object are modified, that change is thrown onto the call stack, and no proceeding event can execute until the stack is empty again. This is how JavaScript’s event loop does it’s thing — first in, first out — even though many of those events simply kick off asynchronous work handled by browser APIs (like `setTimeout()`, for example).

## [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#same-thread-different-pace)Same Thread; Different Pace

Despite it being synchronous, before a DOM change can be made visible _on the screen_, [several processes](https://developers.google.com/web/fundamentals/performance/critical-rendering-path/render-tree-construction#tldr) occur: the render tree is updated, the layout of the page is reflowed, and the result is finally painted. These processes all operate on the same thread as JavaScript’s event loop, _but at a different pace._ Consider this shoddy illustration I put together:

![event loop and repaint cycle illustration](https://macarthur.me/4e4649a2760bd40a129ed9937ca22831/thread.svg)<figcaption>the event loop vs. the repaint cycle</figcaption>

The **horizontal bar** represents the browser’s main thread — everything operates here, unless you’re doing something like leveraging Web Workers (and [you probably should be](https://macarthur.me/posts/use-web-workers-for-your-event-listeners)).

The **smaller vertical bars** represent the execution of JavaScript’s event loop. It’s fast, firing as quickly as it can execute items on the call stack, often communicating with browser APIs along the way.

The **thicker vertical bars** represent how frequently the browser repaints the screen. That rate usually [sits at around 60 frames/second](https://developers.google.com/web/fundamentals/performance/rendering), or about once every ~16. 66ms. Every time it occurs, it’ll take all the DOM changes made between the current paint and the previous paint and make them visible to the user.

Most of the time, the relationship between the event loop and the repaint cycle is inconsequential to our work and virtually irrelevant to the user’s experience. But sometimes, circumstances arise when this whole DOM-to-screen process can make us wonder if modifying the DOM really is a synchronous job at all.

## [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#make-a-dom-update-look-asynchronous-w-alert)Make a DOM Update Look Asynchronous w/ `alert()`

Let’s explore one of these circumstances. To do so, I’m using a simple local Node server with a bit of HTML. You’re welcome to follow along with the changes we’ll be making by [cloning the repo for yourself](https://github.com/alexmacarthur/dom-updates-and-browser-repaints) and uncommenting each example as needed. (I considered using something like [CodeSandbox](https://codesandbox.io/) for these examples, but I needed an environment as pure & predictable as possible. Platforms that serve a site through an embedded iframe couldn’t quite provide that.)

This time around, instead of logging out the modified DOM value, let’s expose it with an `alert()`. When that action is thrown onto the call stack, it’ll freeze the main thread, including any outstanding repaints.

```
const element = document.getElementById('element');
element.innerHTML = "updated text";
alert(element.innerHTML);
```

After refreshing the page this time, you’ll see the “updated text” correctly revealed in the alert box, indicating that the DOM update itself _really was_ synchronous. But if you look at what’s actually rendered to the screen, it’s probably not what you’d expect:

![DOM and browser out of sync](https://macarthur.me/static/f10e488068ea785d94a1bf785fd91c83/5a190/out-of-sync.png "DOM and browser out of sync")

The modification to the DOM object is complete, **but the respective _repaint_ has not yet been able to occur** (remember — the event loop is turning over much faster than the browser’s refresh rate). The instant that the `alert()` is dismissed, repaints are allowed to proceed, and our text is updated.

## [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#making-things-unfold-more-predictably)Making Things Unfold More Predictably

Again, this quirk is unlikely to trip-up any real-world work, but since we’re here anyway, let’s toy with making things behave a little more predictably. Say we wanted the `alert()` to fire _only after the DOM change has been rendered to the screen._ To do so, we have a couple of options, one of which is hackier than the other.

### [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#option-1-delay-the-alert)Option #1: Delay the `alert()`.

By wrapping our `alert()` in a `setTimeout()`, we can wait long enough for any outstanding repaints to occur, and successfully fire it after the updated text has been rendered.

```
const element = document.getElementById('element');
element.innerHTML = "updated text";

setTimeout(() => {
	alert(element.innerHTML);
}, 20); // <-- arbitrary number greater than refresh rate
```

This “works,” but it isn’t the most elegant solution, mainly because we’re making an educated guess as to when the repaint cycle will have turned over. We don’t want to wait too long, causing an unnecessary delay for the `alert()`. But if we cut it close and the timeout fires sooner than that delay, we’re not guaranteed to see the correct text on the screen when it’s frozen, because the browser didn’t have a chance to paint outstanding DOM changes.

We can illustrate this by setting the timeout’s delay to something less than the refresh rate and reload the page a few times. Much of the time, the text will still correctly render. But sooner or later, the rendered text will _not_ have updated like we want.

```
const element = document.getElementById('element');
element.innerHTML = "updated text";

setTimeout(() => {
	alert(element.innerHTML);
}, 5); // <-- risky, since the delay is less than the browser's refresh rate
```

Since we can’t undoubtedly predict the refresh rate of the browser (especially considering the range of things that could impact the main thread’s throughput overall), this option is pretty far under the bar of “ideal.” Thankfully, the browser offers a built-in API for navigating the repaint cycle by design, providing us with a much friendlier option.

### [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#option-2-fire-after-next-repaint)Option #2: Fire After Next Repaint

The browser’s `requestAnimationFrame` API lets us [fire a callback before the browser’s next repaint](https://developer.mozilla.org/en-US/docs/Web/API/window/requestAnimationFrame), allowing us to avoid any guessing games in our objective. To use it for our purposes, it’s tempting to fire our `alert()` in one of its callbacks and call it good:

```
const element = document.getElementById("element");
element.innerHTML = "updated text";

requestAnimationFrame(() => {
	alert(element.innerHTML);
});
```

**But that won’t work.** Firing _before_ the next repaint is pointless. Instead, we want it to happen _after_, since this is when the DOM update will have been painted to the screen. To accomplish this, we just need to nest things a bit:

```
const element = document.getElementById('element');
element.innerHTML = "updated text";

requestAnimationFrame(() => {
	// fires before next repaint

	requestAnimationFrame(() => {
		// fires before the _next_ next repaint
		// ...which is effectively _after_ the next repaint

		alert(element.innerHTML);
	});
});
```

With that, we’re good to go! The `alert()` will only after outstanding DOM changes have been painted to the screen. As a result, what we see in the `alert()` and what’s rendered on the page will consistently be the same:

![DOM and browser in sync](https://macarthur.me/static/2abf9fcc0ceb9efed452cfd2586b4615/5a190/in-sync.png "DOM and browser in sync")

## [<svg aria-hidden="true" focusable="false" height="16" version="1.1" viewbox="0 0 16 16" width="16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg>](#the-broader-implications)The Broader Implications

Experimenting like this is fun in & of itself, but it should also impact the way you think more broadly about the relationship between the DOM and the browser’s rendering engine, especially as it all impacts the performance of your code.

There’s more going than what you might think when a DOM attribute is modified, and when the black box of a browser starts to become a little more translucent, it can set you up to understand & tackle tangential problems that impact user experience more apparently, like how certain changes to the DOM trigger synchronous style & layout updates (Paul Irish has [a good starter resource](https://gist.github.com/paulirish/5d52fb081b3570c81e3a) on this).

But even if you don’t go down a road like that, hopefully this gets you thinking about the the inner-workings of the browser more than before, if for no other reason than to appreciate the tool a little more for all that it does.

_ **Shoutout!** All of this was inspired by [a tweet](https://twitter.com/rauschma/status/1288868746682081285?s=19) from [Dr. Axel Rauschmayer](https://dr-axel.de/), in which he asked for a safe way to fire an `alert()` only after a DOM change is visible on the screen. That question and the following discussion got my gears turning to fully understand why the best solution works the way it does._

 (This is an article published at macarthur.me. [Read it online here](https://macarthur.me/posts/when-dom-updates-appear-to-be-asynchronous).)