「javaScript性能优化」js文件的加载与执行

1,853 阅读5分钟

前言


本文将会从js文件加载和执行的角度,给出一些js的性能优化方案.

其中一些方案可能是老生常谈或者是已经作为隐形标准来使用.我们会从原理层面结合代码,对其进行讨论和验证.

概念


javaScript的阻塞特性\color{red}javaScript的阻塞特性

当浏览器在执行JavaScript代码时,不能同时做其他任何事情.

事实上,大多数浏览器以单一进程处理UI刷新JavaScript脚本执行,单一进程就意味着浏览器同一时间只能做一件事情,要么UI刷新,要么执行JavaScript.所以js脚本执行的时间越长,页面的响应时间也就越长.

这意味着在实际的Html解析过程中,<script>标签每次出现都会让霸道的让页面停止解析接下来的内容.无论这个<script>标签是内嵌的还是外联的,他都会阻止页面的其他动作.

有些同学可能会问了,为什么遇到<script>标签后页面一定要等待它执行完成呢?不能继续进行页面解析吗?

这个问题的答案也非常简单.你无法直接的判断脚本执行过程中是否会修改页面,而页面一旦被修改,且在脚本执行完成之前你又渲染了页面的其他部分.这可能将会出现巨大的冲突问题.

所以,浏览器会停止处理页面,先下载(如果需要下载的话)和执行JavaScript代码,然后再继续解析和渲染页面.

这,就是JavaScript的阻塞特性.

优化方向


脚本位置SrciptPosition\color{red}{脚本位置Srcipt Position}

HTML4规范指出<script>标签可以存放在<head>标签和<body>标签中》

既然规范明确指出了<script的存放位置,那么这两个位置在一定环境下一定有着性能上的区别.先来上一段代码.

<!DOCTYPE html>
<html lang="zh">
<head>
  <title>测试页</title
  <script type="text/javascript" src="摸猫猫.js"></script>
</head>
<body>
<div class="page" id="root">
  <p>阿巴阿巴阿巴</p>
  <p>恶魔妹妹摸猫猫💗</p>
</div>
</body>
</html>

这是一段把<script>标签放在<head>中的代码.但这段看似正常的代码中隐藏着十分严重的性能问题.

首先我们要明确一点:

浏览器的解析顺序是从上至下,从左到右.在解析到<body>标签之前,不会渲染页面的任何部分

那么根据这段代码,一旦摸猫猫.js的下载或执行时间过长(比如这个js非常大亦或者它内部有一个一个处理时间很长的复杂逻辑),这个页面将进入一个长时间“当机”的状态.通常表现为显示空白页面.用户无法看到页面内容,也无法进行页面交互.

在真实的项目情况中,需要加载的<script>标签可能远不止一个.尽管现在的浏览器基本都已经支持并行下载javaScript文件,脚本之间的下载过程也的确不会相互影响,但是页面依然需要在所有的js文件下载并执行完成后才能继续.所以阻塞依旧存在.

所以我们将代码改成如下:

<!DOCTYPE html>
<html lang="zh">
<head>
  <title>测试页</title
</head>
<body>
<div class="page" id="root">
  <p>阿巴阿巴阿巴</p>
  <p>恶魔妹妹摸猫猫💗</p>
</div>
<script type="text/javascript" src="摸猫猫.js"></script>
</body>
</html>

我们将<script>的标签放在了</body>之前.这是一个推荐的存放位置.尽管脚本依然会互相阻塞,但是页面的大部分初始内容已经展示给了用户,这会让页面不会显得太慢.

所以,为了性能,将脚本放在底部

合并脚本GroupScripts\color{red}合并脚本GroupScripts

首先我们再来复习一下浏览器遇上<script>标签后的行为是怎么样的.

解析到<script> => 下载(如果需要的话) => 执行 => 解析到<script> => ......

所以显而易见的,多个<script>标签会严重的性能问题,尽管现代浏览器大多都支持了并行下载,但并行下载的量不是无限的.实践过程中依然会存在这个问题.

