前言
本文将会从js文件加载和执行的角度,给出一些js的性能优化方案.
其中一些方案可能是老生常谈或者是已经作为隐形标准来使用.我们会从原理层面结合代码,对其进行讨论和验证.
概念
当浏览器在执行JavaScript代码时,不能同时做其他任何事情.
事实上,大多数浏览器以单一进程处理UI刷新和JavaScript脚本执行,单一进程就意味着浏览器同一时间只能做一件事情,要么UI刷新,要么执行JavaScript.所以js脚本执行的时间越长,页面的响应时间也就越长.
这意味着在实际的Html解析过程中,<script>
标签每次出现都会让霸道的让页面停止解析接下来的内容.无论这个<script>
标签是内嵌的还是外联的,他都会阻止页面的其他动作.
有些同学可能会问了,为什么遇到<script>
标签后页面一定要等待它执行完成呢?不能继续进行页面解析吗?
这个问题的答案也非常简单.你无法直接的判断脚本执行过程中是否会修改页面,而页面一旦被修改,且在脚本执行完成之前你又渲染了页面的其他部分.这可能将会出现巨大的冲突问题.
所以,浏览器会停止处理页面,先下载(如果需要下载的话)和执行JavaScript代码,然后再继续解析和渲染页面.
这,就是JavaScript的阻塞特性.
优化方向
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>
之前.这是一个推荐的存放位置.尽管脚本依然会互相阻塞,但是页面的大部分初始内容已经展示给了用户,这会让页面不会显得太慢.
所以,为了性能,将脚本放在底部
首先我们再来复习一下浏览器遇上<script>
标签后的行为是怎么样的.
解析到
<script>
=> 下载(如果需要的话) => 执行 => 解析到<script>
=> ......
所以显而易见的,多个<script>
标签会严重的性能问题,尽管现代浏览器大多都支持了并行下载,但并行下载的量不是无限的.实践过程中依然会存在这个问题.
考虑到HTTP请求会带来额外的性能开销,因此下载单个的400kb的文件会比4个100kb的文件更快,也就是说,减少页面中脚本文件的数量将会改善性能.
合并<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的阻塞问题,那么如何创建一个无阻塞脚本呢?
HTML4和HTML5规范中分别新引入了<script>
标签的扩展属性defer
和async
下面举个使用的例子:
<script async type="text/javascript" src="摸猫猫.js"></script>
<script defer type="text/javascript" src="摸猫猫.js"></script>
带defer
和async
的<script>
标签可以放在文档的任何位置,对应的javaScript文件将在浏览器解析到这个<script>
标签时开始下载.当一个带defer
或async
的<script>
标签开始下载时,他不会阻塞浏览器的任何其他进程,因此这类文件可以和其他资源并行下载.
defer
和async
的区别在于.带defer
的<script>
标签会在等待dom加载完成后被触发,而async
则是下载完后立刻执行.
那么这里有个新的疑问,既然有了强大的defer/async
以及十分人性化的preload
策略.是否我们我们就可以抛弃第一条优化方案:将脚本放在底部了呢?
答案是:在不考虑兼容性的情况下,确实可以替代.
所以说抛开兼容性谈性能都是耍流氓😅
async兼容性
defer兼容性
众所周知,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的基础原理....[笑]
其实就是用XHR对象获取脚本并注入页面中.XHR就不再赘述了.
以发送请求的方式获取js文件有个好处.你可以加载javaScript文件但是不立刻执行,你可以根据条件判断精准的控制js的执行时间.同时这个方法兼容性良好.
而制约XHR的缺点就在于请求的javaScript文件必须和页面文件处于相同的域,不然就会出现跨域的问题了.
高性能浏览器的功能
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 是一个声明式 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">
浏览器会在空闲的时候,下载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
同上,同时也不要忽视其本身带来的问题.
-
觉得文章对你有用的话请点个赞👍吧~这就是对作者最大的鼓励