0

I ask for help finding the best solution making loops non-blocking the UI.

Given example below, I would expect that the button text changes first to 'I am running'. Instead, the browser get's blocked until the loop finishes and afterward the button text changes.

function addUpTo(n) {
  let total = 0;
  for (let i = 1; i <= n; i++) {
    total += i;
  }
  alert('loop done')
  return total;
}

const button = document.getElementById('button__test')
button.addEventListener("click", function(){
    button.firstChild.data = 'I am running'
    addUpTo(1000000000);
} );
<button id="button__test">Click me</button>

Question:

  1. button.firstChild.data = 'I am running' comes first in code, why is the loop 'gaining the upper hand' is it somehow of higher priority or hoisted?

  2. I've tried with promises, async/await, timeout and this interesting solution passing a function as worker without luck. What is the suggested crossbrowser way in 2019 to solve this issue?

EDIT: Find here my fiddle trying to run the loop as worker: https://codepen.io/t-book/pen/VoqNPY?editors=1011 The reason I have choosen to use it as blob is as I'm using es6 modules. (As far as I know it's not possible yet to use an importes module as worker)

3
  • 3
    using web workers would be the way to go. They should be used to do bigger calculations (like yours) that can otherwise freeze up the browser. Maybe you can post why the webworker didn't work? And what exactly you tried? Commented Aug 12, 2019 at 12:34
  • @tBook do you want the text to go from "I am running 0" to "I am running 1000000" or am I misunderstanding the question? Commented Aug 12, 2019 at 12:45
  • @NikolaDimitroff you are misunderstanding. The issue is about the order. the button text should immediatley change to 'I am running' instead it is updated after the loop as the loop is blocking. Commented Aug 12, 2019 at 12:47

2 Answers 2

2

Your example doesn't actually use web workers.

When you run the code, you have this statement: run(addUpTo(10000000))
This statement doesn't pass the function in. Instead, it first runs addUpTo(10000000) and then passes it into the run() function, so it essentially does run(<some large number>) instead.

If you change your logic to make it actually use Web Workers, it works:

const button = document.getElementById('button__test')

function addUpTo(n) {
  let total = 0;
  for (let i = 1; i <= n; i++) {
    total += i;
  }
  button.innerText = 'done';
  return total;
}

function run(fn, arg) {
  return new Worker(URL.createObjectURL(new Blob(['('+fn+')(' + arg + ')'])));
}


button.addEventListener("click", function(){
    button.innerText = 'running';
    run(addUpTo, 10000000);
} );
Sign up to request clarification or add additional context in comments.

1 Comment

thanks a million. I bang my head so easy but I would have never seen it.
1

Here's the deal - when you execute JS in your browser, the browser executes all of it at once. You don't get to choose when the browser stops working and you also don't get to see any intermediate results - that's how browsers work. They batch all of the changes at once to make your web page run faster. If they didn't do that, imagine adding a new CSS class for each of your 1000 divs at the page and having the browser stop, apply the change, render the screen, apply the next one, render the screen, etc. - this will destroy every browser's performance.

What you can do is tell your browser to leave some work for next frame / after some timeout. This will prevent the browser from blocking as it would only execute a piece of all operations at a time. There are different ways to break the work and I'll show you one below.

Alternatively, yes, you can use a web worker but that seems like an overkill for your task and since the other guy already fixed your web worker code, I'll take care of the other option.

To tell the browser to leave some work for next frame, you can split the loop in separate work items. Let's call each work item a bucket. Then all you want to do is just exit the function when you complete each bucket. So let's do this thing, here's a function that computes just one bucket:

  let total = 0;
  const bucketSize = 1000;
  let nextBucketStart = 0;
  // Here's a function that computes the next bucket
  const addUpBucket = () => {
    for (let i = nextBucketStart; i <= bucketSize; i++) {
      total += i;
    }
    if (bucketStart < n) {
      nextBucketStart += bucketSize;
    }
  }

Now we need to call this function every frame. The browser has this helpful callback called requestAnimationFrame which does exactly that so we arrive at this code which works exactly as you want it - break the loop into buckets of a 1000 each, execute them frame per frame.

function addUpTo(n) {
      let total = 0;
      const bucketSize = 1000;
      let nextBucketStart = 0;
      const addUpBucket = () => {
        for (let i = nextBucketStart ; i <= bucketSize; i++) {
          total += i;
        }
        if (nextBucketStart < n) {
          nextBucketStart += bucketSize;
          requestAnimationFrame(addUpBucket);
        }
      }
      addUpBucket();
      alert('loop done')
      return total;
    }

    const button = document.getElementById('button__test')
    button.addEventListener("click", function(){
        button.firstChild.data = 'I am running'
        addUpTo(100000000);
    } );
<button id="button__test">Click me</button>

4 Comments

Hey @Nikola Dimitroff thanks a lot for this complete answer. Unfortunately your code snippet is not running. I guess it's because bucketStart is not defined?
@tBook Sorry, my bad - I did some last minute renaming and I failed to rename some of the vars. Should be fixed now
really nice @Nikola Dimitroff I guess the alert would go in an if when finished, right? codepen.io/t-book/pen/VoqNPY?editors=1010 (currently it fires immediately). I've learned a lot and try to find some read for requestAnimationFrame. Thanks!
@tBook It will fire when the calculation completes, just put a bigger a number (call addUpTo with more zeroes) and it will take longer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.