As you might know I am a big Web Component fan and I listen/read a lot about it. This blog post idea came out when I watched the talk “The Virtue of Laziness: Leveraging Incrementality for Faster Web UI” by the great Justin Fagnani. If you do not have time to watch it, this blog post is kind of a summary with references and demos.

Prerequisite knowledge

Here is the knowledge that you should acquire to fully understand the blog post. Read carrefuly and open the source links to even go further if needed.

Device refresh rates

Most devices today refresh their screens 60 times a second, which means each frame has a budget of 16.6ms. However it is recommended to complete any work under 10ms. If you cannot make it work under 10ms, the browser will drop one or more frames and as a consequence the users will experience jank as illustrated below:

Browser frame rate explaination

source

Event loop

The event loop model in JavaScript, unlike a lot of other languages, never blocks I/O operations (few exceptions exist).

To understand how the event loop works, read the following piece of code.

console.log('script start');

setTimeout(function() {
  console.log('setTimeout');
}, 0);

Promise.resolve().then(function() {
  console.log('promise1');
}).then(function() {
  console.log('promise2');
});

console.log('script end');

In what order do you think those logs will appear in the console ?

Solution:

script start
script end
promise1
promise2
setTimeout

To fully understand what happened you need to understand what are microtasks and tasks and in which order the browser uses them.

  • JS stack: JS thread, proceed with one task at the time
  • Microtask are usually scheduled for things that should happen straight after the currently executing script, such as reacting to a batch of actions, or to make something async without taking the penalty of a whole new task.
  • Tasks are scheduled so the browser can get from its internals into JavaScript/DOM land and ensures these actions happen sequentially.

You can imagine those to be three piles of actions with different priorities. The priorities being the following:

JS stack > Microtask queue > Task queue

Must see: Jake Archibald: In The Loop - JSConf.Asia 2018

More details on Jake Archibald’s blog (Developer advocate on Chrome)

more on the JS event loop

LitElement

LitElement is a simple base class for creating fast, lightweight web components.

Batch work

LitElement rendering is always async and this feature is leveraged to update the DOM by batch.

To illustrate that, let’s create the Web Component MyElement that will observe changes on firstName and lastName properties using @property decorator:

@customElement('my-name')
export class MyElement extends LitElement {
  @property() firstName = '';
  @property() lastName = '';

  render() { /* renders the component */ }
}

Will the render method be called everytime I set a new value to any of its properties ?

The answer is NO, litElement will batch updates so that both examples below will call render only once:

const el = new MyElement();
el.firstName = 'Julien'; // Create a microtask
const el = new MyElement();
el.firstName = 'Julien'; // Create a microtask
el.lastName = 'Renaux'; // reuse the same microtask

Demo

To get notify when the update is complete you can use the getter updateComplete that returns a Promise.

const el = new MyElement();
el.firstName = 'Julien';
el.lastName = 'Renaux';
await el.updateComplete;

Update lifecycle

The complete list can be found here but here is the main ones:

  1. requestUpdate (called when a property has changed).
  2. performUpdate (called after one or many requestUpdate have been called).
  3. shouldUpdate (controls whether an update should proceed).
  4. update (reflects property values to attributes and calls render to render DOM).
  5. render (uses lit-html to render the element template).

Default rendering

By default, performUpdate is scheduled as a microtask after the end of the next execution of the browser event loop. This means that we can have async rendering but still blocking paint by using microtask queue.

Imagine a tree of nodes where each node takes 50ms (or more) to render (way above the 10ms that makes smooth paint).

When the tree is rendered using the default LitElement microtask timing, browser layout & paint is blocked until the microtask queue is empty (as shown below):

Demo

Source: https://codesandbox.io/s/r1r8x76wrq?from-embed

You can reproduce this at home running this code 😂 (use with caution)

const loop = () => Promise.resolve().then(loop);
loop();

It will block the rendering forever.

Lazy rendering

We just saw that for complex UI trees the default lit-element rendering can be blocking and therefore introduce jank. To prevent that we have the ability to force the browser paint by using the task queue instead of the microtask queue per render batch.

To do so, we can overwrite performUpdate method and use setTimeout or requestAnimationFrame. It will let the browser paint and handle input before the render.

class LazyLitElement extends HTMLElement {
  async performUpdate() {
    await new Promise((resolve) => setTimeout(resolve));
    super.performUpdate();
  }
}

Now it paints intermidiate steps!

Demo

Source: https://codesandbox.io/s/r1r8x76wrq?from-embed

Urgent update

To go even further we can manually force an update to be prioritized on a user input. What we need to do is to store the resolve function on the instance of the element when an update is scheduled.

class LazyLitElement extends HTMLElement {
  [resolveUrgentUpdate]: () => void;

  async performUpdate() {
    await new Promise((resolve) => {
      setTimeout(resolve);
      this[resolveUrgentUpdate] = resolve;
    });
    super.performUpdate();
  }
}

Then we can create a function requestUrgentUpdate as below:

class LazyLitElement extends HTMLElement {
  requestUrgentUpdate() {
    this.requestUpdate();
    if (this[resolveUrgentUpdate] !== undefined) {
      this[resolveUrgentUpdate]!();
    }
  }
}

Calling requestUrgentUpdate will cause the scheduleUpdate promise to resolve earlier, jumping from the task queue up to the microtask queue and therefore forcing an immediate paint.

Thanks for reading, I hope rendering with LitElement is now a bit more comprehensible. If you have any question you can ping me on twitter or leave a comment below.

Big thanks to Kenneth for the review.