JavaScript性能优化:(一)优化脚本加载

JavaScript 在浏览器中的性能对于改善用户体验来说,至关重要,而这其中,首当其中的就是页面中 JavaScript 代码的加载时间。本文记述了常用的方法技巧,可以应用到实践中帮助提升 JavaScript 的加载时间。

JavaScript 代码位置

大多数情况下,浏览器遇到 <script> 标签时,会停止渲染页面,直到完成对该脚本的读取和解析。所以,我们应该尽可能地把 <script> 标签置于 </body> 之前。

另外,脚本位置与 <link> 标签位置的关系也值得我们注意:

把一段内嵌脚本放在引用外链样式表的 <link> 标签之后会导致页面阻塞去等待样式表的下载。这样做是为了确保内嵌脚本在执行时能获得最精确的样式信息。因此,建议永远不要把内嵌脚本紧跟在 <link> 标签后面。——《高性能 JavaScript》

合并 JavaScipt 文件

每个 <script> 标签初始下载时都会阻塞页面渲染,所以一般来说,我们应该进可能地减少页面中 <script> 标签的数量。

对于处理以 src 属性外链 JavaScript 文件的 <script> 标签而言,考虑到 HTTP 请求会带来额外的性能开销,因此单独下载单个 100KB 的文件将比下载 4 个 25KB 的文件更快。也就是说,我们应该尽可能地合并多个 JavaScipt 文件,以减少页面渲染所需的 HTTP 请求数,从而提高脚本加载性能。

!!!注意: HTTP2 标准的发布,将使得这一现状发生改变,对于使用了 HTTP2 的客户端和服务器端而言,同时下载多个文件将比单独下载一个合成文件性能开销更小。

压缩 JavaScript 文件

压缩 JavaScript 源文件

JavaScript 源文件越小,浏览器所用的下载时间就越少。因此,我们要想方设法地让包含源代码的 JavaScript 文件的体积尽可能地小。有三种方法可以帮助我们减小 JavaScript 源文件的体积:

  • 缩编:将 JavaScript 源文件中所有的空格和换行符移除
  • 混淆:使用短小精悍的名称对局部变量和局部函数进行重命名
  • 编译:对代码进行全面分析,并对代码中的语句进行简化、缩减、整合,生成有着相同处理行为的另一语句

好在我们无需手动去做这种重复性高效率低下的工作,有许多工具可以帮助我们很好地完成以上这些工作,下面推荐几款比较流行的:

  • JSMin
  • UglifyJS
  • YUI Compressor
  • Google Closure Compiler(大多数情况下,这个工具的压缩结果最理想)

根据这些工具压缩 JavaSript 源文件的原理,我们知道,代码中使用的全局变量或函数越小,压缩后代码的体积就越小,因此在实际开发中,我们应该尽可能地减少全局变量。

JavaScript 文件的 HTTP 压缩

当浏览器请求一个资源时,它通常会发送一个 Accept-Encoding HTTP 头(始于 HTTP/1.1)来告诉服务器它支持哪种编码转换类型。这个信息主要用于压缩文档以获得更快地下载,从而改善用户体验。

Accept-Encoding 可用的值包括:

  • gzip
  • compress
  • deflate
  • identity

如果 Web 服务器在 HTTP 请求中看到这些头信息,它就会选择最合适的编码方案,并通过 Content-Encoding HTTP 响应头通知浏览器它的决定。

Gzip 是目前最流行的编码方式,它通常能减少 70% 的下载量。目前几乎所有的服务器都支持启用这项设置,例如 Apache、Microsoft IIS、Node.js Express 等。

!!!注意: 在每个请求发生时进行即时的 Gzip 编码处理会额外消耗服务器上的资源和 CPU 处理时间。另外,Gzip 压缩主要适用于文本文件,包括 JavaScript 源文件。而二进制文件,诸如图片或 PDF 文件,则不应该使用 Gzip 压缩,因为它们本身已经被压缩过了,试图重复压缩只会浪费服务器资源。

缓存 JavaScript 文件

缓存 HTTP 组件能极大提高用户体验。缓存适用于大多数静态文件,Web 服务器通过 “Express HTTP 响应头” 来告诉客户端一个资源应该缓存多长时间。它的值是一个遵循 RFC1123 标注的绝对时间戳。例如:Express: Thu, 01 Dec 1994 16:00:00 GMT

