Skip to content

Front-End Performance

SeungAh Hong19min read

JavaScript Memory Leaks

Accidental Global Variables

  • Global variables cannot have their memory released (when they are not set to null or reassigned).
// 브라우저의 경우 글로벌 객체는 window다.
function foo(arg) {
  bar = 'this is a hidden global variable';
}
 
function foo() {
  this.variable = 'potential accidental global';
}
 
// Foo가 호출되면, this는 글로벌 객체인 윈도우를 가리키게 된다.
foo();
 
// use strict 엄격한 모드로 선언해서 실수를 방지한다.

Forgotten Timers or Callbacks

var someResource = getData();
setInterval(function() {
    var node = document.getElementById('Node');
    if(node) {
        // Do stuff with node and someResource.
        node.innerHTML = JSON.stringify(someResource));
    }
}, 1000);
 
 
// 이 element는 onClick에서 참조됨
// 과거 특정 브라우저 (IE6)가 순환 참조를 잘 관리하지 못했기 때문에 이 부분은 특히 중요
var element = document.getElementById('button');
 
function onClick(event) {
    element.innerHtml = 'text';
}
 
element.addEventListener('click', onClick);
 
// 객체를 없애기전에 이러한 observer를 명시적으로 제거하는 것은 좋은 관례
element.removeEventListener('click', onClick);
element.parentNode.removeChild(element);

References Outside the DOM

//
var elements = {
  button: document.getElementById('button'),
  image: document.getElementById('image'),
  text: document.getElementById('text'),
};
 
function doStuff() {
  image.src = 'http://some.url/image';
  button.click();
  console.log(text.innerHTML);
}
 
function removeButton() {
  document.body.removeChild(document.getElementById('button'));
 
  // 이 시점에서도 여전히 elements에서 button의 참조를 가지고 있다.
  // 이 경우 button element는 여전히 메모리에 있으며, GC에 의해 해제 될 수 없다.
}

Front-End Performance Optimization

Rendering Optimization - Reducing Reflow and Repaint

So far we have looked at how a web page is rendered. So how can we optimize web performance? To understand this, we first need to cover Reflow and Repaint.

Reflow (Layout)

Just because the page is finally drawn after going through the rendering process described above does not mean that the rendering process is entirely finished. When some action or event modifies layout values such as the size or position of an HTML element, the Layout process is performed again, including the affected child nodes and parent nodes. When this happens, the Render Tree and the size and position of each element are recalculated. This process is called Reflow.
// reflow 발생 예제
function reflow() {
  document.getElementById('content').style.width = '600px';
}

Typical cases where Reflow occurs are as follows.

- On the initial page rendering (the first Layout process) - On window resizing (when the Viewport size changes) - Adding or removing a node - Changing an element's position or size (left, top, margin, padding, border, width, height, etc.) - Font changes (text content) and image size changes (when switching to an image of a different size)

Repaint (Paint)

Performing only Reflow does not reflect the changes on the actual screen. Just like the rendering process described above, a process to draw the Render Tree back onto the screen is required. Ultimately, the Paint step is performed again, and this is called Repaint.

However, Repaint does not always require Reflow to occur first. When style properties that do not affect the layout, such as background-color or visibility, are changed, there is no need to perform Reflow, so only Repaint is performed.

Reducing Reflow and Repaint

The performance optimization covered in this post is simply about introducing ways to reduce Reflow and Repaint operations. The content below only covers what has been researched so far. In addition, since this is theoretical content and excerpted material rather than something actually tested, it needs verification. The content below will be continuously updated.

Use display: none instead of visibility: invisible for unused nodes

Since visibility: invisible still occupies layout space, it becomes a target for reflow. However, display: none does not occupy Layout space, so it is completely excluded from the Render Tree.

Avoid using properties that trigger Reflow and Repaint

Below are the CSS properties that trigger Reflow and Repaint respectively. Since Repaint inevitably occurs when Reflow occurs, it is better to use properties that only trigger Repaint rather than properties that trigger Reflow whenever possible.

Typical properties that trigger Reflow

position width height left top
right bottom margin padding border
border-width clear display float font-family
font-size font-weight line-height min-height overflow
text-align vertical-align white-space ...

Typical properties that trigger Repaint

background background-image background-position background-repeat background-size
border-radius border-style box-shadow color line-style
outline outline-color outline-style outline-width text-decoration
visibility ....

There are also properties such as transform and opacity that trigger neither Reflow nor Repaint. Therefore, using transform instead of left, right, width, height, and using opacity instead of visibility/display, helps improve performance.

Reducing the number of affected nodes

For elements that have a lot of animation or frequent layout changes by combining JavaScript and CSS, you can reduce the number of affected surrounding nodes by using position: absolute or fixed. When there are no affected nodes at all, as with fixed, the Reflow process is not needed at all, so only the Repaint operation cost is incurred.

Another approach is to change the element to absolute or fixed when the animation starts and restore it to its original state when the animation ends, which also helps reduce Reflow and Repaint operations.

Reducing frames

