浏览器加载及渲染机制

4,085 阅读9分钟

本文仅是技术验证,记录,交流,不针对任何人。有冒犯的地方,请谅解。 该文首发于https://vsnail.cn/static/doc/blog/browserPerformance.html

最近看了一篇文章《JS一定要放在Body的最底部么?聊聊浏览器的渲染机制》.从一道面试题说起,然后剖析了浏览器的加载和渲染机制.

为什么大家普遍把这样的代码放在body最底部?(为了沟通效率,我会提前和对方约定所有的讨论都以chrome为例)

在没有看过文章之前,估计我会回答为了是加快浏览器对页面的渲染.但是是哪个步骤,哪个时刻加速了就不得而知了.看过了这篇文章,大致了解了浏览器的加载和渲染机制.但是对于文中的有些说法百思不得其解,因此在某些问题上以实验的形式加以验证,故而对实验结果的总结对错与否估计也不能下个定论,只能权当参考.

浏览器的渲染机制

几个概念

  • 1、DOM:Document Object Model,浏览器将HTML解析成树形的数据结构,简称DOM。

  • 2、CSSOM:CSS Object Model,浏览器将CSS代码解析成树形的数据结构。

  • 3、DOM 和 CSSOM 都是以 Bytes → characters → tokens → nodes → object model. 这样的方式生成最终的数据。如下图所示:

上图是从大神那拷贝过来的,视觉中国哥哥,千万别来找我哦。我就是前端打杂一名,没钱的

DOM树的构建过程是一个深度遍历过程:当前节点的所有子节点都构建好后才会去构建当前节点的下一个兄弟节点。

  • 4、Render Tree:DOM 和 CSSOM 合并后生成 Render Tree,如下图:

上图是从大神那拷贝过来的,视觉中国哥哥,千万别来找我哦。我就是前端打杂一名,没钱的

Render TreeDOM一样,以多叉树的形式保存了每个节点的css属性、节点本身属性、以及节点的孩子节点。

注意:display:none 的节点不会被加入Render Tree,而visibility: hidden 则会,所以,如果某个节点最开始是不显示的,设为display:none是更优的。具体可以看这里

浏览器的渲染过程

  • 1、Create/Update DOM And request css/image/js:

浏览器请求到HTML代码后,在生成DOM的最开始阶段(应该是 Bytescharacters 后),并行发起css、图片、js的请求,无论他们是否在HEAD里。

注意:发起js文件的下载request并不需要DOM处理到那个script节点,比如:简单的正则匹配就能做到这一点,虽然实际上并不一定是通过正则。这是很多人在理解渲染机制的时候存在的误区

  • 2、Create/Update Render CSSOM

CSS文件下载完成,开始构建CSSOM

  • 3、Create/Update Render Tree

所有CSS文件下载完成,CSSOM构建结束后,和 DOM 一起生成 Render Tree

  • 4、Layout:

有了Render Tree,浏览器已经能知道网页中有哪些节点、各个节点的CSS定义以及他们的从属关系。下一步操作称之为Layout,顾名思义就是计算出每个节点在屏幕中的位置。

  • 5、Painting:

Layout后,浏览器已经知道了哪些节点要显示(which nodes are visible)、每个节点的CSS属性是什么(their computed styles)、每个节点在屏幕中的位置是哪里(geometry)。就进入了最后一步:Painting,按照算出来的规则,通过显卡,把内容画到屏幕上。

以上五个步骤前3个步骤之所有使用 “Create/Update” 是因为DOMCSSOMRender Tree都可能在第一次Painting后又被更新多次,比如JS修改了DOM或者CSS属性。

LayoutPainting 也会被重复执行,除了DOMCSSOM更新的原因外,图片下载完成后也需要调用LayoutPainting来更新网页。

以上都是《JS一定要放在Body的最底部么?聊聊浏览器的渲染机制》归纳总结出来的,并且文中一直都有提到"首屏"这个词,为了一致,本文中的"首屏"也是同一个意思,即,“没有图片的首屏”.

再来回到之前的面试问题,"script标签的位置会影响首屏时间么?"

其实通过上面的浏览器渲染过程的描述可以得知,其实script的位置不会影响首屏的时间,但有可能截断首屏的内容,使其只显示上面一部分。

并且《JS一定要放在Body的最底部么?聊聊浏览器的渲染机制》文中也总结了以下几点:

  • 如果script标签的位置不在首屏范围内,不影响首屏时间
  • 所有的script标签应该放在body底部是很有道理的
  • 但从性能最优的角度考虑,即使在body底部的script标签也会拖慢首屏出来的速度,因为浏览器在最一开始就会请求它对应的js文件,而这,占用了有限的TCP链接数、带宽甚至运行它所需要的CPU。这也是为什么script标签会有async或defer属性的原因之一。

