JS 高级 - 浏览器渲染原理

1,140 阅读10分钟

网页请求过程

image.png

image.png

网页解析过程

image.png

HTML解析过程

默认情况下服务器会给浏览器返回index.html文件,所以浏览器拿到网页的第一步是解析index.html, 并构建对应的DOM树

image.png

生成css规则

在解析的过程中,如果遇到CSS的link元素,那么会由浏览器负责下载对应的CSS文件

下载和解析css的过程,并不会阻塞DOM Tree的解析和形成,但是可能会阻塞Render Tree的生成

浏览器下载完CSS文件后,就会对CSS文件进行解析,解析出对应的规则树( 规则树 又被称之为 CSSOM - CSS Object Model,CSS对象模型 )

image.png

构建Render Tree

当有了DOM Tree和 CSSOM Tree后,就可以两个结合来构建Render Tree了

image.png

  1. link元素不会阻塞DOM Tree的构建过程,但是会阻塞Render Tree的构建过程

    即生成DOM Tree的解析过程和CSSOM的解析过程是两个独立的线程

  2. 虽然Render Tree在构建时,需要对应的CSSOM Tree,但是可能出现对应css文件根本无法解析的情况,如css文件不存在,此时浏览器就不会一直阻塞对应的Render Tree的生成

  3. Render Tree和DOM Tree并不是一一对应的关系

    • 比如对于display为none的元素,压根就没有必要出现在render tree中

布局(layout)和绘制(Paint)

布局

在渲染树(Render Tree)上运行布局(Layout)以计算每个节点的几何体(也就是实际需要渲染的位置和大小相关信息)

  • 渲染树会表示显示哪些节点以及其他样式,但是不表示每个节点的尺寸、位置等信息
    • render tree的主要作用是组合(attachment) DOM Tree 和 CSSOM,并移除那些不需要渲染的元素
    • 布局过程会根据实际需要渲染的设备相关信息,确定呈现树(布局树 render tree)中所有节点的宽度、高度和位置等相关信息,这些实际需要渲染的相关信息被称之为一个个的frame

绘制

在绘制阶段,浏览器将布局阶段计算的每个frame转为屏幕上实际的像素点,也就是实际进行渲染。

image.png

重绘和回流

回流 (reflow)

第一次确定节点的大小和位置,称之为布局(layout)

之后对节点位置和大小进行修改,而导致需要重新layout和render的过程被称之为回流 (也可以称之为重排)

  • 比如DOM结构发生改变(添加新的节点或者移除节点)
  • 比如改变了布局(修改了width、height、padding、font-size等值)
  • 比如窗口resize(修改了窗口的尺寸等)
  • 比如调用getComputedStyle方法获取尺寸、位置信息
    • getComputedStyle方法会重新computed style, 即重新计算并获取元素最新的frame相关信息
    • 所以getComputedStyle方法会导致重新layout和paint,也就是会导致回流

回流就意味着需要重新进行layout和paint,即重新计算每一个元素的frame,并重新进行render操作,所以回流是十分消耗浏览器性能的操作,在开发中应该尽可能避免不必要的回流操作

重绘 (repaint)

第一次渲染内容称之为绘制(paint), 之后对页面元素所进行的重新渲染操作就被称之为重绘

比如修改背景色、文字颜色、边框颜色、样式等 就会导致元素的重绘

回流一定会引起重绘,所以回流是一件很消耗性能的事情, 因为回流一定会重新执行paint操作

在开发中要尽量避免发生回流:

  1. 修改样式时尽量一次性修改

    • 比如通过cssText修改, 或将样式定义为对应的class后在进行修改
  2. 尽量避免频繁的操作DOM

    • 可以创建DocumentFragment,将要进行的DOM操作在fragment中进行统一处理后在更新到DOM中
  3. 尽量避免通过getComputedStyle获取尺寸、位置等信息

    • 虽然有的浏览器会对getComputedStyle方法获取元素尺寸、位置之类的行为进行优化,并不会引发对应的重绘和回流
    • 但并不是所有的浏览器都会对getComputedStyle方法获取元素尺寸、位置之类的行为进行相应的优化
  4. 将某些元素设置为绝对定位元素 ( position的值为absolute或者fixed的元素 )

    • 并不是不会引起回流,而是因为绝对定位元素脱标了,所以开销相对较小,并不会对其他元素造成影响

特殊解析 – composite合成

composite合成是浏览器在render阶段进行的一种优化,其可以在绘制的过程中,将布局后的元素绘制到多个合成图层中,以最大程度的减少页面的回流

在浏览器进行render的时候,标准流中的内容会作为一个渲染图层(render layout)进行布局,

使用了absolute定位的元素,在render 的时候,也会生成一个render layout来进行布局

但是实际上,浏览器在渲染的时候,会将这多个render layout合并为一个图层来进行渲染,

而这个图层就是合成图层(CompositingLayer 复合图层)

我们可以通过浏览器调试工具的layout页签(位于 更多->more tools 中), 查看网页的渲染图层的构成

每一个合成图层,浏览器都会单独进行渲染,且在渲染合成图层的时候,浏览器会采用GPU进行硬件加速

所以如果我们将那些需要频繁改变,导致页面回流的元素放置到一个单独的复合图层中进行单独渲染的时候

就可以有效的避免这些元素的回流对于界面中其它元素布局的影响。

但是开辟一个新的图层,必然需要在内存中开辟对应的空间去存储新图层中相关信息,并需要对新图层进行单独管理和维护

