LitElement: A Deepdive Into Batched Updates

Pascal Schilp
ING Blog
Published in
9 min readDec 3, 2020

Whats always better than doing work? Not doing work!

Introduction

LitElement is a base class for developing web components. It’s very small, efficient at updating, and takes a lot of the heavy lifting of writing web components out of the developers hands, by being lazy.

As some of you may already know, ING is a long time user of web components. However, during workshops, or when onboarding new colleagues who have just started developing with web components, I often get the following questions:

  • How exactly does LitElement achieve its efficient updates?
  • What does asynchronous rendering mean?
  • How does LitElement pick up property changes?

So I figured, it’s blog time!

Lets start by taking a look at the following code example:

LitElement is reactive and observes your properties to kick off what is known as its “render pipeline”. What this means is that whenever we change a property in our component, it’ll update and re-render the component. This is nice, because it means we don’t have to do anything manually. In the code example above, just the act of setting a new value to this.myPropertyA will cause our component to re-render.

Now consider another example:

Hrm. Looks like we’re in a bit of a pickle here. When should we re-render? Should we re-render after myPropertyA has been set? That would mean we might do a bunch of unnecessary work and rendering, because we also want our component to update when we set myPropertyB, so we'll end up with two renders, where one would have been sufficient. That doesn't sound very efficient. This would get out of hand really quickly if we'd have a lot more properties to set in a method.

Fortunately, LitElement is smart and lazy, and uses a clever technique to batch these updates, and only re-render once. In order to understand how all this works, we’re going to take a look at some popular batching patterns, dive into the event loop, and the internals of LitElement. Finally, we’ll write a naive implementation of a batching web component base class.

Batching

Before we get started, lets make clear what we actually mean when we talk about batching work, and go over a couple techniques to demonstrate how we can achieve batching behavior in JavaScript.

Batching is basically limiting the amount of work we need to do, and is often implemented as a performance measure. A popular usecase for batching, or limiting work, is limiting API calls that may get fired in quick succession for example. We don’t want to put any unnecessary load on the API, and do a bunch of wasteful requests. Or in the case of LitElement; efficiently limiting re-renders of our components. Remember; the goal is to avoid any unnecessary work in order to be performant.

A popular pattern for batching is using a debounce function. As mentioned above, debouncers are often used for batching API calls. Imagine we have a search input, that every time a new search keyword is entered, fetches some search results from a server. It would put a pretty heavy load on the API if we would fire a request for every single input change or every new character, and that can be pretty wasteful. Instead, we can debounce the API calls, to only actually fire a request on the last input change.

Consider the following example:

If the user types the search keyword “javascript” in the input, instead of firing 10 API requests (one request for every character in the word “javascript”), we only fire one request; when the user has finished typing.

If you’re interested in reading more about debouncing, here’s a great blog on css-tricks.com

However, and I’m sorry to disappoint, this is not how LitElement does things but important to illustrate the point of “batching”. Before we go deeper into that, we’re going to require some knowledge about…

THE EVENT LOOP!

The event loop… can be a pretty elusive subject to understand. I’ll try to keep it brief here with a basic example, but if you’re interested in learning more about how the event loop works, here are some incredible resources that I cannot recommend enough:

Fortunately, where we’re headed, we don’t need to understand all the ins and outs of the event loop, we’re really only interested in microtasks for our case. Consider the following piece of code (… that often does the rounds on Twitter polls, and has confused many a developer before). What do you think is the order the console.log statements are called?

Here’s the answer, the order of the resulting console logs of this code is:

// 1
// 3
// 2

We’ll have Jake Archibald explain to us why exactly this happens:

The microtask queue is processed after callbacks as long as no other JavaScript is mid-execution, and at the end of each task. Any additional microtasks queued during microtasks are added to the end of the queue and also processed.

If we go back to our code snippet, we can see that the following happens:

  • console.log(1); is called, and logged to the console
  • We schedule a new microtask with Promise.resolve().then(() => {}), but at this point in time, JavaScript has not finished executing; it's not time to actually process microtasks just yet
  • JavaScript continues executing code, because we’re not done yet
  • console.log(3); is called, and logged to the console
  • JavaScript has finished executing! The browser can now process any microtasks we may have
  • console.log(2); is called, and logged to the console.

So why is this important? How does this allow us to use microtasks to batch work? Let’s take a look at a practical example:

Essentially, scheduleUpdate guards the update function against multiple incoming calls with its if statement. This cleverly uses our newfound knowledge of microtasks to our advantage to process any other incoming calls first. update only gets called once JavaScript has finished executing and microtask processing begins.

As a fun tidbit of knowledge, we can also write the scheduleUpdate method like so:

Since, from MDN:

If the value of the expression following the await operator is not a Promise, it’s converted to a resolved Promise.

In other words, once we set updateRequested, we don't immediately unset it, the await keyword ensures that we only schedule it to be unset.