疑惑和注意点

对于浏览器渲染过程有一定的了解了,但是还是有些疑惑以及注意点.

1. 第一次渲染耗时与script位置是无关的

通过对浏览器渲染过程的了解,我们可以发现其实浏览器第一次做Painting,是在Render tree构建完成后,layout计算完成以后进行的.那么可以理解为,如果(我是说如果,当然估计这种情况会很少)您要加载的js不会去改变dom的结构或样式.那么其实script标签放在什么位置并不会影响到浏览器的渲染耗时.

我们可以看看下面的例子.我们写了一个简单的web服务,使用node.js做后台,实际上后台仅仅只是一个静态资源服务器而已.我们在这个服务上面动了一点点的手脚,即,如果是请求bootstrap.min.js,那么我们将等待2秒后才返回,其他资源正常返回.

例子工程地址:github.com/btshj-snail…

同时,我们建立一个简单的html界面,这个界面引入了三个css,三个js.这个界面代码如下:


<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Document</title>
    <link rel="stylesheet" href="./style/bootstrap.min.css" />
    <link rel="stylesheet" href="./style/bootstrap-theme.min.css" />
    <link rel="stylesheet" href="./style/main.css" />
  </head>
  <body>
    <div>
      <p id="first" class="beautiful"></p>
      <p>人之初 性本善 性相近 习相远</p>
      <p>苟不教 性乃迁 教之道 贵以专</p>
      昔孟母 择邻处 子不学 断机杼<br />
      <img src="./img/1000.jpeg" alt="" style="width:200px;height:200px;margin:20px;"/>
      <p id="content"></p>
    </div>

    <script src="./js/jquery-2.1.1.min.js"></script>
    <script src="./js/bootstrap.min.js"></script>
    <script src="./js/main.js"></script>
  </body>
</html>

main.js文件中仅有一个简单的逻辑处理,即在id为content的元素中插入一些话而已.

//main.js
document.querySelector("#content").innerHTML = "请注意,请注意,重点不是美女....重点是你看到这串文字..这才是老夫的真实用意..这句话是通过js动态追加的.当你看到这句话的时候,表明这个js文件已经运行了...其实在你不知道的时候,我偷偷的将另外一个js延迟加载了..."


OK,一切都准备就绪了,那么我们来看看当我们延迟2秒返回bootstrap.min.js文件,会对main.js造成什么样的影响.

githubclone下来工程,然后进入工程目录,输入命令node ./server/app.js ,然后使用chrome访问http://localhost:9001/static/index.html.我们接着使用F12,打开chrome的开发者工具,点击performance,我们可以轻松的获取浏览器对该界面的渲染过程.

我们可以从图中可以看出,在93.6ms的时候,浏览器就已经开始paint了.而在2048ms时,才接受到bootstrap.min.js的数据.因此第一次渲染耗时与script位置是无关的.

2. 浏览器是严格按照script标签的顺序来加载的吗?

其实这个答案在不了解浏览器渲染过程的情况下,也知道这是肯定的.否则无法保证代码的依赖性.在很早以前,我们开发的时候,通常会将jquery的文件放在第一个引用,其他依赖jquery库的js文件放在其后面,这就是为了保证后面js文件使用jquery时,jquery已经被加载.通过这个,我们也可以得知浏览器是严格按照script标签的顺序来加载js文件的.

OK,我们依然用上面的例子来验证.上面的列子中我们引用main.js文件的script标签是放在bootstrap.min.jsscript标签之后的,并且在后台,会延迟2s才返回bootstrap.min.js,那么此时main.js上面时候返回,浏览器什么时候开始解析执行main.js?

从上图我们可以得知,其实第一次paint时,其实是没有渲染出美女图片下方的文字的.那么也就是说,其实第一次paint时,浏览器是没有执行main.js.那么有可能你会说,也许接收到main.js文件,实在paint之后呢.其实这个说法,也是完全有可能的,但是呢,结论先不说,嘿嘿..我们先来看看chromeperformance的渲染过程,用事实说话,嘿嘿.

上面按照时间顺序标了4条记录.

第一条,是浏览器接收到main.js数据;

第二条,是浏览器第一次开始paint

第三条,执行bootstrap.min.js文件.

第四条,执行main.js文件.

那么完全可以看出,下载js文件是"并发"的.而执行js文件,则是上一个文件执行完后,才执行下一个的.那么为了加快最终渲染效果,可以将不重要或者是第一次无需使用的js文件,放在所有script的后面加载.也可以给这些script标签加上deferasync

参考文献

1、《JS一定要放在Body的最底部么?聊聊浏览器的渲染机制》delai.me/code/js-and…

2、案例工程(exp-browserPainting) github.com/btshj-snail…