前言
《高性能 JavaScript》于 2015 年 8 月首次付梓问世,不知不觉间,它已在我的书架上静置了好些年。回首往昔,虽曾多次试图将其读完,却总是因缺乏足够的耐心而未能如愿。时过境迁,书中提及的诸多解决方案或许已难以契合当下的技术潮流,然而,这并不妨碍它作为一本具有参考价值的书籍而存在。鉴于此,我决定不再进行全文通读,而是采用笔记的形式对其进行精读记录,如此一来,往后查阅关键知识点时便能更加便捷高效,让这本旧书在新的学习方式下重新焕发光彩,为我的知识储备持续贡献力量。
正文
多数浏览器使用单一进程来处理用户界面(UI)刷新和JavaScript脚本执行,所以同一时刻只能做一件事。JavaScript执行过程耗时越久,浏览器等待响应的时间就越长。
JavaScript代码为什么会阻塞DOM的加载和渲染
<script>标签每次出现都会霸道地让页面等待脚本的解析和执行。无论当前的JavaScript代码是内嵌还是包含在外链文件中,页面的下载和渲染都必须停下来等待脚本执行完成。这是页面生命周期中的必要环节,因为脚本执行过程中可能会修改页面内容。
示例代码:
<html>
<head>
<title>Script Example</title>
</head>
<body>
<p>
<script type="text/javascript">
document.write("The date is" + (new Date()).toDateString())
</script>
</p>
</body>
</html>
当浏览器遇到<script>标签时,当前HTML页面无从获知JavaScript是否会向〈p〉标签添加内容,或引入其他元素,或甚至关闭该标签。因此,这时浏览器会停止处理页面,先执行JavaScript代码,然后再继续解析和渲染页面。
如果使用src加载外部资源,同样也会先下载外链文件中的代码,然后解析并执行。在这个过程中,页面渲染和用户交互式被完全阻塞的。
如何改善阻塞
脚本位置
将<script>标签放在</body>之前,减少对整个页面下载的影响。
组织脚本
将多个外链的js文件合并,减少http请求的性能开销。
无阻塞脚本
在页面加载完成后才加载JavaScript代码。
defer
带有defer属性的<script>标签中的JavaScript文件将在页面解析到<script>标签时开始下载,但并不会执行,直到DOM加载完成(onload事件被触发之前)。
任何带有defer属性的<script>元素中DOM完成加载之前都不会执行,无论内嵌或外链脚本都是如此。
示例代码:
<html>
<head>
<title>Script Defer Sample</title>
</head>
<body>
<script defer>
alert("defer")
</script>
<script>
alert("script")
</script>
<script>
window.onload = function(){
alert("load")
}
</script>
</body>
</html>
依次输出:defer、script、load。
defer和async的区别
相同点:都是异步加载,不会阻塞DOM的解析和渲染。
不同点:执行时机不同。
defer在window.onload之前执行。async在异步下载完成后,立即执行。
动态脚本
使用JavaScript动态创建script标签。
var script = document.createElement('script')
script.type = "text/javascript"
script.src = "file1.js"
document.head.appendChild(script)
给script标签添加onload事件,确保加载完成后执行一些任务。
var script = document.createElement('script')
script.type = 'text/javscript'
script.onload = function(){
alert('script loaded')
}
script.src = 'file1.js'
document.head.appendChild(script)
在IE浏览器中以另外一种方式实现,它会触发一个readystatechange事件。<script>元素提供一个readyState属性。它的值在外链文件的下载过程的不同阶段会发生变化。该属性有5种取值:
uninitialized- 初始状态loading- 开始下载loaded- 下载完成interactive- 数据完成下载但尚不可用complete- 所有数据已准备就绪
IE在标识最终状态的readyState的值时并不一致,有时<script>元素到达loaded状态而不会到达complete,有时甚至不经过loaded就到达complete状态。所以使用readystatechange事件最靠谱的方式是同时检查2种状态,只要其中任何一个触发,就剔除事件处理器(以确保事件不会处理两次)。
var script = document.createElement('script')
script.type = 'text/javascript'
//IE
script.onreadystatechange = function(){
if(script.readystate == 'loaded' || script.readystate == 'complete'){
script.onreadystatechange = null
alert('script loaded')
}
}
script.src = 'file.js'
document.head.appendChild(script)