某些浏览器,特别是移动设备上的浏览器,可能会有缓存限制。这种情况下,应该权衡 HTTP 组件数量和它们的可缓存性,考虑将它们分解成更小的块。

还有一种技术是使用 HTML5 离线应用缓存,这种不展开论述了。

静态资源更新了?

适当的缓存能切实提升用户体验,但它有一个缺点:当应用升级时,浏览器可能还是会从缓存中加载静态资源而不是重新向 Web 服务器请求更新之后的静态资源。

对于这个问题,我们可以通过给静态资源文件名附加时间戳来解决。

使用 CDN

内容分发网络(CDN)是在互联网上按地理位置分布计算机网络,它负责传递内容给终端用户。使用 CDN 的主要原因是增强 Web 应用的可靠性、可扩展性,更重要的是提升性能。事实上,通过向地理位置最近的用户传输内容,CDN 能够极大地减少网络延时。

一般说来,大型互联网公司和一些第三方 Web 服务商都可能会提供部分的免费 CDN 服务,对于 Web 世界普遍使用的一些库和框架,我们应该尽可能地使用这些免费的 CDN 服务,以提高用户的访问速度。

无阻塞加载

使用 defer 或 async 属性

HTML4 为 <script> 标签引入了 defer 属性,而 HTML5 又为 <script> 标签引入了 async 属性;我们可以通过使用这两个属性来实现异步加载脚本。

1
2
<script src="" defer></script> <!-- 延迟加载 -->
<script src="" async></script> <!-- 异步加载 -->

!!!注意: 按照 HTML5 规范,deferasync 属性只适用于外部脚本文件。

使用了 asyncdefer 属性的 <script> 的脚本均是在页面下载解析过程中异步下载,不同的是:

  • 使用了 async 属性的脚本在下载完毕后就会立刻开始执行脚本,会中断 HTML 的解析过程
  • 使用了 defer 属性的脚本会延迟到 HTML 解析完毕之后再执行

动态添加 JavaScript

我们可以动态地创建 <script> 标签并添加到 DOM 树中从而实现无阻塞地加载外部脚本文件,其优点是:脚本文件的下载和执行过程不会阻塞页面其他进程。

通常来将,把新创建的 <script> 标签添加到 <head> 标签里比添加到 <body> 标签里更保险,尤其是在页面加载过程中执行代码更是如此。当 <body> 标签中的内容没有全部加载完成时,IE可能会抛出一个“操作已中止”的错误消息。——《高性能 JavaScript》

下面是一个通用的动态添加脚本的函数:

1
2
3
4
5
6
function loadScript(url, handler) {
const script = document.createElement('script');
script.addEventListener('load', handler, false);
script.src = url;
document.head.appendChild(script);
}

XMLHttpRequest注入

通过使用 XMLHttpRequest 对象,我们也可以实现无阻塞加载外部脚本文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function xhrLoadScript(url, handler) {
const xhr = new XMLHttpRequest();
xhr.open('get', url, true);
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status >= 200 && xhr.status < 300) {
const script = document.createElement('script');
script.addEventListener('load', handler, false);
script.text = xhr.responseText;
document.body.appendChild(script);
}
}
}
}

传入合适的 URL 和回调函数,调用上面这段函数,结果就是创建一个带有内联脚本的 <script> 标签。

这种方法优点如下:

  • 可以下载 JavaScript 代码但不立即执行
  • 浏览器兼容性好

但有以下缺点:

  • 请求的 JavaScript 文件必须与当前页面出于相同的源

推荐的无阻塞加载模式

如果需要向页面中添加大量 JavaScript,推荐按如下方式加载:

  1. 先加载页面必需的最少代码和用于动态添加脚本的 loadScript() 函数
  2. 初始化页面之后使用 loadScript() 函数动态地加载剩余的 JavaScript 文件

参考

  • 《高性能JavaSript》,[美]Nicbolas C.Zakas 著,丁琛 译,电子工业出版社 2015
  • 《精通JavaSript开发》,[英]Den Odell 著,邝健威 厉海洋 译,人民邮电出版社 2015