Posts Tagged ‘Long-Running Script’

浏览器根据什么来判定脚本失控?

星期六, 二月 14th, 2009

本文译自Nicholas C. Zakas于2009年1月5日在个人网站上发表的《What determines that a script is long-running?》。原文是唯一的正式版,本文是经过原文作者授权的简体中文翻译版。译者在翻译的准确性上做了大量的努力,并承诺译文的内容完全忠于原文,但可能还是包含疏漏和不妥之处,欢迎大家指正。译序和译注的内容是非正式的,仅代表译者个人观点。

译者序:在Web开发的时候,经常会遇到的一种情况就是浏览器提示脚本运行时间过长,停止还是继续,无论你选择什么,相信你都会想尽一切办法让这个对话框远离你的用户们。可你是否知道,这些不同的浏览器究竟是如何判断,哪些脚本处于“失控”状态么?本文作者,就从Internet Explorer、Firefox、Safari、Chrome和Opera五种浏览器,分析了这个情况出现的原因。

以下是对原文的翻译:

Web开发者经常遇到并必须及时处理的问题就是“提示脚本运行时间过长的提示框”(或者称为“失控脚本提示”),这些令人讨厌的对话框会在你的脚本 执行时间过长的时候出现。对于Web开发者的基本准则就是,无论什么时候,都不要让用户看到这些对话框,因为这会给人一种代码缺乏结构化的印象,更简单的说,你的代码负担太重了。

用Brendan Eich(JavaScript的发明人)的话来讲,如果JavaScript运行的时间需要用秒来计算,一定是什么地方搞错了。我个人可以忍受的上限可 能更小一些,不论什么脚本,在任何时间、任何浏览器上执行,都不应该超过100毫秒。如果实际执行的时间长于这个底限,一定要将进程分解成若干更小的代码段。

另外,其实很少有人真正意识到究竟是什么原因导致脚本在不同的浏览器中运行时间过长,连我自己都没有深究过。所以我决定坐下来好好研究一下,我们究竟会在什么情况才会看到那个讨厌的对话框。判断脚本是否失控,无外乎就两种方法。一种是根据执行了多少条语句,一种是判断脚本执行花费的时间。各个浏览器 判断脚本失控的具体方法会有略微的不同。

Internet Explorer

Internet Explorer判断一个脚本是否失控,主要通过JScript引擎执行语句的总数来判断。默认情况下,这个上限是500万条语句,这个值是可以通过注册表修改的。当你的脚本执行的语句数量超过这个限制,你就会看到下面的窗口。

IE Dialog: A script on this page is causing Internet Explorer to run slowly. If it continues to run, your compute may become unresponsive.

这个对话框提示:“这个页面上有一段脚本导致Internet Explorer运行缓慢,如果你继续运行,你的计算机可能会变为无响应状态”。要不是追求技术上的准确性,这样说确实有点过了。对话框有两个选项,要么 停止脚本执行,要么允许脚本继续运行。当这个对话框显示的时候,脚本已经被完全停止了。如果你选择继续运行脚本,就会重新计算当前执行的语句数,也就是 说,如果这个数值再次达到上限时,你会再次看到这个对话框。

Firefox

Firefox是根据脚本引擎持续执行代码的时间来判断一段脚本是否失控。默认的上限是10秒,可以通过about:config页面来修改这个值。这里需要注意的是,当弹出类似alert的模式对话框的时候,是不计时的。当浏览器执行脚本的时间达到这个上限,Firefox就会显示类似下面的对话框:

Firefox Dialog: A script on this page may be busy, or it may have stopped responding. You can stop the script now, open the script in the debugger, or let the script continue.

Firefox的对话框提示:“这个页面的一段脚本目前运行忙,或者这段脚本已经停止响应。你可以停止执行这段脚本,并在调试器中打开这段脚本,或 者保持这段脚本继续运行”。更清楚的描述了遇到的问题,并且没有IE说的那么恐怖。在这个对话框上可以执行三种操作:停止脚本执行、调试脚本或者让脚本继 续运行。和Internet Explorer一样,当运行脚本继续运行以后,对持续运行脚本时间的统计就会重置。调试脚本按钮,只有在你安装了Firebug,并在该页面激活了调试 的时候才会出现。执行调试脚本操作后,可以显示执行时间过长的代码段的具体位置。

