2019独角兽企业重金招聘Python工程师标准>>>
一、JavaScript 的性能优化:加载和执行
随着 Web2.0 技术的不断推广,越来越多的应用使用 JavaScript 技术在客户端进行处理,从而使 JavaScript 在浏览器中的性能成为开发者所面临的最重要的可用性问题。而这个问题又因 JavaScript 的阻塞特性变的复杂,也就是说当浏览器在执行 JavaScript 代码时,不能同时做其他任何事情。本文详细介绍了如何正确的加载和执行 JavaScript 代码,从而提高其在浏览器中的性能。
概览
无论当前 JavaScript 代码是内嵌还是在外链文件中,页面的下载和渲染都必须停下来等待脚本执行完成。JavaScript 执行过程耗时越久,浏览器等待响应用户输入的时间就越长。浏览器在下载和执行脚本时出现阻塞的原因在于,脚本可能会改变页面或 JavaScript 的命名空间,它们对后面页面内容造成影响。一个典型的例子就是在页面中使用document.write()
。例如清单 1
清单 1 JavaScript 代码内嵌示例
Source Example
当浏览器遇到标签时,当前
HTML 页面无从获知
JavaScript 是否会向
标签添加内容,或引入其他元素,或甚至移除该标签。因此,这时浏览器会停止处理页面,先执行
JavaScript代码,然后再继续解析和渲染页面。同样的情况也发生在使用
src
属性加载
JavaScript的过程中,浏览器必须先花时间下载外链文件中的代码,然后解析并执行它。在这个过程中,页面渲染和用户交互完全被阻塞了。
脚本位置
HTML 4 规范指出 标签可以放在 HTML 文档的
或
中,并允许出现多次。Web 开发人员一般习惯在
中加载外链的 JavaScript,接着用
标签用来加载外链的 CSS 文件或者其他页面信息。例如清单 2
清单 2 低效率脚本位置示例
Source Example Hello world!
然而这种常规的做法却隐藏着严重的性能问题。在清单 2 的示例中,当浏览器解析到 标签(第 4 行)时,浏览器会停止解析其后的内容,而优先下载脚本文件,并执行其中的代码,这意味着,其后的 styles.css 样式文件和
标签都无法被加载,由于
标签无法被加载,那么页面自然就无法渲染了。因此在该 JavaScript 代码完全执行完之前,页面都是一片空白。图 1 描述了页面加载过程中脚本和样式文件的下载过程。
图 1 JavaScript 文件的加载和执行阻塞其他文件的下载
我们可以发现一个有趣的现象:第一个 JavaScript 文件开始下载,与此同时阻塞了页面其他文件的下载。此外,从 script1.js 下载完成到 script2.js 开始下载前存在一个延时,这段时间正好是 script1.js 文件的执行过程。每个文件必须等到前一个文件下载并执行完成才会开始下载。在这些文件逐个下载过程中,用户看到的是一片空白的页面。
从 IE 8、Firefox 3.5、Safari 4 和 Chrome 2 开始都允许并行下载 JavaScript 文件。这是个好消息,因为标签在下载外部资源时不会阻塞其他
标签。遗憾的是,JavaScript 下载过程仍然会阻塞其他资源的下载,比如样式文件和图片。尽管脚本的下载过程不会互相影响,但页面仍然必须等待所有 JavaScript 代码下载并执行完成才能继续。因此,尽管最新的浏览器通过允许并行下载提高了性能,但问题尚未完全解决,脚本阻塞仍然是一个问题。
由于脚本会阻塞页面其他资源的下载,因此推荐将所有标签尽可能放到
标签的底部,以尽量减少对整个页面下载的影响。例如清单 3
清单 3 推荐的代码放置位置示例
Source Example Hello world!
这段代码展示了在 HTML 文档中放置标签的推荐位置。尽管脚本下载会阻塞另一个脚本,但是页面的大部分内容都已经下载完成并显示给了用户,因此页面下载不会显得太慢。这是优化 JavaScript 的首要规则:将脚本放在底部。
组织脚本
由于每个标签初始下载时都会阻塞页面渲染,所以减少页面包含的
标签数量有助于改善这一情况。这不仅针对外链脚本,内嵌脚本的数量同样也要限制。浏览器在解析 HTML 页面的过程中每遇到一个
标签,都会因执行脚本而导致一定的延时,因此最小化延迟时间将会明显改善页面的总体性能。
这个问题在处理外链 JavaScript 文件时略有不同。考虑到 HTTP 请求会带来额外的性能开销,因此下载单个 100Kb 的文件将比下载 5 个 20Kb 的文件更快。也就是说,减少页面中外链脚本的数量将会改善性能。
通常一个大型网站或应用需要依赖数个 JavaScript 文件。您可以把多个文件合并成一个,这样只需要引用一个标签,就可以减少性能消耗。文件合并的工作可通过离线的打包工具或者一些实时的在线服务来实现。
需要特别提醒的是,把一段内嵌脚本放在引用外链样式表的之后会导致页面阻塞去等待样式表的下载。这样做是为了确保内嵌脚本在执行时能获得最精确的样式信息。因此,建议不要把内嵌脚本紧跟在
标签后面。
无阻塞的脚本
减少 JavaScript 文件大小并限制 HTTP 请求数在功能丰富的 Web 应用或大型网站上并不总是可行。Web 应用的功能越丰富,所需要的 JavaScript 代码就越多,尽管下载单个较大的 JavaScript 文件只产生一次 HTTP 请求,却会锁死浏览器的一大段时间。为避免这种情况,需要通过一些特定的技术向页面中逐步加载 JavaScript 文件,这样做在某种程度上来说不会阻塞浏览器。
无阻塞脚本的秘诀在于,在页面加载完成后才加载 JavaScript 代码。这就意味着在 window
对象的 onload
事件触发后再下载脚本。有多种方式可以实现这一效果。
延迟加载脚本
HTML 4 为标签定义了一个扩展属性:
defer
。Defer
属性指明本元素所含的脚本不会修改 DOM,因此代码能安全地延迟执行。defer
属性只被 IE 4 和 Firefox 3.5 更高版本的浏览器所支持,所以它不是一个理想的跨浏览器解决方案。在其他浏览器中,defer
属性会被直接忽略,因此标签会以默认的方式处理,也就是说会造成阻塞。然而,如果您的目标浏览器支持的话,这仍然是个有用的解决方案。清单 4 是一个例子
清单 4 defer 属性使用方法示例
带有 defer
属性的标签可以放置在文档的任何位置。对应的 JavaScript 文件将在页面解析到
标签时开始下载,但不会执行,直到 DOM 加载完成,即
onload
事件触发前才会被执行。当一个带有 defer
属性的 JavaScript 文件下载时,它不会阻塞浏览器的其他进程,因此这类文件可以与其他资源文件一起并行下载。
任何带有 defer
属性的元素在 DOM 完成加载之前都不会被执行,无论内嵌或者是外链脚本都是如此。清单 5 的例子展示了
defer
属性如何影响脚本行为:
清单 5 defer 属性对脚本行为的影响
Script Defer Example
这段代码在页面处理过程中弹出三次对话框。不支持 defer
属性的浏览器的弹出顺序是:“defer”、“script”、“load”。而在支持 defer
属性的浏览器上,弹出的顺序则是:“script”、“defer”、“load”。请注意,带有 defer
属性的元素不是跟在第二个后面执行,而是在
onload
事件被触发前被调用。
如果您的目标浏览器只包括 Internet Explorer 和 Firefox 3.5,那么 defer
脚本确实有用。如果您需要支持跨领域的多种浏览器,那么还有更一致的实现方式。
HTML 5 为标签定义了一个新的扩展属性:
async
。它的作用和 defer
一样,能够异步地加载和执行脚本,不因为加载脚本而阻塞页面的加载。但是有一点需要注意,在有 async
的情况下,JavaScript 脚本一旦下载好了就会执行,所以很有可能不是按照原本的顺序来执行的。如果 JavaScript 脚本前后有依赖性,使用 async
就很有可能出现错误。
动态脚本元素
文档对象模型(DOM)允许您使用 JavaScript 动态创建 HTML 的几乎全部文档内容。元素与页面其他元素一样,可以非常容易地通过标准 DOM 函数创建:
清单 6 通过标准 DOM 函数创建
代码首先创建 SCRIPT 标记并插入到 HEAD 标记中,再将 SCRIPT 的 src 属性指向外部 JS 文件,由之后的 SCRIPT 标记中代码调用外部程序全局变量。
各浏览器表现及分析如下:
IE Firefox Opera | 弹出($)函数体。此方法中动态附加进文档的 js 文件会阻断下一个 SCRIPT 标记内的代码解析,直至它全部解析完。 |
---|---|
Chrome Safari | 脚本出错,"$ is not defined"。此方法中动态附加进文档的 js 文件不会阻断下一个 SCRIPT 标记内的代码解析。 |
情况2:
代码首先创建 SCRIPT 标记,将 src 属性指向外部 JS 文件。最后插入到 HEAD 标记中,由之后的 SCRIPT 标记中代码调用外部程序全局变量。
各浏览器表现及分析如下:
Firefox Opera | 弹出($)函数体。此方法中动态附加进文档的 js 文件会阻断下一个 SCRIPT 标记内的代码解析,直至它全部解析完。 |
---|---|
IE Chrome Safari | 脚本出错,"$ is not defined"。此方法中动态附加进文档的 js 文件不会阻断下一个 SCRIPT 标记内的代码解析。 |
情况3:
代码首先创建 SCRIPT 标记,将 src 属性指向外部 JS 文件。最后插入到 HEAD 标记中,其后代码立即调用外部程序全局变量。
各浏览器表现及分析如下:
所有浏览器 | 脚本出错,"$ is not defined"。此方法中动态附加进文档的 js 文件不会阻断同一个 SCRIPT 标记内的代码解析。 |
---|
情况4:
代码获取某个 SCRIPT 标记的引用,在将其 src 属性指向外部 JS 文件,其后代码立即调用外部程序全局变量。
各浏览器表现及分析如下:
所有浏览器 | 脚本出错,"$ is not defined"。此方法中动态附加进文档的 js 文件不会阻断同一个 SCRIPT 标记内的代码解析。 |
---|
情况5:
代码获取某个 SCRIPT 标记的引用,在将其 src 属性值变更为外部 JS 文件,由之后的 SCRIPT 标记中代码调用外部程序全局变量。
各浏览器表现及分析如下:
IE Firefox Opera | 弹出($)函数体。此方法中动态附加进文档的 js 文件会阻断下一个 SCRIPT 标记内的代码解析,直至它全部解析完。 |
---|---|
Chrome Safari | 脚本出错,"$ is not defined"。此方法中动态附加进文档的 js 文件不会阻断下一个 SCRIPT 标记内的代码解析。 |
综合以上情况,对于动态插入的 SCRIPT 文件,使用不同的插入方法将有不同的表现,不能保证在各浏览器能阻塞其后脚本的执行。
解决方案
对于必须动态附加到文档的外部 js 文件,要保证动态引入的脚本全部执行完成后,才能执行后续代码。
可以将此部分代码封装后调用,如:
function loadScript(url, callback){
var script = document.createElement ("script")
script.type = "text/javascript";
if (script.readyState){ //IE
script.onreadystatechange = function(){
if (script.readyState == "loaded" || script.readyState == "complete"){
script.onreadystatechange = null;
callback();
}
};
} else { //Others
script.onload = function(){
callback();
};
}
script.src = url;
document.getElementsByTagName("head")[0].appendChild(script);
参考链接
http://www.w3help.org/zh-cn/causes/BX9013#
http://www.ibm.com/developerworks/cn/web/1308_caiys_jsload/