前端性能优化之浏览器关键渲染路径

字数 2174阅读 387

写在前面

关于前端性能优化的文章非常多,写浏览器关键渲染路径的也不少,但总是感觉哪里错了或者哪里疏忽了,于是自己写一篇,同时也是最近面试的一篇总结~
下面分别从浏览器渲染过程文档资源的加载与阻塞关键渲染路径及优化等三个角度来看。

浏览器渲染过程

面试常被问到 “前端怎样性能优化”,我自己在回答时总是会按照 “浏览器从输入一个 URL 到页面显示经历了什么” 的思路去回答,这样的回答既显条理,又不易因紧张而将一些简单的性能优化法忘记。扯回来,浏览器渲染可以是这个题目的最后一个环节,先上一张自己画的图:

浏览器渲染过程

类似的图也有不少,但大同小异,大概即是浏览器首先加载 HTML 文档,过程中遇到了其他资源会同时加载并解析其他资源,最后生成 Rendering tree 经过 layout 与 paint 显示到页面。需要注意的是,html 渲染为增量式渲染,浏览器无需等待 html 加载完便开始解析构建 DOM。这张图有助于后面的理解,但不再细说。

记:哪些操作会触发 reflow ?哪些会触发 repaint ?

HTML / CSS / JS 的加载与阻塞

关于加载

需要明确,.html / .css / .js 包括图片音频等其他资源,在加载上几乎完全并行(几乎?请看下文),不存在阻塞一说。诚然,我们访问 example.com/index.html 时需要解析到 <link rel="stylesheet" href="/style.css"> 才会去加载 style.css,但这并不影响浏览器同时去加载另外的资源。

  1. 加载:汉语词语,字面意思是增加装载量。现多用于计算机相关领域,表示启动程序时文件或信息的载入。英译 “load”。(来自百度百科)
  2. 在前端这里,“加载” 就是下载。
  3. 很多文章将 “加载 (load)” 与 “解析 (parse)” “渲染 (layout+paint)” 混淆,我认为是不妥的,本文会一一讲到。

资源之间的加载是并行,互不影响的,但是加载也是有限制的,现代浏览器对同一域名下的最大并发连接数一般是 6,也就是说如果同时对同一域名发起超过 6 个连接,超出的请求就会被阻塞。常看到 js css 等静态文件使用 CDN 托管,一个原因即是通过使用多域名并发加载,提高加载速度。

但加载需要时间,现代浏览器为了性能考虑,不会无限制的等待资源加载,对资源的加载会有失效时间。在 Chrome 60 中测试加载 js / css 文件时最多会等待 4 min,若 4 min 内服务器无任何响应数据返回,则会触发加载错误,会在 console 输出 net::ERR_EMPTY_RESPONSE 错误提示,同时浏览器继续解析。但若 4 min 内返回部分响应数据,则会继续等待完整的响应返回(即会突破 4 min 的失效时间限制,我测试了 13 min 仍然没有报错我放弃了,推测浏览器没有对此做进一步的时间限制,这是合理的),这里与服务器推送技术 long-polling 相似。见下图:

net::ERR_EMPTY_RESPONS 错误

关于阻塞

