javascriptfor-loopasynchronousrequestanimationframe

JavaScript: how to update progress bar inside nested for loops?


I have some nested for loops that take a while to run, so I want to display a progress bar. The problem is that this is not an inherently async process, it is a block of code with 3 nested loops. I tried a slew of ways to yield so as to render the page, with and without requestAnimationFrame(), async await, and an async generator w/for await...of. The snippet below represents the only way I could get it to work.

Is there a better way to do this? One that doesn't involve calling the generator function inside the animation callback, for example.

let i, start, val;
const progress = document.getElementsByTagName("progress")[0];
function run() {
  i = 0;
  val = 0;
  start = performance.now();
  requestAnimationFrame(animateProgress);
}
function animateProgress() {
  const next = loop().next();
  if (!next.done) {
    progress.value = next.value;
    frame = requestAnimationFrame(animateProgress);
  }
  else
    console.log(`Calculations took ${performance.now() - start}ms`);
}
function* loop() {
  let j;
  while (i < 100) {
    for (j = 0; j < 100; j++) {
      ++val;
    }
    ++i;
    yield val;
  }
}
* {
  font-family:monospace;
  font-size:1rem;
}
<button onclick="run()">Run</button>
<progress value="0" max="10000"></progress>


Solution

  • Run your calculations in a worker if they aren't DOM manipulations:

    const progress = document.getElementsByTagName("progress")[0];
    
    const executeFunctionInWorker = function(fn, progressCb){
    
      return new Promise(resolve => {
        const blob = new Blob([`
        let start = performance.now();
        (${fn.toString()})();
        postMessage({duration: performance.now() - start});
        `], {type: 'application/javascript'});
        const worker = new Worker(URL.createObjectURL(blob));
        worker.addEventListener('message', e => {
          if('duration' in e.data){
            resolve(e.data.duration);
          }else{
            progressCb(e.data.progress);
          }
        });
      });
      
    };
    
    const doComputation = () => {
      let count = 0;
      while(count++<1000){
        structuredClone(Array.from({length: 30000}, () => Math.random()));      
        postMessage({progress: Math.round(count/1000 * 100)});
      }
    };
    
    const run = async() => {
      $run.disabled = true;
      const duration = await executeFunctionInWorker(doComputation, value => progress.value = value);
      $run.disabled = false;
      console.log('Calculations took', duration, 'ms');
    };
    * {
      font-family:monospace;
      font-size:1rem;
    }
    <button id="$run" onclick="run()">Run</button>
    <progress value="0" max="100"></progress>

    On the main thread (note how much it's slower, since we need to calculate and report the progress):

    const progress = document.getElementsByTagName("progress")[0];
    
    const doComputation = async () => {
      let count = 0;
      while(count++<1000){
        structuredClone(Array.from({length: 30000}, () => Math.random()));      
        await new Promise(resolve => requestAnimationFrame(() => (progress.value = Math.round(count/1000 * 100), resolve())));
      }
    };
    
    const run = async() => {
      $run.disabled = true;
      const start = performance.now();
      await doComputation();
      $run.disabled = false;
      console.log('Calculations took', performance.now() - start, 'ms');
    };
    * {
      font-family:monospace;
      font-size:1rem;
    }
    <button id="$run" onclick="run()">Run</button>
    <progress value="0" max="100"></progress>