Web UI Performance — Behind The Scenes

Kfir Zuberi
WalkMe Engineering
8 min readJun 10, 2019

--

How do async processes happen in a single threaded javascript? How can you help the browser render faster on screen — and prevent heavy rendering from causing your JS code to falter?

JavaScript runs in a single thread. This thread runs in an infinite loop (a.k.a. CallStack). Every async action (like user click, promise resolve, timer fire, etc…) calls a function (callback).

From then on, all of the synchronous calls made by this callback or its children, are part of that CallStack and are ready to be called in the next cycle. Once the synchronous code finishes running, it is removed from the stack.

The JavaScript main thread loop

Let’s see an example:

element.addEventListener(‘click’, function() {
function1();
function2();
});
function1() {
function3();
}

The above code’s stack will be processed this way:

callback execution — detailed explanation below
  1. The user clicked the element.

2. The anonymous function (the event’s callback) is then entered into the CallStack.

3. function1 enters the CallStack.

4. function3 enters the CallStack.

5. function3 finishes running and removed from CallStack.

6. function1 finishes running and removed from CallStack.

7. function2 enters the CallStack.

8. function2 finishes running and removed from CallStack.

9. Anonymous function finishes running and is removed from the CallStack.

This is how synchronous processes are being handled.

Async Processes

We saw how the system is handling a single asynchronous event that has only synchronous flow. What happens if we have another asynchronous call during the same run?

In addition to the CallStack, JavaScript has a “Callback Queue”. This is a queue with JavaScript code which needs to be evaluated. Each cycle, the main thread checks if there are any items in the Callback Queue. If so, it waits until the regular queue (CallStack) is empty and only then puts the next method from the callback queue:

The JavaScript main thread loop with the Callback Queue

Let’s see an example:

setTimeout(() => {
console.log('a');
}, 1000);
console.log('b');

What happens is like this:

  1. setTimeout and console.log(‘b’) are in the CallStack.
  2. They are running and being removed from the CallStack.
  3. After 1000ms, the timer fires and console.log(‘a’) is entered into the CallBack Queue.
  4. Because the CallStack is empty, console.log(‘a’) is entered into the CallStack and ran.
Example of evaluated

Rendering

So far we saw how the browser runs pieces of JavaScript code. There are other processes that the browser handle — the browser also renders something on the screen.

A single process of the JavaScript callback and the rendering cycle is called a Frame.

A Frame as recorded in chrome performance tool

Besides the “Callback Queue”, there is another process which is called Rendering Cycle. This cycle is executed every time there’s a change in the window — namely style or DOM changes.

JavsScript Rendering Cycle
console.log('a');
let item = document.getElementById('item');
item.style.display = 'none';

In the CallStack, our code changes the display property — which calls for a layout change. The browser then starts a rendering cycle in order for the change to take effect.

The CallStack can get your app non-responsive

This is the JavaScript code attempt at animation:

The result is this:

Because we used while(true), we will never go out from the infinite loop. This means the function will never finish and hence will never be removed from the CallStack. The main thread will be busy with running the loop and won’t execute the rendering cycle — so the screen won’t be updated.

Flowing animation

We learned about how async processes are entered into the callback queue. We will use setInterval and cause the opacity changes to work without blocking the main thread. (later we will see a different way, and understand why setInterval is not the correct way to use it):

Every 60 milliseconds, the opacity of the element will be changed — Fade in or Fade out according to the direction.

And the result:

Let’s break down what’s happening behind the scenes:

  1. The main thread executes the JS code.
  2. The code changes the opacity of a DOM element.
  3. The browser needs to update the screen.
  4. The rendering cycle is initiated in order to apply the update.
  5. Repeat.

Optimizations

The following code takes a list of items and changes their width to a random width:

Code A — Set randomize width to the items

The next code does the exact same thing.
Let’s see what happens behind the scenes in each code…

Code B — Set randomize width to the items

Here is the chrome performance tool output of action’s recording:

Code A:

Performance screen capture code A— Force recalculate style

Code B:

Performance screen capture code B—optimized

What we see is that Code A had lots of layout recalculations while Code B had only 1. This phenomenon is an indication of something called “Forced Layout” — and it can impact the performance of your application. What is it? How does it affect your performance? and, how can you avoid it?

The rendering cycle flow — Frame
  • JavaScript —

Typically JavaScript is used to handle work that will result in visual changes, whether it’s jQuery’s animate function, sorting a data set, or adding DOM elements to the page. It doesn’t have to be JavaScript that triggers a visual change, though: CSS Animations, Transitions, and the Web Animations API are also commonly used.

  • Style calculations —

