1

I have got a vanilla Javascript that sets an img's src attribute to a data-uri. In the function assigned to the onload event, I try to show the progress of some preparations on the page that might take a while (multiple canvas elements are generated and a few settings are set).

This is the function, that should render a progress bar:

let progress = document.querySelector('.progress');

function showProgress(message, percent) {
    console.log(message + ' (' + (percent * 100).toFixed(0) + '%)');
    progress.querySelector('.progress--message').innerHTML = message;
    progress.querySelector('.progress--bar-container--bar').style.width = (percent * 100).toFixed(0) + '%';
}

The code is run like this:

img.onload = function() {
    showProgress('Preparing image...' .1);

    // Some preparations...

    showProgress('Preparing tools...', .2);

    // etc...

    showProgress('Done...', 1);
}

The console logging works as intended, but the rendering of the DOM elements stops until the end of the function and displays the "done" state after everything is ready.

Is there any render-blocking in the onload event handling and can it be circumvented? Or am I missing something? The code itself does not have any effect on the outcome. Even the first line in the function does have this strange behaviour.

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
zoku
  • 7,056
  • 3
  • 20
  • 27
  • It depends on the content of the `Some preparations...` and the `etc`, can you show what's inside those sections? – CertainPerformance Jan 23 '21 at 00:21
  • 4
    Generally nothing is rendered until you return from JavaScript to the main event loop. This allows JS to make many changes to the DOM without the user seeing all the intermediate steps. – Barmar Jan 23 '21 at 00:23

2 Answers2

1

Unless you go with background Web Workers massaging (more complicated, espacially in your situation), the rendering of your progress bar can only occur after a complete return from your script; therefore, after each showProgress call. Now, how to return, render then restart for the remaining? Well, that's where a setTimeout of 0 millisecond comes handy. With it, the following should work:

img.onload = function() {
    showProgress('Preparing image...' .1);
    setTimeout(function() {
        // Some preparations...
        showProgress('Preparing tools...', .2);
        setTimeout(function() {
            // etc...
            showProgress('Done...', 1);
        }, 0);
    }, 0);
}

An alternative approach to nesting code would be a kind of recursive call - yet each call will complete before the following will start - on an array of steps, like this:

steps = [
  function() { // first step (img loading just completed so, show Preparing it right away)
      showProgress('Preparing image...', .1);
  },
  function() { // second step
      // Some preparations...
      showProgress('Preparing tools...', .2);
  }, // other steps...
  function() { // last step
      // Last things to do...
      showProgress('Done...', 1);
  }
];
// function that processes "steps" - array of at least 1 function - until empty
function displayAfterEachStep() {
    steps.shift()(); // Pull out the first step and run it
    if(steps.length > 0) // if there's still steps
         setTimeout(displayAfterEachStep, 0); // Recall the process after page rendering
}
img.onload = displayAfterEachStep;

Many other good ideas to get around that lack of Rendered Event in HTML can be found here and around.

denis hebert
  • 128
  • 10
0

I came back to the matter after working on some other projects and solved my problem using Promises like this:

showProgress("Loading image...", 0.0) // Reset progress bar

const img = document.createElement("img") // Get image element

const promise = new Promise((resolve, reject) => { // Instantiate a Promise
  img.onload = resolve // Set onload callback to resolve (this triggers the chain)
  img.onerror = reject

  img.src = "/path/to/image.jpg" // Set src attribute to start loading image
})
.then(() => { // When promise is resolved: Start step 2
  showProgress("Preparing image...", 0.3)
  // ...
})
.then(() => { // When promise is resolved: Start step 3
  showProgress("Preparing tools...", 0.6)
  // ...
})
.then(() => { // When promise is resolved: Finish up
  showProgress("Ready...", 1)
  // ...
})

With Promises you can put asynchronous tasks in an order while still maintaining readability.

Further reading: Promises on MDN

zoku
  • 7,056
  • 3
  • 20
  • 27
  • As per [MDN](https://developer.mozilla.org/en-US/docs/Web/API/HTML_DOM_API/Microtask_guide), promises are microtasks and "microtasks will be executed until the microtask queue is empty, and then the browser may choose to update rendering". Still, this is a very good answer to my fast sequence of clicks and DOM changes **before** rendering. Yes, in my case, rendering in between useless and takes time for nothing. – denis hebert Jul 15 '22 at 06:02