在 Javascript 中进行长时间运行的计算时如何避免冻结浏览器

IT技术 javascript performance
2021-01-17 10:20:52

我有一个网页,其中函数中的 javascript 计算需要大量时间才能完成并使页面冻结。我应该使用什么技术来确保在后台进行计算时 javascript 不会冻结浏览器?

5个回答

如果你只需要做一个计算,在长时间运行的计算中不需要访问 DOM,那么你有两个选择:

  1. 您可以将计算分解为多个部分,并在setTimeout(). 在每次setTimeout()调用时,浏览器都可以自由地为其他事件提供服务,并使页面保持活动和响应。完成最后一部分计算后,即可执行结果。
  2. 您可以在现代浏览器中使用网络工作者在后台运行计算。在 webworker 中完成计算后,它会向主线程发送一条消息,然后您可以使用结果更新 DOM。

这是一个相关的答案,也显示了一个示例:Best way to iterate over an array without blocks the UI

添加了对另一个相关答案的参考,其中包含一些使用setTimeout().
2021-03-22 10:20:52
接受。今天我学到了一些关于网络工作者和使用 setTimeout() 的新知识。
2021-04-06 10:20:52

让我通过给出一个具体的精简示例来详细说明@jfriend00 的回答。这是一个可以通过单击按钮启动的长时间运行的 JavaScript 进程。一旦运行,它就会冻结浏览器。该过程由一个长循环组成,该循环重复一些工作负载,其中一次迭代花费的时间相对较少。

由于浏览器冻结,调试这样的脚本并不容易。避免浏览器冻结的一种替代方法是使用网络工作者。这种方法的缺点是 Web Workers 本身的可调试性很差:不支持 Firebug 之类的工具。

<html>
<head>
    <script>
        var Process = function(start) {
            this.start = start;
        }

        Process.prototype.run = function(stop) {
            // Long-running loop
            for (var i = this.start; i < stop; i++) {
                // Inside the loop there is some workload which 
                // is the code that is to be debugged
                console.log(i);
            }
        }

        var p = new Process(100);

        window.onload = function() {
            document.getElementById("start").onclick = function() {
                p.run(1000000000);
            }
        }
    </script>
</head>
<body>
    <input id="start" type="button" value="Start" />
</body>
</html>

