Web UI Performance — Behind The Scenes
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.
Let’s see an example:
element.addEventListener(‘click’, function() {
function1();
function2();
});function1() {
function3();
}
The above code’s stack will be processed this way:
- 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:
Let’s see an example:
setTimeout(() => {
console.log('a');
}, 1000);
console.log('b');
What happens is like this:
setTimeout
andconsole.log(‘b’)
are in the CallStack.- They are running and being removed from the CallStack.
- After 1000ms, the timer fires and
console.log(‘a’)
is entered into the CallBack Queue. - Because the CallStack is empty,
console.log(‘a’)
is entered into the CallStack and ran.
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.
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.
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:
- The main thread executes the JS code.
- The code changes the opacity of a DOM element.
- The browser needs to update the screen.
- The rendering cycle is initiated in order to apply the update.
- Repeat.
Optimizations
The following code takes a list of items and changes their width
to a random width:
The next code does the exact same thing.
Let’s see what happens behind the scenes in each code…
Here is the chrome performance tool output of action’s recording:
Code A:
Code B:
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?
- 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:
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.
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:
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:
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).
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.