The process of understanding which CSS rules and style apply to which elements based on matching selectors (.button/#id .class). Then apply the final styles for each element.

  • Layout —

Now, after the browser knows which rules apply to every element, it can start to calculate how much space it takes up and where it placed on the screen. It means that one element can affect others. For example, the width of the <body> element will affect its children’s widths… so the process can be quite involved for the browser.

  • Paint —

Filling in the pixels- it means: drawing text, colors, images, borders, and shadows — every visual part of the elements.

  • Composite —

Ordering the element in the page. This part is important for overlapping elements since a mistake could result in one element appearing on top of another incorrectly.

Now we know more about the main thread, how the Callback Queue works, and how the rendering cycle involved and take part. Why it is important for us to know it?

Guess what… writing code in wrong way, directly affect our applications and may cause performance issues (as we saw before).

The most famous one is the recalculating style, we will see it within this example:

In line 1 we changed the class and then in line 2 we ask for the property, which forces calling the layout part. Because we changed the style of the element in the line before, and the new style hasn’t been calculated yet. Then in line 3+4, we do it again. now imagine it inside a loop which executed frequently and on a page with many DOM element — not recommended.

Can we write it in a better way?

In this way, we don’t force calculating by doing the DOM changed before asking property which force calculate layout.

What forces layout/reflow

The following properties will force layout calculating:

  • elem.offsetLeft, elem.offsetTop, elem.offsetWidth, elem.offsetHeight, elem.offsetParent
  • elem.clientLeft, elem.clientTop, elem.clientWidth, elem.clientHeight
  • elem.getClientRects(), elem.getBoundingClientRect
  • elem.scrollBy(), elem.scrollTo()
  • elem.scrollIntoView(), elem.scrollIntoViewIfNeeded()
  • elem.scrollWidth, elem.scrollHeight
  • elem.scrollLeft, elem.scrollTop (also setters)
  • window.getComputedStyle()

Here you can find the full list

How To Reduce Reflows?

There are many code techniques and optimization we can adapt in order to change the UI in a “Smart” way:

1. Use the Chrome DevTools’ Timeline and JavaScript Profiler to assess the impact of JavaScript.

Use the chrome developer tool to track and measure performance issues such as drawing time, bottle-necks and more:

The chrome developer tool (image from google blog —Optimize JavaScript Execution)

2. Move long — running JavaScript off the main thread to Web Workers.

Move heavy javascript executions to WebWorkers

3. Order UI changes lines.

When you need to change the UI (add DOM element, change style property, etc…), keep the getter before the setter. If you change items in a loop or frequently, save the UI value getter in variables in order to reduce style calculations.

4. Avoid setTimeout or setInterval for visual updates — always use requestAnimationFrame instead.

We can requestAnimationFrame — rAF:

We will want to make visual changes right at the start of the frame (at the beginning of the rendering cycle). To make sure that your JavaScript will run at the start of a frame, use requestAnimationFrame.

Animate with requestAnimationFrame

There are many libraries that may use setTimeout or setInterval to do visual changes like animations, but the problem with this is that the callback will run at some point in the frame, possibly right at the end, and that can often have the effect of causing us to miss a frame, resulting in jank.

For example, the following:

Animate with setInterval

Both codes (Animate with requestAnimationFrame and Animate with setInterval) will do the same work and will animate the div, but the performance tool will show a different picture:

A performance snapshot of setInterval animation

The above snapshot was taken during the setInterval animation.

As we can see, in a single frame we invoked JS calculation for changing the position twice which means that only the final change in the frame will be shown, and the first change is unnecessary.

In this case, the first change has a small calculation complexity and thin impact. It could be more complex JS calculations or even cause reflows (as we saw before when measuring style properties from the DOM before they have been calculated).

On the other hand, in the below snapshot, I used the requestAnimationFrame, and we can see that the JS calculate been invoked only once in a single frame (closest to the Rendering cycle).

A performance snapshot of requestAnimationFrame animation

Meet fastdom — library for batching DOM read/write operation

fastdom is a library for managing read (measure) / write (mutate) jobs. By batching DOM access we avoid unnecessary document reflows and dramatically speed up layout performance:

fastdom.measure(() => {
console.log('measure');
});

fastdom.mutate(() => {
console.log('mutate');
});

For full API and documentation — fastdom

Summary

In this article, we saw examples of bad practices when writing UI code changes and how they directly impact performance.

We dived into the JavaScript Rendering Cycle and its different components.

In addition, we mentioned ways and techniques to write UI code changes correctly and reduce performance issues.

--

--