Search code examples
javascriptnonblockinganimated-gif

How to show/hide animated GIF during the execution of a function?


I want to display an animated GIF (spinner icon) during the execution of a function and then hide the GIF when the function is completed.

However, the code that I'm implementing does not display the GIF apparently because the function is hogging the thread and not allowing the GIF to ever be shown.

Example: (https://jsfiddle.net/jzxu8cLt/)

HTML
<button onclick="go();">Go</button>
<span id="result"></span>
<hr>
<img style="display: none;" id="spinner" src="https://upload.wikimedia.org/wikipedia/commons/b/b1/Loading_icon.gif">
JS
function longLoop() {
  let count = 1000000000;
  let result = 0;
  for (let i = 1; i <= count; i++) {
    result = i;
  }
  return result;
}

function go() {
  let spinnerElement = document.getElementById('spinner');
  let resultElement = document.getElementById('result');
  spinnerElement.style.display = 'block';
  let result = longLoop();
  resultElement.innerHTML = result;
  spinnerElement.style.display = 'none';
}

The go() function above should do the work of showing the GIF prior to calling a long running function, then will hide the GIF when the long running function is completed.

However, the GIF never has a chance to be displayed in the browser and I can't figure out how to get the GIF to be displayed. Note: If I place a breakpoint at the line that calls the longLoop() function, the GIF is displayed correctly.

What do I need to do to allow the GIF to be displayed and animated during the longLoop() function?


Solution

  • Yep you're right, since JavaScript is single-threaded, the longLoop function is blocking the event loop and not giving the browser a chance to render the gif.

    JsFiddle solution: https://jsfiddle.net/12yf4tgd/

    I used setTimeout to make the longLoop function asynchronous and wrap that in a Promise so that the other assignments run after it completes.

    function go() {
      let spinnerElement = document.getElementById('spinner');
      let resultElement = document.getElementById('result');
      spinnerElement.style.display = 'block';
    
      new Promise(resolve => setTimeout(() => {
        resolve(longLoop())
      }))
      .then((result) => {
        spinnerElement.style.display = 'none';
        resultElement.innerHTML = result;   
      })
    }
    

    Since it is being handled elsewhere by some Web Api and not on the main call stack, it won't be blocking.

    If you don't want to use .then() because of all the nesting, the other option is to make go() and async function to take advantage of async/await syntax.

    async function go() {
      let spinnerElement = document.getElementById('spinner');
      let resultElement = document.getElementById('result');
      spinnerElement.style.display = 'block';
      let result = await new Promise((resolve, reject) => {
        setTimeout(() => resolve(longLoop()))
      })
      spinnerElement.style.display = 'none';
      resultElement.innerHTML = result;
    }
    

    They both work the same way.