Simply put, an element that moves 3px every 0.1 seconds reduces the Reflow and Repaint operation cost by a factor of three compared to an element that moves 1px every 0.1 seconds. Therefore, you can improve performance by slightly reducing the smoothness of the effect.

Ways to avoid or minimize reflow

  • When you want a style change based on a class change, add it to a node located as far down in the DOM structure as possible.

  • Add the class to the node at the very bottom of the DOM tree

  • For elements with animation, specify position: fixed or position: absolute whenever possible

    • Animations that implement positional movement (changing width or height values, etc.) cause reflow to occur repeatedly within a short period of time. So it is best not to use them, but if you must, apply the position: absolute or position: fixed property to the animated element. Since it does not affect other elements, reflow occurs only for that element rather than the entire page.
  • When you need to make style changes through JS, handle them all at once whenever possible

// style을 여러번 호출(7.7ms), 클래스를 통하여 스타일 변화(5.3ms)
var div = document.getElementsByTagName('div');
for (var i = 0; i < div.length; i++) {
  div[i].style.height = '80px';
  div[i].style.backgroundColor = '#00f';
  div[i].style.display = 'inline-block';
  div[i].style.overflow = 'hidden';
  div[i].style.fontSize = '40px';
  div[i].style.color = '#fff';
}
 
var div = document.getElementsByTagName('div');
for (var i = 0; i < div.length; i++) {
  div[i].className = 'block';
}
  • Avoid inline styles as much as possible
    • Inline Style: a short, single-line style specified and used directly inside a tag. Used mixed with HTML
<p style="color:#ff0a00">이 문장은 인라인 스타일이 적용되었습니다.</p>
  • You should avoid table layouts.
    • When you use a table layout, the width is calculated according to the table values, so rendering becomes slower. Therefore, it is better not to use table layouts except when absolutely necessary. If you do use one, using the CSS property table-layout: fixed can make rendering a bit faster.
10×10 테이블
table-layout: fixed 미 적용
table-layout: fixed 적용
table-layout: fixed 미 적용(0.6ms) < table-layout: fixed 적용(0.4ms)
 
100×100 테이블
table-layout: fixed 미 적용
table-layout: fixed 적용
table-layout:fixed 미 적용(35.4ms) < table-layout:fixed 적용(27.1ms)
  • It is good to trim CSS descendant selectors down to only what is necessary.
    • This is relevant not so much to reflow itself, but to the CSS Recalculation that reflow triggers. CSS rules are evaluated from right to left. This process continues until there are no more matching rules or a non-matching rule is found. Therefore, using unnecessary selectors can degrade performance.
<div class="reflow_box">
  <ul class="reflow_list">
    <li>
      <button type="button" class="btn">버튼</button>
    </li>
 
    <li></li>
    <li>
      <button type="button" class="btn">버튼</button>
    </li>
 
    <li></li>
  </ul>
</div>
 
/_ 잘못된 예 _/ .reflow_box .reflow_list li .btn{ display:block; } /_ 올바른 예
_/ .reflow_list .btn { display:block; }
  • In the case of IE, avoid JS expressions in CSS.
    • The reason CSS expressions are so costly is that the expression is recalculated every time the entire document or part of the document is Reflowed.
    • This ultimately means that when a reflow occurs due to a change such as an animation, the expression may be recalculated thousands or tens of thousands of times per second depending on the case.
.expression {
  width: expression(
    document.documentElement.clientWidth > 0 ? '1000px': 'auto'
  );
}
  • Minimizing Reflow using caching
.expression { width: expression(document.documentElement.clientWidth > 0 ? '1000px' : 'auto'); }
function collect() {
    var elem = document.getElementById('container');
    var cw = elem.style.width;
 
    return parseInt(cw, 10) * parseInt(cw + document.documentElement.clientWidth, 10);
    return false;
}
  • Minimizing DOM usage
    • When adding nodes using a document fragment (document.createDocumentFragment), a node clone (elem.cloneNode), or a character array ([]), you can reduce cost by minimizing DOM access, as shown in the code below.
  1. The basic way to add an element.
function notReflow() {
  var elem = document.getElementById('container');
 
  for (var i = 0; i < 10; i++) {
    var a = document.createElement('a');
    a.href = '#';
    a.appendChild(document.createTextNode('test' + i));
    elem.appendChild(a);
  }
 
  return false;
}
  1. Adding elements using a document fragment
function notReflow() {
  var frag = document.createDocumentFragment();
 
  for (var i = 0; i < 10; i++) {
    var a = document.createElement('a');
    a.href = '#';
    a.appendChild(document.createTextNode('test' + i));
    frag.appendChild(a);
  }
 
  document.getElementById('container').appendChild(frag);
 
  return false;
}
  1. Adding elements using a node clone
function notReflow() {
  var elem = document.getElementById('container');
  var clone = elem.cloneNode(true);
 
  for (var i = 0; i < 10; i++) {
    var a = document.createElement('a');
    a.href = '#';
    a.appendChild(document.createTextNode('test' + i));
    clone.appendChild(a);
  }
 
  elem.appendChild(clone);
 
  return false;
}
  1. Adding elements using a character array
