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>
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>