In fact, Justin Fagnani, one of the authors of LitElement, once told me on twitter that the pre-LitElement version did exactly this:

Deferring promises

Alright, we’re almost ready to explain how LitElement achieves its efficient updates. But before we go there, there’s one final concept we need to explore, and that’s deferring resolving of Promises, which will provide us with a handy utility later on in this blog post.

Take a look at the following code:

This pattern is often used in cases (like unit tests) where you have to await a specific amount of time before continuing.

In this example, the sleep function returns a new Promise. Inside the promise, we set a timeout, but we only resolve the promise once the setTimeout callback is executed. So by awaiting the sleep function, we essentially force to wait until setTimeout callback has executed.

Be aware, however, that JavaScript is single-threaded, and blocking the main thread is generally considered a bad practice.

Back to LitElement

🧙‍♂️ Automagically requested updates

LitElement is reactive. What this means is that, like many other modern frontend libraries, we dont have to worry as much about manually rendering or re-rendering the contents of our component. All we have to do as a user of LitElement is declare our properties, LitElement will then observe these properties for us, and when a property has changed LitElement takes care of updating our component for us. Nice. I like not doing work.

Simple counter element, courtesy of the lovely folks over at webcomponents.dev

Imagine we have a simple counter element that has a count property. All thats needed for LitElement to rerender, is set a new value to the count property like so: this.count = 1;. This will automagically cause LitElement to request an update.

But… how? How does LitElement know that the count property was set? How does it observe our properties?

LitElement requires users to declare the properties of their component in a static properties getter. This static properties getter is very, very useful, because it takes a lot of boilerplate out of the developers hands, it takes care of attributes for us, handles attribute reflection, allows us to react to changes, but more importantly: It creates getters and setters for us. This is nice, because this means that our components dont get littered with getters and setters all over the place, but it also requests updates for us!

Consider the following example:

Under the hood, LitElement essentialy turns this into the following getters/setters:

Note: this is pseudocode for illustration purposes, the actual source code of LitElement looks a little bit different, but the general concept still applies. If you’re interested in reading the source code, you can find it here.

What this means is that whenever we assign a new value to a property like so: this.count = 1;, the setter for count is called, which then calls requestUpdate(). So by simply setting a property, an update is requested for us!

Objects and arrays

We’ll take a small aside here to explain another common occurence when using LitElement, that I often see people get confused about. We now know that we can trigger updates simply by setting a new value to a property, but… what about objects and arrays?

Imagine we have a component with a user property that looks something like this:

{
name: "Mark",
age: 30
}

People often expect that setting, for example, the name property on the user object should trigger a rerender, like so: this.user.name = "Nick";, and are surprised to find that it doesn't. The reason for this is that by setting the name property of the user object, we don't actually change the user object itself, we changed a property of the user object, and as such, the this.requestUpdate() method is never called!

An easy fix for this is to just call this.requestUpdate() manually, or alternatively replace the entire object to make sure the setter does get called:

this.user = {
...this.user,
name: "Mark"
}

The same is true for arrays. Imagine we have a users property on our element, that contains a list of users similar to the object we used in the example above. The following code will not schedule an update:

this.users.push({name: 'Nick', age: 30});

Because we only mutate the already existing array, and as such don’t trigger the setter for the users property itself.

Back to batching updates

Alright, we now know what batching means, how microtasks work, how we can defer resolving promises, and how setting properties in LitElement schedules an update. We have all the required knowledge to figure out how LitElement actually uses this to batch its updates.

To do so, we’re going to write a very naive implementation of this behavior. We’re going to start with a very simple JavaScript class that efficiently calls an update method when our properties change. Follow along:

You can see a live demo of this in your browser here

Taking things a step further

Neat! We now have a simple implementation of batching updates. We can now take things even further and provide some kind of API so users can await updates, like LitElement's updateComplete property. Take a look at the following code:

You can see a live demo of this in your browser here

Back to web components land

Sweet, now that we have a solid understanding of using microtasks to our advantage to batch work, let’s go back to web components land, and see how we could implement this as a simple base class for web components.

Again, this code is a simplified example of the technique LitElement uses for illustration purposes, if you’re interested in seeing how LitElement achieves this exactly, you can find the source code here.

And, well, that’s it. Not much has changed here really, other than the fact that we now extend from HTMLElement, and have to call super() in the constructor. Let's see how we can actually use it in a component:

Neat! We’ve now implemented our BatchingElement base class in a real life web component. Whenever we change multiple properties in a row, we'll only call our update method once. Consider the following example:

You can see a live demo of this in your browser here.

If you’re interested in seeing another real world example that implements this pattern, you can take a look at @generic-components, where I use this pattern to batch calls to the slotchange event which is another useful application of batching. The implementation is slightly different, but all the same concepts apply.

And thats how LitElement efficiently batches updates and avoids doing unnecessary work. Combine that with lit-html to do efficient DOM updates, and you have a really efficient and powerful combination of libraries at your disposal.

Thanks for reading!

--

--