所以分层确实可以提高性能,但是它以内存管理为代价,因此不应作为 web 性能优化策略的一部分过度使用

可以产生复合图层的元素:

  1. 3D transforms

  2. video、canvas、iframe

  3. 对opacity、transform进行animation动画或transition动画的动画执行过程中

    • 在动画开始和结束的时候,并不会开启新的复合图层,只有在动画执行过程中会开辟对应的新的复合图层

      所以只会在动画开始和结束的时候才会可能产生回流,而不需要在动画的执行过程中频繁的产生回流

    • 因此在执行动画的时候,推荐尽可能使用opacity或transform属性作为动画的变化css属性,而不是其它css属性

  4. position: fixed

  5. will-change --> 一个实验性的属性,用于提前告诉浏览器,未来元素的某个属性可能会发生改变

    • 当will-change的值是opacity或者transform的时候,浏览器会预先为对应的元素生成一个单独的复合线程,并在对应的合成图层上渲染对应的内容

示例:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box,
    .container {
      width: 200px;
      height: 200px;
      background-color: red;
      display: inline-block;
    }

    .container {
      background-color: orange;
      transition: all 1s ease;
    }

    .box1 {
      background-color: green;
    }

    .container:hover {
      /* 
      	transform在执行动画的过程中会开辟新的复合图层
        所以div.container在执行动画的过程中,并不会对其它元素产生影响
      	因此在执行动画的过程中div.box1并不受影响
      */
      transform: translateX(100px);
    }
  </style>
</head>
<body>
  <div class="box"></div>
  <div class="container"></div>
  <div class="box box1"></div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
  <style>
    .box,
    .container {
      width: 200px;
      height: 200px;
      background-color: red;
      display: inline-block;
    }

    .container {
      background-color: orange;
      transition: all 1s ease;
    }

    .box1 {
      background-color: green;
    }

    .container:hover {
      /*
      	使用margin-left执行动画的时候,并不会开辟一个新的图层
      	所以其会影响后续元素的布局,且需要频繁的进行回流,性能比较低
      	因此div.box1会随着div.container在执行动画过程中位置的改变而相应的发生改变
      */
      margin-left: 100px;
    }
  </style>
</head>
<body>
  <div class="box"></div>
  <div class="container"></div>
  <div class="box box1"></div>
</body>
</html>

script 和 页面解析关系

事实上,浏览器在解析HTML的过程中,遇到了script元素是不能继续构建DOM树的

它会停止继续构建,首先下载JavaScript代码,并且执行JavaScript的脚本

只有等到JavaScript脚本执行结束后,才会继续解析HTML,构建DOM树

这是因为JavaScript的作用之一就是操作DOM,并且可以修改DOM

如果我们等到DOM树构建完成并且渲染再执行JavaScript,会造成严重的回流和重绘,影响页面的性能

所以会在遇到script元素时,优先下载和执行JavaScript代码,再继续构建DOM树

但是如果在JS脚本比页面结构重的情况下,解析JS必然会花费比较多的时间,

而JS的解析又会造成页面的解析阻塞,所以在脚本下载、执行完成之前,用户在界面上什么都看不到

所以 绝大部分的浏览器对这类行为进行了优化

为了避免用户在加载网页的时候,出现长时间的白屏,

浏览器即使遇到了JS脚本也会先将部分已经解析完的dom tree在界面中先渲染出来

这是浏览器的一种优化手段

<div>这是遇到JS脚本之前的内容</div>

<script>
  // 虽然原则上,dom树应该等到所有的JS脚本下载并解析完毕后在生成对应的DOM Tree
  // 但是为了避免用户访问网页的时候出现长时间的白屏,浏览器对这种行为进行了一定程度上的优化
  // 即在解析脚本之前,会先将已经解析完的部分DOM Tree先在浏览器中渲染出来,随后再去解析对应的JS脚本
  
  // 所以在debugger的过程中,可以在界面中看到 "这是遇到JS脚本之前的内容"
  // 而不会在界面中看到 "这是遇到JS脚本之后的内容" 
  debugger
</script>

<div>这是遇到JS脚本之后的内容</div>

但是有的时候这并不是我们所需要的,有的时候我们就希望JS脚本的加载和解析过程并不会阻塞DOM Tree的形成,

为此script元素给我们提供了两个属性(attribute): defer和async

defer

defer 属性告诉浏览器不要等待脚本下载,而继续解析HTML,构建DOM Tree

也就是说defer属性 可以让 JS脚本的下载 和 HTML的解析,DOM树的构建并行处理

添加了defer属性的脚本,在下载好后,也不会立即执行,

而是会放到DOM Tree构建完成,DOMContentLoaded事件发生之前执行defer中的代码

defer的特点:

  1. 多个带defer的脚本是可以保持正确的执行顺序
  2. 从某种角度来说,defer可以提高页面的性能,并且推荐放到head元素中
  3. defer仅适用于外部脚本,对于script内部脚本,defer属性会被忽略

async

async是让一个脚本完全独立运行的属性

  • 浏览器不会因 async 脚本而阻塞(与 defer 类似)
  • async脚本不能保证顺序,它是独立下载、独立运行,不会等待其他脚本
  • async不会能保证在DOMContentLoaded之前或者之后执行

defer通常用于需要在文档解析后操作DOM的JavaScript代码,并且对多个script文件有顺序要求的

async通常用于独立的脚本,对其他脚本,甚至DOM没有依赖的脚本