Search code examples
javascripthtmlcssweb-component

Does a hidden/transparent element impact rendering performance?


So I have a Polymer app that I am writing. I have written a non-polymer web-component for a loading overlay that I can show whilst Polymer is loading and when the app Websocket is connecting/reconnecting.

Here is an exert of some of the CSS I have to give an indication of what I am doing:

  .overlay {
    background: #000;
    bottom: 0;
    height: 100%;
    left: 0;
    opacity: 0;
    pointer-events: none;
    position: fixed;
    right: 0;
    transition: opacity 0.2s;
    top: 0;
    width: 100%;
    z-index: 9999999;
  }

  .overlay[opened] {
    opacity: 0.8;
    pointer-events: auto;
  }

  .loader {
    display: none;
  }

  .overlay[opened] .loader {
    display: block;
  }

Now this overlay and the CSS based loader animation I have is only used when I load the application realistically, however if the WebSocket were to disconnect it would be shown too.

My question is, for performance reasons, should I be removing the element from the DOM entirely and add it back if its required? Does the fact that the overlay is completely transparent when not in use and the loader animation is hidden mean they have no impact on drawing performance?

Note: I am looking to avoid the "don't micro-optimise" answer if possible ;)


Solution

  • TL;DR:

    In general, a rendered element affects page performance when changes to it trigger repaint on subsequent elements in DOM or when it triggers resize on its parent(s), as resize can get expensive from being fired up to 100 times/second, depending on device.


    As long as changes to your element do not trigger repaint on subsequent elements in DOM tree, the difference between having it rendered, hidden behind some opaque element (or above the content, with opacity:0 and pointer-events:none) and having it not displayed at all is insignificant.

    Changes to your element will not trigger repaint on anything but itself, because it has position:fixed. The same would be true if it had position:absolute or if the changes to it would be made through properties that do not trigger repaint on subsequent siblings, like transform and opacity.

    Unless the loader is really heavy on the rendering engine (which is rarely the case — think WebGL loaders with 3d scenes, materials and lights mapping — in which case it would be better to not display it when not shown to the user), the difference would be so small that the real challenge is to measure this difference, performance wise.

    In fact, I would not be surprised if having it rendered and only changing its opacity and pointer-events properties is not, overall, less expensive than toggling its display property, because the browser doesn't have to add/remove it from DOM each time you turn it on/off. But, again, the real question is: how do we measure it?


    Edit: Actually, I made a small testing tool, with 10k modals. I got the following results, in Chrome, on Linux:

    `opacity` average: 110.71340000000076ms | count: 100
    `display` average: 155.47145000000017ms | count: 100
    

    ... so my assumption was correct: display is more expensive overall.

    The opacity changes are mostly around 110ms with few exceptions, while the display changes are faster when nodes are removed but slower when added.

    Feel free to test it yourself, in different browsers, on different systems:

    $(window).on('load', function () {
      let displayAvg = 0, displayCount = 0,
          opacityAvg = 0, opacityCount = 0;
      for (let i = 0; i < 10000; i++) {
        $('body').append($('<div />', {
          class: 'modal',
          html:'10k &times; modal instances'
        }))
      }
      $(document)
        .on('click', '#display', function () {
          $('.modal').removeClass('opacity');
          let t0 = performance.now();
          $('.modal').toggleClass('display');
          setTimeout(function () {
            let t1 = performance.now();
            displayAvg += (t1 - t0);
            console.log(
              '`display` toggle took ' + 
              (t1 - t0) +
              'ms \n`display` average: ' + 
              (displayAvg / ++displayCount) + 
              'ms | count: ' + 
              displayCount
            );
          })
        })
        .on('click', '#opacity', function () {
          $('.modal').removeClass('display');
          let t0 = performance.now();
          $('.modal').toggleClass('opacity');
          setTimeout(function () {
            let t1 = performance.now();
            opacityAvg += (t1 - t0);
            console.log(
              '`opacity` + `pointer-events` toggle took ' + 
              (t1 - t0) + 
              'ms \n`opacity` average: ' + 
              (opacityAvg / ++opacityCount) + 
              'ms | count: ' + 
              opacityCount
            );
          });
        })
    });
    body {
      margin: 0;
    }
    .buttons-wrapper {
      position: relative;
      z-index: 1;
      margin-top: 3rem;
    }
    .modal {
      height: 100vh;
      width: 100vw;
      position: fixed;
      top: 0;
      left: 0;
      padding: 1rem;
    }
    .modal.display {
      display: none;
    }
    .modal.opacity {
      opacity: 0;
      pointer-events: none;
    }
    .as-console-wrapper {
      z-index: 2;
    }
    <script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
    <div class="buttons-wrapper">
      <button id="display">Toggle `display`</button>&nbsp;
      <button id="opacity">Toggle `opacity` + `pointer-events`</button>
    </div>

    But this average is for 10k elements. Divide it by 10k and it's virtually no difference at all: we're talking less than 0.45% of a millisecond.