明确了这点,那么资源之间的阻塞是怎样的?阻塞是指资源加载完之后按照一定的顺序解析 (parse) (或 html 文档的渲染等)(注意这里的顺序不是指完全的串行),有顺序就有阻塞,下面讲到 css 阻塞 html 渲染,js 阻塞 DOM 树构建,css 阻塞 js 的解析,js 阻塞一切等:

  1. css 阻塞 html 渲染 (layout)
    试想,如果 css 不阻塞 html 渲染,那么浏览器会先将无样式的 html 渲染出来,然后突然产生样式,即 FOUT(Flash Of Unstyled Text)。因此现代浏览器中,通过内联 <style>,外联 <link> 以及
    document.write('<link ...>') 等引入的 css 均会阻塞 html 渲染,但不会影响 html 构建 DOM 树。
    需要注意的是,这与 css 在文档中引入位置无关,考虑下面的代码:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    
    <body>
        <h1>hello, zphhhhh</h1>
        <link rel="stylesheet" href="/css/style.css">
    </body>
    
    </html>
    

    上面代码若 css 文件加载需要 2s,文档会在 2s 后才显示出来,但 DOM 树早已构建完毕,就等 CSSOM 了。但也存在 css 不阻塞的情况,比如

    • 媒体查询不符合时,会同时加载但不会阻塞 html 渲染:
      <link rel="stylesheet" href="index_print.css" media="print">
      
    • 使用 DOM API 动态生成 link:
      document.createElement('link');
      
    • CSS preload,是 Resource Hints 规范,兼容性问题不再细说,可自行了解。
      <link rel="preload" href="index.css" as="style" onload="this.rel='stylesheet'">
      
  2. js 阻塞 DOM 树构建(没有 DOM 树就没有 HTML 渲染)
    js 可以通过 document.write 修改 HTML 文档流,因此在执行 js 时,浏览器会停止构建 DOM,但由于浏览器的增量构建,浏览器可能会渲染出一部分 DOM( js 之前),考虑下面的代码:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <title>Document</title>
    </head>
    
    <body>
        <h1>hello,</h1>
        <script src="/main.js"></script>
        <h1>zphhhhh</h1>
    </body>
    
    </html>
    

    上面代码若 js 文件加载需要 2s,文档会先显示 hello,,2s 后再显示 zphhhhh。但也存在 js 不阻塞 DOM 构建的情况,可以:

    • 加入 async / defer 属性(只是有可能不阻塞),声明 js 中没有使用 document.write(),二者区别在于,声明 async 会在 js 加载完后立即解析执行,而声明 defer 在 js 加载完仍需等待 DOM 树构建完成(即推迟执行),看图就明白:

      async VS defer

      如下代码:

      <script async src="./main.js"></script>
      或
      <script defer src="./main.js"></script>
      

      但这也意味着 js 中真的不能使用 document.write() 了,强行使用会有提示,且没有生效,但同一文件中其他代码仍可正常执行:

      Failed to execute 'write' on 'Document': It isn't possible to write into a document from an asynchronously-loaded external script unless it is explicitly opened.
      
    • 使用 DOM API 动态生成 script

      document.createElement('script');
      
  3. css 阻塞 js 解析(执行)
    由于 js 可能会读取或修改 CSSOM,因此需等待CSSOM构造完成后,js 才能执行。考虑如下代码:

    <body>
        <h1>hello,</h1>
        <link rel="stylesheet" href="/css/style.css">
        <script src="/main.js"></script>
        <h1>zphhhhh</h1>
    </body>
    

    若 css 加载 2s,则 js 会等待 css 的加载和构建 CSSOM,完毕后才会执行。当然,若上例的 js 声明了 async / defer,则以 async / defer 的约定执行,即可能会 js 不被 css 阻塞。

  4. 扩展上述第 2 点即为 js 的解析执行几乎阻塞一切
    不难理解,浏览器 js 的单线程模型也使其更加简单,js 在解析执行期间几乎会阻塞一切,当然是指阻塞一切其他资源的解析构建渲染等等,不包含加载。

浏览器关键渲染路径

理清了 html / css / js 等资源的加载与阻塞,终于可以开心的继续理解浏览器的关键渲染路径了。关键渲染路径什么鬼?这是指浏览器从最初的加载资源到第一次显示内容需要的资源与时间,考虑下面代码:

<html>
  <head>
    <link rel="stylesheet" href="style.css">
    <script src="index.js"></script>
    <script src="baidu_tongji.js"></script>
  </head>

  <body>
  </body>
</html>

此时关键资源数有 1*.html + 1 * .css + 2 * .js = 4 个,尝试改为:

<html>
  <head>
    <style>
        /* style.css */
    </style>
    <script>
        // index.js
    </script>
    <script defer src="baidu_tongji.js"></script>
  </head>

  <body>
  </body>
</html>

则关键资源数只有 1*.html 个,当然这只是一个最简单的模型,实际操作当然不能把所有 js css 写到 html 中,意会就好~

优化的角度来看,可以从下面几个方面考虑:

  1. 考虑到浏览器对同一域名最大并发连接限制,可以减少 http 请求,合并请求,比如合并 css,打包 js,小文件使用 base64 等,Webpppppack~
  2. 适量的多域名提高并发数量,但不能太多,因为 DNS 解析等也需要花费时间。
  3. 启用压缩,压缩静态资源体积。
  4. 启用缓存。
  5. 以及上面讲 “加载与阻塞” 提到的方法~

// 暂时想到这么多,经验不足如本文有错误之处,望请及时指出,免误后人,非常感谢。

推荐阅读更多精彩内容