Updating DOM during long running operation

21 במרץ 2015

Suppose I need to implement a long running operation (longer than a few seconds) and during that operation display some user feedback

Well, JavaScript is single threaded. This means that during operation execution the single thread is busy and cannot handle UI events like mouse move and mouse click. From end user perspective the UI is dead.

Modern solution is to use Web Worker. As long as your algorithm is pure (only accesses JavaScript objects) then you are OK. However, if the algorithm needs to access DOM objects you cannot use web workers since they have no access to the DOM

I agree that the first case is the common one. However, for this post I would like to explore the second case. How can I implement a long running operation which must be executed under the "UI" thread but still give some feedback to the user during execution.

For this discussion lets implement a dummy long running operation

function longRunningOperation() {
    for (var i = 0; i < 500; i++) {
        sleep(10);
    }
}
 
function sleep(timeout) {
    var begin = new Date();
 
    while (new Date() - begin < timeout) {
    }
}

Let's integrate it into a plain AngularJS controller

angular.module("MyApp").controller("HomeCtrl"function ($scope) {
    $scope.start = function () {
        longRunningOperation();
    }
 
    function longRunningOperation() {
        var progress = 0;
 
        $scope.message = "RUNNING";
 
        for (var progress = 0; progress < 250; progress++) {
            sleep(10);
 
            $scope.message = progress;
        }
 
        $scope.message = "FINISHED";
    }
});

Running above code yields the following behavior (see Plunker)

  • User clicks the Start button which executes the longRunningOperation function
  • No progress is displayed. UI is considered non responsive
  • Only after longRunningOperation completes a "FINISHED" message is displayed

This is the expected behavior since JavaScript is single threaded and the DOM has no chance to refresh itself until longRunningOperation finishes

The solution is to break the function into multiple "short running" operations and ask the browser to refresh between each two steps. Here is a fixed version (see Plunker)

angular.module("MyApp").controller("HomeCtrl"function ($scope, $q, $timeout) {
    $scope.start = function () {
        longRunningOperation();
    }
 
    function longRunningOperation() {
        var progress = 0;
 
        $scope.message = "RUNNING";
 
        function step() {
            if (++progress == 250) {
                $scope.message = "FINISHED";
 
                return;
            }
 
            sleep(10);
 
            $scope.message = progress;
 
            $timeout(step, 0);
        }
 
        $timeout(step, 0);
    }
});

The longRunningOperation defines an "internal" function named step. The step function represents one step during operation which can be isolated from another steps. After each step we use the $timeout service with zero timeout.

Zero timeout means "I want to execute my code at a later time after the browser processes its event queue and UI is updated". I addition, the zero tells the browser that we want to wait as short as possible.

Running our fixed version, yields the following behavior:

  • User clicks the Start button
  • After each step we use $timeout and therefore the browser has a chance to update the DOM
  • The end user sees a progress from 1 to 250
  • FINISHED message is displayed

 

Here comes the interesting part. How long does is take to the complete the longRunningOperation function ?

Each step is 10 milliseconds long. Assuming DOM update is efficient we expect the whole operation to complete in 250*10 = ~2500 milliseconds

Let's fix our code so it measures the time

angular.module("MyApp").controller("HomeCtrl"function ($scope, $q, $timeout) {
    $scope.start = function () {
        longRunningOperation();
    }
 
    function longRunningOperation() {
        var begin = new Date();
        var progress = 0;
 
        $scope.message = "RUNNING";
 
        function step() {
            if (++progress == 250) {
                var time = new Date() - begin;
                $scope.message = time;
 
                return;
            }
 
            sleep(10);
 
            $scope.message = progress;
 
            $timeout(step, 0);
        }
 
        $timeout(step, 0);
    }
});

On my machine it takes 3769 milliseconds to completes (see Plunker)

This is a significant delay with respect to the pure algorithm time of 2.5 seconds.

You probably thinking about AngularJS. Every $timeout event, Angular executes its internal digest cycle which is known to be not efficient. So, let's remove it (see Plunker):

angular.module("MyApp").controller("HomeCtrl"function ($scope, $q, $timeout) {
    $scope.start = function () {
        longRunningOperation();
    }
 
    function longRunningOperation() {
        var begin = new Date();
        var progress = 0;
 
        $scope.message = "RUNNING";
 
        function step() {
            if (++progress == 250) {
                var time = new Date() - begin;
 
                $scope.$apply(function () {
                    $scope.message = time;
                });
 
                return;
            }
 
            sleep(10);
 
            $scope.message = progress;
 
            setTimeout(step, 0);
        }
 
        setTimeout(step, 0);
    }
});

This time we use setTimeout instead of $timeout. This means that Angular is not aware of the change of $scope.message and no DOM updating happens. So basically, our code almost does nothing. It increments the variable progress by one, waits for 10 milliseconds and then calls setTimeout again.

Surprisingly, new code achieves result of 3541 milliseconds which is still far from the pure calculated time of 2.5 seconds.

What is going here ? Where is the code that cost us more than 1 second of processing ?

The answer lies inside the setTimeout function. Apparently, setTimeout has a maximum frequency.

According to the standard it is recommended that setTimeout waits at least 4 milliseconds before raising the event. This is the exact delay we saw in our code. We are executing 250 steps. Each step invokes setTimeout which adds another delay of 4 milliseconds and thus a total delay of ~1 second.

In case our algorithm is divided into smaller steps the setTimeout overhead is bigger. For example, our code simulate a step of 10 milliseconds while setTimeout overhead is 4 milliseconds. This is a 40% overhead. For a 5 milliseconds step, the overhead is 80% (see Plunker).

Can we improve ? Well, depends …

Probably the best solution is to use Web Workers for executing long running operation. As I mentioned before, it is not a relevant solution when the long running operation is DOM related.

Second option is to use a standard function named setImmediate. It allows you to register a callback which is executed later by the browser. setImmediate has no timeout parameter which means it should execute as soon as possible after flushing the browser internal queue. It does not use timers and therefore has no delay of 4 milliseconds.

Here is an updated version (see Plunker – you must use IE)

angular.module("MyApp").controller("HomeCtrl"function ($scope, $q, $timeout) {
    $scope.start = function () {
        longRunningOperation();
    }
 
    function longRunningOperation() {
        var begin = new Date();
        var progress = 0;
 
        $scope.message = "RUNNING";
 
        function step() {
            if (++progress == 250) {
                var time = new Date() - begin;
 
                $scope.$apply(function () {
                    $scope.message = time;
                });
 
                return;
            }
 
            sleep(10);
 
            $scope.message = progress;
 
            setImmediate(step);
            //setTimeout(step, 0);
        }
 
        setImmediate(step);
        //setTimeout(step, 0);
    }
});

New code achieves a time of 2504 milliseconds (much better …).

Unfortunately, only IE supports this method. For Firefox you can get similar behaviour using postMessage but Chrome is out of the game. BTW, when was the last time you encountered something useful that is only supported by IE ?

A very nice demo which demonstrates the problem and the solution can be found here

The W3C specification for setImmediate can found here

Add comment
facebook linkedin twitter email

Leave a Reply

Your email address will not be published.

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*