使用队列数据结构(例如http://code.stephenmorley.org/javascript/queues/)、间隔计时器和对原始进程的控制流的一些小的修改可以构建一个不会冻结浏览器的 GUI , 使过程完全可调试,甚至允许其他功能,如步进、暂停和停止。

这是它的过程:

<html>
<head>
    <script src="http://code.stephenmorley.org/javascript/queues/Queue.js"></script>
    <script>
        // The GUI controlling process execution
        var Gui = function(start) {
            this.timer = null; // timer to check for inputs and/or commands for the process
            this.carryOn = false; // used to start/pause/stop process execution
            this.cmdQueue = new Queue(); // data structure that holds the commands 
            this.p = null; // process instance
            this.start = start;
            this.i = start; // input to the modified process 
        }

        Gui.prototype = {
            /**
             * Receives a command and initiates the corresponding action 
             */
            executeCmd: function(cmd) {
                switch (cmd.action) {
                    case "initialize":
                        this.p = new Process(this);
                        break;
                    case "process":
                        this.p.run(cmd.i);
                        break;
                }
            },

            /*
             * Places next command into the command queue
             */
            nextInput: function() {
                this.cmdQueue.enqueue({
                    action: "process",
                    i: this.i++
                });
            }
        }

        // The modified loop-like process
        var Process = function(gui) {
            this.gui = gui;
        }

        Process.prototype.run = function(i) {
            // The workload from the original process above
            console.log(i);

            // The loop itself is controlled by the GUI
            if (this.gui.carryOn) {
                this.gui.nextInput();
            }
        }

        // Event handlers for GUI interaction
        window.onload = function() {

            var gui = new Gui(100);

            document.getElementById("init").onclick = function() {
                gui.cmdQueue.enqueue({ // first command will instantiate the process
                    action: "initialize"
                });

                // Periodically check the command queue for commands
                gui.timer = setInterval(function() {
                    if (gui.cmdQueue.peek() !== undefined) {
                        gui.executeCmd(gui.cmdQueue.dequeue());
                    }
                }, 4);
            }

            document.getElementById("step").onclick = function() {
                gui.carryOn = false; // execute just one step
                gui.nextInput();
            }

            document.getElementById("run").onclick = function() {
                gui.carryOn = true; // (restart) and execute until further notice
                gui.nextInput();
            }

            document.getElementById("pause").onclick = function() {
                gui.carryOn = false; // pause execution
            }

            document.getElementById("stop").onclick = function() {
                gui.carryOn = false; // stop execution and clean up 
                gui.i = gui.start;
                clearInterval(gui.timer)

                while (gui.cmdQueue.peek()) {
                    gui.cmdQueue.dequeue();
                }
            }
        }
    </script>
</head>
<body>
    <input id="init" type="button" value="Init" />
    <input id="step" type="button" value="Step" />
    <input id="run" type="button" value="Run" />
    <input id="pause" type="button" value="Pause" />
    <input id="stop" type="button" value="Stop" />
</body>
</html>

虽然这种方法肯定不适合您能想到的所有长时间运行的脚本,但它当然可以适应任何类似循环的场景。我正在使用它来将Numenta 的 HTM/CLA人工智能算法移植到浏览器。

有些浏览器只有一个线程来运行你的代码和更新 UI(换句话说,在计算完成之前,浏览器会出现“冻结”)。您会想要尝试以一种或另一种方式异步执行操作。

如果计算真的很昂贵,您可能希望调用服务器并让服务器进行计算,并在计算完成后回调客户端。

如果计算有点昂贵,您可以尝试在客户端上分块进行。这实际上不是异步的(因为客户端在执行每个块阻塞),但目标是使块足够小,使阻塞不明显。

这个答案来自 2012 年,但现在情况好多了;Chrome、Safari、Firefox、IE/Edge 等现在支持web 工作者
2021-03-28 10:20:52
您能否提供一个浏览器的具体示例,该浏览器具有用于运行代码和更新 UI 的单独线程?
2021-04-02 10:20:52
 setTimeout(function() { ..code  }, 0);

我推荐这个用于繁重的执行时间,以及加载 ajax 你可以尝试添加

$(window).on("load", function (e) { }); // for jquery v3

如果它在加载过程中。

我们正在尝试计算某种长时间运行的操作。这只会推迟长期运行的操作。它仍然会执行并冻结浏览器
2021-03-20 10:20:52

我认为这应该可以解决您的问题,

function myClickOperation(){
    var btn_savebutton2 = document.querySelector("input[id*='savebutton2']");
    setTimeout(function () { btn_savebutton2.click() }, 1000);
}

// 完整的 HTML 内容

<html>
<script>
    function myClickOperation(){
        var btn_savebutton2 = document.querySelector("input[id*='savebutton2']");
        document.getElementById('savebutton1').disabled = true;
        setTimeout(function () { btn_savebutton2.click() }, 1000);
    }
    function testClick(){
        var idd = document.getElementById("myid");
        idd.innerHTML =idd.innerHTML +"<br/>" + new Date();
        if(true){
            setTimeout(function () { testClick() }, 1);
        }
    }

</script>
<body>
    <input type="button" id="savebutton1" onclick="myClickOperation()" value="Click me" />
    <input type="button" id="savebutton2" onclick="testClick()" value="Do not click this" />
    <input type="text"/>

    <input type="button" value="temp"/>
    <div style="height: 300px;overflow-y: scroll;" id="myid"/>
</body>

避免循环语句并使用 setTimeout 它不会冻结。例如 for(1 to 10){}, 你可以使用 var x =function(i){ console.log(i);if(i<10) setTimeout(x,1,++i);}, x(1 );
2021-03-28 10:20:52
我们正在尝试计算某种长时间运行的操作。这只会推迟长期运行的操作。它仍然会执行并冻结浏览器
2021-04-01 10:20:52