function notReflow() {
  var h = [];
  for (var i = 0; i < 10; i++) {
    h.push('test' + i + '');
  }
  document.getElementById('container').innerHTML = h;
  return false;
}

Test results by situation:

First situation: 153ms Second situation: 136ms Third situation: 129ms Fourth situation: 127ms

  • Except for the first situation, the remaining situations did not show a large difference in performance, but the character array approach using innerHTML, an element property, produced a somewhat faster result than the methods using object members (fragment, clone).

References

Coding techniques that improve front-end performance

Use an object/Map instead of an array

  • Use an IF statement instead of handling exceptions first

Use an IF statement instead of handling exceptions first

Use as few loops as possible

Use a basic loop

Use built-in DOM methods

References

Improving Canvas Performance

Pre-render with an offscreen canvas

Anyone who has done image processing programming already knows this, but instead of rendering to the canvas immediately, render using RequestAnimationFrame, following a principle similar to an image buffer or double buffering.

Batch drawing operations into a single call

For example, when drawing lines such as a rectangle on the canvas, rather than calling beginPath() and stroke() for each line, call beginPath() once, finish all the drawing operations using moveTo and lineTo, and then call stroke(). This will make immediate sense when you look at the sample code.

Avoid unnecessary canvas state changes

This is similar to the point above about avoiding unnecessary operations.

Render only the changed part of the canvas state

This means finding the bounding box of the part of the image that changed and rendering only that part.

Compose the canvas in layers for complex scenes

This is similar to the offscreen canvas point mentioned at the beginning. Additionally, even if you compose canvases by stacking them and render them, the GPU renders them all at once through alpha compositing, so it is beneficial.

Avoid shadow blur effects

Obviously, this means turning off blur or shadow effects. By the way, I didn't know the canvas supported this out of the box.

Learn the various ways to clear the canvas

HTML5's canvas uses what is called Immediate mode, outputting directly to the display without image buffering. When you make something like an animation, you need to clear the previous frame in order to draw the next one. In this case, instead of clearing the entire canvas, track the bounding box of the part of the image that changed, as mentioned above, and use something like clearRect.

Avoid floating-point coordinates

When you place an image, if the coordinates are floating-point, anti-aliasing is applied automatically. Since we said to avoid blur effects above, it is best for coordinates to always land on integers.

Optimize using RequestAnimationFrame

If you have done Windows programming, you may have noticed that you can think of it as the UIThread. Unfortunately, however, not all browsers support it.

Creating the DOM (Document Object Model) and CSSOM (CSS Object Model)

The very first step is downloading the HTML and CSS received from the server. And since HTML and CSS files are just plain text, they are turned into an Object Model for easier computation and management. The HTML and CSS files are turned into the DOM Tree and CSSOM respectively.

The Rendering Process

Creating the DOM (Document Object Model) and CSSOM (CSS Object Model)

The very first step is downloading the HTML and CSS received from the server. And since HTML and CSS files are just plain text, they are turned into an Object Model for easier computation and management. The HTML and CSS files are turned into the DOM Tree and CSSOM respectively.

The rendering process

You can find the detailed process of how each document (HTML, CSS) is parsed and how the DOM Tree is formed in Google's developer documentation.

To add a bit more TMI here, the rendering engine is built to display content as quickly as possible for a better user experience. Therefore, even before all HTML parsing is finished, it performs the subsequent steps to output some content that can be shown to the user in advance.

Creating the Render Tree

Once the DOM Tree and CSSOM Tree are built, the Render Tree is then created using the two. Unlike the DOM Tree, which contains only the structure and text of pure elements, the Render Tree has style information set on it and consists only of the nodes that are actually rendered on the screen.

Render Tree diagram

So while you can understand that style information is set on each element here, you might question the statement that it consists only of the nodes actually rendered on the screen, thinking "Aren't all elements rendered on the screen?"

To get to the point: yes, they are not. As a simple example, a node with the display: none property set does not occupy any space on the screen, so it is excluded from the process of building the Render Tree. To give a bit more of a tip here, visibility: invisible behaves similarly to display: none, but since it occupies space and only makes the element invisible, it is included in the Render Tree.

Layout

The Layout step calculates the exact position and size of each node within the browser's Viewport. To put it plainly, it is the step that calculates at what position and what size each node of the generated Render Tree will be output on the browser screen, according to the styles and properties it holds. Through the Layout step, relative position and size properties such as %, vh, and vw are converted into pixel units that are actually drawn on the screen.

Render Tree diagram

Here, the Viewport refers to the area and size of the browser in which graphics are displayed. The viewport varies depending on the display size for mobile and the browser window size for PC. And since the size and position of each element drawn on the screen are often computed relatively using %, vh, vw, etc., the calculation must be redone every time the viewport size changes.

Paint

Once the Layout calculation is complete, the elements are now drawn on the actual screen. Using the Render Tree, for which the position, size, and style calculations of the elements were already completed in the previous step, the actual pixel values are filled in. At this time, text, color, images, shadow effects, and so on are all processed and drawn.

At this point, the more complex the styles that need to be processed, the more time the Paint step takes. As a simple example, a plain solid background-color paints quickly, whereas gradients and shadow effects take comparatively longer to paint.