考虑到HTTP请求会带来额外的性能开销,因此下载单个的400kb的文件会比4个100kb的文件更快,也就是说,减少页面中脚本文件的数量将会改善性能.

合并<script>外链的方法有不少,这里就不再赘述了.

无阻塞脚本NonBlockingScript\color{red}无阻塞脚本NonBlocking Script

合理规避阻塞特性

浏览器单线程决定了JavaScript的阻塞特性,这个过程中阻塞的可能是UI刷新也可能是下一段Js代码的执行.总之这是开发者们所面临的最日常的性能问题.控制脚本位置合并脚本只是创造高性能web应用的第一步.

因为对于web应用来说,越复杂的页面功能所需要的script代码就越多,精简源代码并不是总是可行的.尽管下载一次较大的javascript文件只产生一次http开销,却会锁死浏览器一大段时间,为了避免这种情况.我们在实际项目开发中经常需要逐步加载javascript文件,这样做在某种情境下并不会阻塞浏览器.

无阻塞脚本的关键在于,在页面加载完成后才执行JavaScript代码/下载完成后立刻执行.这意味着他们的下载并不会组合其他资源的解析.

当然值得一提的是,所谓的阻塞在现代浏览器里并不是完全阻塞的.为了针对早期浏览器中javaScript尴尬的串型加载行为,现代浏览器优化了这一特性,及preloader策略.简单的介绍下:

什么叫preloader呢在早期浏览器,script资源是阻塞加载的,当页面遇到一个script,那么要等这个script下载和执行完了,才会继续解析剩下的DOM结构,也就是说script是串行加载的,并且会堵塞页面其它资源的加载,这样会导致页面整体的加载速度很慢,所以早在2008年的时候浏览器出了一个推测加载(speculative preload)策略,即遇到script的时候,DOM会停止构建,但是会继续去搜索页面需要加载的资源,如看下后续的html有没有img/script标签,先进行预加载,而不用等到构建DOM的时候才去加载。这样大大提高了页面整体的加载速度。

所以综上所述,preload策略+无阻塞脚本能很好的解决javascript的阻塞问题,那么如何创建一个无阻塞脚本呢?

延迟/异步脚本defer/asyncScript\color{red}延迟/异步 脚本 defer/async Script

HTML4和HTML5规范中分别新引入了<script>标签的扩展属性deferasync

下面举个使用的例子:

  <script async type="text/javascript" src="摸猫猫.js"></script>
  <script defer type="text/javascript" src="摸猫猫.js"></script>

deferasync<script>标签可以放在文档的任何位置,对应的javaScript文件将在浏览器解析到这个<script>标签时开始下载.当一个带deferasync<script>标签开始下载时,他不会阻塞浏览器的任何其他进程,因此这类文件可以和其他资源并行下载.

deferasync的区别在于.带defer<script>标签会在等待dom加载完成后被触发,而async则是下载完后立刻执行.

那么这里有个新的疑问,既然有了强大的defer/async以及十分人性化的preload策略.是否我们我们就可以抛弃第一条优化方案:将脚本放在底部了呢?

答案是:在不考虑兼容性的情况下,确实可以替代.

所以说抛开兼容性谈性能都是耍流氓😅

async兼容性

async

defer兼容性

defer

动态脚本元素DynamicScriptElements\color{red}动态脚本元素 Dynamic Script Elements

众所周知,script标签是个元素,而js又有操作DOM的功能,那么显而易见的,js可以很容易的在DOM中新增,移动,或者是删除一个<script>标签.就像这样:

var script = document.createElement('script')
script.type = 'text/javascript'
script.src = '摸猫猫.js'
// 支持H5的浏览器
document.head.appendChild(script)
// 通用写法
document.getElementsByTagName('head')[0].appendChild(script)

新创建的<script>标签加载了摸猫猫.js.这个文件将会在该元素被添加到页面时开始就下载. 这种技术的重点在于,无论何时启动下载,文件的下载和执行过程都不会阻塞页面的其他进程.

使用这种技术下载文件时,返回的代码通常会立刻执行.但是当这个js中包含供页面其他接口需要跳用的接口时,就会出现问题.在这种情况下,我们必须要保证这个js加载完成才能执行.