Safari

Safari同样根据脚本引擎持续执行脚本的时间来判断,当我对Webkit的源代码进行反复研究后,发现默认的超时时间是5秒,一旦达到这个上限,就会给出下面的对话框提示:

Safari Dialog: A script on the page [url] is making Safari unresponsive. Do you want to continue running the script, or stop it?

对话框提示:“在页面url上的脚本让Safari失去响应,你是要继续运行脚本还是终止脚本”。同样的,对于用户来说,也不是什么可怕的提示。在Safari中,可以关闭失控脚本的检测功能

Chrome

Chrome在跟踪技术上有点狡猾,失控脚本检测功能似乎和tab的事故控制(crash control)关联到一起。我仔细看了源代码,却没有找到具体的限制,但基本确定的是,这个限制是以时间为基础的,估计在10秒左右(要么是5秒,要么 是10秒,总要和Safari或者Firefox看齐么)。我正在联系Chrome项目组中的朋友,看看能不能得到确定的信息。尽管如此,如果网页中存在 失控的脚本,用户还是会看到下面的对话框:

Chrome Dialog: The following page(s) have become unresponsive. You can wait for them to become responsive or kill them.

毫无疑问,Chrome的提示比起其他浏览器来说,显得都更加严重。点击“Wait”按钮,脚本会继续运行,直到达到下一个上限为止,也可以点击“Kill pages”,直接关闭该页面在内存中的所有信息,并用一个空白页取而代之。

Opera

Opera的情况比较有趣:他貌似没有针对失控脚本的相应限制。我运行了几个很长的测试,甚至花了几分钟,而在这个过程中,浏览器一直可以正常响应,这很出我的意料之外。我不是很确定,对于现在的情况来说,这个方法是好是坏,但至少它生效了,不是么?

一些建议

无论你的用户使用什么浏览器,都不应该在任何时候看到类似的提示。在你的网站或者Web应用程序作为产品发布之前,做一些常规的性能测试是非常有必要的。在这方面有很多工具可以加以利用,比如Firebug’s profiler(只支持Firefox)、YUI Profiler (支持全部浏览器)或者Internet Explorer 8’s Profiler。 你应该毫不犹豫地将那些执行时间超过100毫秒的脚本找出来,哪怕这些脚本只是在某些浏览器上运行不畅,这些脚本包含了一些需要执行很长时间的代码段,而 这些代码应该通过性能检测工具进行重新评估。确保你不是使用Chrome作为测试的底线,因为Chrome在执行JavaScript的速度上比其他浏览 器要高出一个数量级(和Firefox 3.1还有最新的WebKit Nightly相当)。最好使用Internet Explorer作为测试的底线,然后再测试其他浏览器,因为无论什么时候,IE的JavaScript引擎都是最慢的,当在IE上修复问题以后,十有八 九在其他浏览器上也可以正常运行了。

==== 本系列的所有文章 ====

如何提升JavaScript的运行速度(循环篇)

星期六, 二月 14th, 2009

本文译自Nicholas C. Zakas于2009年1月13日在个人网站上发表的《Speed up your JavaScript, Part 1》。原文是唯一的正式版,本文是经过原文作者授权的简体中文翻译版。译者在翻译的准确性上做了大量的努力,并承诺译文的内容完全忠于原文,但可能还是包含疏漏和不妥之处,欢迎大家指正。译序和译注的内容是非正式的,仅代表译者个人观点。

译者序:根据Nicholas的说法,有四种代码会拖慢脚本的运行,并最终导致脚本失控。分别是次数过多的同步循环、庞大的函数体、不恰当的递归和不合理的DOM调用。这篇着重讲第一个原因。最后给出了一个开发模式,替换传统的循环结构,可以完全避免脚本失控的状况发生。

以下是对原文的翻译:

