《高性能Javascript》第一章:加载和执行

90 阅读2分钟

前言

《高性能 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>

依次输出:deferscriptload

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)