var script = document.createElement('script')
script.type = 'text/javascript'
script.onload = () => {
    alert('Okkk')
}
script.src = '摸猫猫.js'
// 支持H5的浏览器
document.head.appendChild(script)
// 通用写法
document.getElementsByTagName('head')[0].appendChild(script)

写到这想必大家都会觉得眼熟...这它喵的不就是JSONP吗.你说的没错,它还真就是JSONP的基础原理....[笑]

XMLHTTPRequest脚本注入\color{red}XMLHTTPRequest 脚本注入

其实就是用XHR对象获取脚本并注入页面中.XHR就不再赘述了.

以发送请求的方式获取js文件有个好处.你可以加载javaScript文件但是不立刻执行,你可以根据条件判断精准的控制js的执行时间.同时这个方法兼容性良好.

而制约XHR的缺点就在于请求的javaScript文件必须和页面文件处于相同的域,不然就会出现跨域的问题了.

减少加载时间\color{red}减少加载时间

高性能浏览器的功能

DNS预解析DNSPrefetch\color{red}DNS预解析 DNS Prefetch

DNS Prefetch 是一种DNS 预解析技术,当你浏览网页时,浏览器会在加载网页时对网页中的域名进行解析缓存,这样在你单击当前网页中的连接时就无需进行DNS的解析,减少用户等待时间,提高用户体验。

目前支持 DNS Prefetch 的浏览器有 google chrome 和 firefox 3.5

如果要控制浏览器端是否对域名进行预解析,可以通过Http header 的x-dns-prefetch-control 属性进行控制

<meta http-equiv="x-dns-prefetch-control" content="on" />
<link rel="dns-prefetch" href="http://bdimg.share.baidu.com" />

preload\color{red}preload

preload 是一个声明式 fetch,可以强制浏览器在不阻塞 document 的 onload 事件的情况下请求资源。 当资源被 preload 后,会从网络堆栈传输到 HTTP 缓存并进入渲染器的内存缓存。 如果资源可以被缓存(例如,存在有效的 cache-control 和 max-age),它将存储在 HTTP 缓存中,可用于当前和未来的会话。 如果资源不可缓存,则不会将其存储在 HTTP 缓存中。 相反,它会被缓存到内存缓存中并保持不变直到它被使用.

不要和高性能浏览器自带的preloader策略混为一谈

浏览器会在遇到如下link标签时,立刻开始下载main.js(不阻塞parser),并放在内存中,但不会执行其中的JS语句。 只有当遇到script标签加载的也是main.js的时候,浏览器才会直接将预先加载的JS执行掉。

<link rel="preload" href="/main.js" as="script">

prefetch\color{red}prefetch

浏览器会在空闲的时候,下载main.js, 并缓存到disk。当有页面使用的时候,直接从disk缓存中读取。其实就是把决定是否和什么时间加载这个资源的决定权交给浏览器。

如果prefetch还没下载完之前,浏览器发现script标签也引用了同样的资源,浏览器会再次发起请求,这样会严重影响性能的,加载了两次...所以不要在当前页面马上就要用的资源上用prefetch,要用preload。

<link href="main.js" rel="prefetch">

总结


  • <script>放在</body>标签闭合之前

  • 合并<script>数量,减少请求数

  • 合理的使用动态脚本注入

    • 在兼容性允许的情况下使用 defer和async来代替策略一
    • 动态创建<script>标签来下载并执行代码
    • 在条件允许的情况下使用XHR对象来动态创建<script> 标签
  • 以合理的预加载方式来减少js加载时间

    • DNS prefetch,使用前注意兼容性问题,其次,大量使用DNSprefetch会增加DNS解析次数.最后,chrome和firefox3.5+已经做过优化,会自动预解析cdn

    • 使用对立刻需要使用的资源进行preload,这样能保证在文档解析完成的第一时间执行它.

    • prefetch 同上,同时也不要忽视其本身带来的问题.

觉得文章对你有用的话请点个赞👍吧~这就是对作者最大的鼓励