user6269864
user6269864

Reputation:

Progress bar wouldn't repaint during heavy work, even though I've tried many methods to force repaint

I have a JavaScript method that iterates over ~200 inputs, inserts values, and triggers onchange events on them. That takes about 2000 ms in Chrome and 10000 ms in IE.

I want to make a progress bar that will show the progress.

Now, the issue is that browser wouldn't repaint the progress bar until the very end of the process, when it would immediately reach 100%.

After researching here, I've found three methods and tried all of them, but they don't work:

Here is my code:

    <div class="cg-progress-bar">
        <div class="cg-progress-bar-completed">
            &nbsp;
        </div>
        <div class="cg-inline-block cg-progress-bar-force-repaint">
            &nbsp;
        </div>
    </div>

JavaScript:

var progressBarCurrent = 0;
var progressBarTotal = 236;

$.each(sqlData, function(column, value){
        //doing some work here to update values in inputs

        //update progress bar
        progressBarCurrent++;
        if (progressBarCurrent % 22 === 0) { //don't call too often
            var percentageComplete = Math.floor( (progressBarCurrent/progressBarTotal)*100 ) + "%";    

            var bar = $(".cg-progress-bar-completed").width(percentageComplete)[0];

            //hack 1
            bar.style.display = 'none';
            bar.offsetHeight;
            bar.style.display = '';

            //hack 2
            setTimeout(function() { bar.style.display = 'block'}, 0);

            //hack 3
            $(".cg-progress-bar-completed").animate({ width: percentageComplete }, 100).height();

            //hack 4 - insert empty text node
            bar.appendChild(document.createTextNode(' '));
            $(bar).hide().show(0);
            bar.appendChild(document.createTextNode(' '));

            //hack 5 - nuclear option
            $(window).trigger("resize");
        }
    }
});

How do I force the browser to repaint?

Upvotes: 2

Views: 1800

Answers (3)

Feirell
Feirell

Reputation: 799

@K48 added it should be compatible with IE9+, so here is the solution with IE8+ support:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>IE8 compatible</title>
    <style type="text/css">
        div{
            margin: 0px;
            padding: 0px;
            height: 30px;
        }
        #wrapper{
            width: 300px;
            border: solid 1px black;
        }
        #progressBar{
            width: 0px;
            background-color: black;
            color: grey;
        }
    </style>
</head>
<body>
    <div id="wrapper"><div id="progressBar"></div></div>
    <script type="text/javascript">
        var continueExecution = false;
        var progressBar = document.getElementById( "progressBar" );
        var i = 0;
        var sqlData = new Array( 236 );
        var lastDraw = (new Date).valueOf();

        function heavyloadFunction() {
            var entry = sqlData[ i ]

            // do something with the entry
            for ( var n = 0; n < 2000; n++ ) {
                for ( var h = 0; h < sqlData.length; h++ ) {
                    var cEntry = sqlData[h]
                    cEntry = Math.random()
                }
            }

            // finish entry work

            if(i < sqlData.length){
                i++;
                continueExecution = true;
            }else{
                console.log("finished")
                continueExecution = false;
            }
        }

        function drawProgress( progress ) {
            var percentageComplete = Math.floor( ( progress.current / progress.maximum ) * 100 ) + "%";
            progressBar.style.width = percentageComplete
        }

        function shouldReDraw(){
            var dNow = (new Date).valueOf();
            if(dNow - lastDraw > 16){
                // around 60fps
                lastDraw = dNow;
                return true;
            }else{
                return false;
            }
        }

        function excutionLoop() {
            heavyloadFunction();

            if ( continueExecution ) {
                if(shouldReDraw()){
                    drawProgress({
                        current: i,
                        maximum: sqlData.length
                    })

                    window.setTimeout( excutionLoop, 0 )
                }else{
                    excutionLoop()
                }
            }else{
                drawProgress({
                    current: i,
                    maximum: sqlData.length
                })
            }           
        }

        excutionLoop();
    </script>
</body>
</html>

The idea is quite simple, you interrupt the execution every loop iteration so the redrawing can take place.

EDIT: made a little improvement which prevents that the drawing results into a bottleneck.

Upvotes: 0

Feirell
Feirell

Reputation: 799

Ok so my previous anwser didn't work as I hoped so here is a working example:

index.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>-</title>
    <style type="text/css" media="screen">
        div{
            margin: 0px;
            padding: 0px;
            height: 30px;
        }
        #wrapper{
            width: 300px;
            border: solid 1px black;
        }
        #progressBar{
            width: 0px;
            background-color: black;
            color: grey;
        }
    </style>
</head>
<body>
    <div id="wrapper"><div id="progressBar"></div></div>
    <script type="text/javascript">

        var progressBar = document.getElementById("progressBar")    
        var workerThread = new Worker('worker.js');

        function drawProgress(progress){
            var percentageComplete = Math.floor( (progress.current/progress.maximum)*100 ) + "%";
            progressBar.style.setProperty("width",percentageComplete)
        }

        workerThread.onmessage = function(e){
            drawProgress(e.data)
        }
    </script>
</body>
</html>

worker.js

var sqlData = new Array( 236 );
var i = 0;
for ( let entry of sqlData ) {
    console.log( 'iteration of each' );
    postMessage( {
        current: i,
        maximum: sqlData.length
    } )
    for ( var n = 0; n < 200000; n++ ) {
        for ( let entry of sqlData ) {
            entry = Math.random()
        }
    }
    i++;
}

I think that this could be the best option, because there is no hack involved it is just the way for this kind of task. The only problem is it is kind of hard to get JQuery in the worker. Best way there is to retrieve the SQL dataset completely in the main thread and then transmit is into the worker.

Upvotes: 1

Bardo
Bardo

Reputation: 2523

Probably your process is too heavy and is collapsing the execution thread. This means that while it's calculating the browser freezes slightly, and probably no other interaction with the UI can be made while calculations are processed.

To avoid this situations, HTML5 provides web workers technology. This is a way to emulate multitasking in browser environments, and allow you to execute heavy tasks avoiding browser freezing.

Here you have an excellent introductory article to this technology, wich I've succesfully used to create a file uploader capable to upload Gb length files maintaining a live progress bar:

https://www.html5rocks.com/en/tutorials/workers/basics/

I hope it helps.

Upvotes: 2

Related Questions