在我上一篇帖子中,谈到了各个浏览器究竟会在什么情况下弹出脚本失控提示,对于Internet Explorer来说,当浏览器执行了数量过多的语句时就会停止执行脚本,而其他的浏览器,则是持续执行脚本超过一定时间的时候就会给出提示。而我们要探讨的核心问题,不是这些浏览器如果探测失控的脚本,而是我们如何才可以让脚本运行的更快一些,从而避免这些警告。

脚本失控基本上有以下四个方面的原因:
1. 在循环中执行了太多的操作。
2. 臃肿的函数体
3. 过多的递归
4. 过多的DOM调用

在这篇帖子中,我将会把重点放到第一条上:循环中的过多操作。循环的操作是同步进行的,所以执行一个循环所花费的时间完全取决于循环的次数。因此有两种情况会导致循环执行的时间过长,并直接导致锁定浏览器。一是循环体中包含了太多的操作,二是循环的次数过多。这两种情况都能直接导致锁定浏览器,并显示脚本失控的提示。

解决这个问题的诀窍就是用下面这两个问题来评估每个循环:
1. 这个循环必须要同步执行么?
2. 循环里面的数据,必须要按顺序执行么?

如果两个问题的答案都是否定的话,你就可以选择将循环里的操作进行分解。关键是要根据代码的具体环境确定上面两个问题的答案。一个典型的循环可能像下面这个样子:

/*
for (var i = 0; i < items.length; i++) {
    process(items[i]);
}
*/

乍一看这个循环并没有太大的问题,是不是会运行很长时间完全取决于循环的次数。如果紧接循环后没有其他代码在执行的时候需要依赖于循环的结果,那么对于第一个问题的答案就是“不”。你还可以发现,循环每次只处理一个数值,而且不依赖于上一次循环的结果,所以对于第二个问题的答案同样也是否定的。这就意味着,循环可以通过某种方式进行拆解,不会导致锁定浏览器而显示脚本失控的提示。

在《Professional JavaScript, Second Edition》这本书中,对于那些执行次数非常巨大的虚幻,我推荐使用下面的方式来处理:

/*
function chunk(array, process, context) {
    setTimeout(function() {
        var item = array.shift();
        process.call(context, item);

        if (array.length > 0) {
            setTimeout(arguments.callee, 100);
        }
    },
    100);
}
*/

chunk()函数的用途就是将一个数组分成小块处理(这也是名字的由来),我们可以传递三个参数。要处理的数组对象、处理函数以及一个可选的上下文变量,用于设置process()函数中对应的this对象。第一个timer用于处理操作之间的延时(这里设置为100毫秒,大家可以根据实际需要自行修改)。每次执行这个函数,都会将数组中的第一个对象取出,并传给process()函数进行操作,如果这时process()中还有未处理完的对象,另外一个timer就会启动,用于重复等待。上面提到的循环,可以通过下面的方法使用这个函数:

/*
chunk(items, process);
*/

需要注意的是,在这里数组采用了队列(queue)的形式,而且在循环的过程中,每次都会发生修改。如果你要修改数组的原始状态,这里介绍两种途径:一种是通过concat()函数,在传递之前,建立一个当前数组的副本:

/*
chunk(items.concat(), process);
*/

另外一种选择是直接修改chunk()函数,直接在函数内部进行修改:

/*
function chunk(array, process, context) {
    var items = array.concat(); //clone the array
    setTimeout(function() {
        var item = items.shift();
        process.call(context, item);

        if (items.length > 0) {
            setTimeout(arguments.callee, 100);
        }
    },
    100);
}
*/

注意这种方法要比只保存一个索引安全的多,因为数组的内容在下次计时器生效之前可能会发生变化。

这里提到的chunk()函数,只是优化循环性能的一个起点。你可以根据需要不断改进它,让它拥有更多的功能。比如说,在数组中所有对象都处理完成以后,可以增加一个函数回调。无论你是否会按照这种方式对函数进行修改,这只是一种JavaScript的代码开发模式,可以帮助优化数组的处理性能,还可以避免那个脚本失控的警告。

==== 本系列的所有文章 ====