浏览器渲染原理

959 阅读6分钟

浏览器渲染总体来说分为以下几步

  1. 浏览器通过HTTP或者HTTPS协议,先服务端请求页面
  2. 把请求回来的HTML解析成DOM树
  3. 把CSS解析成CSSOM树
  4. 把DOM树和CSSOM树组合在一起,生成渲染树(RENDER TREE)
  5. 通过渲染树计算出布局(layout)
  6. 渲染引擎会遍历Render树,绘制(painting)到界面上

网络部分放到TCP协议中,在这就不多说了

渲染流程

构建DOM树、CSSOM树

DOM树和CSSOM树的构建流程非常像,就以DOM树为例

浏览器从服务器获取的是16进制文件流,比如3C 62 6F 64 79 3E 48 65.....,浏览器要把16进制的Bytes转化成字符串,再遍历这个字符串解析成tokens

浏览器是怎么将字符串解析成tokens的。使用的方法是状态机

状态机怎么执行的

  • 接受到<字符。可能是一个标签的开头,开启一个状态
    • 下一个字符是字母,就是标签名
    • 下一个字符是!,就是注释
    • ....
  • 接受到非<字符。可能是一个文本节点,开启一个状态
    • ....

浏览器一步步将文件流转化为字符串再通过状态机转化为token,得到token后,按照W3C规则转换成DOM树。

简单总结下:

  1. 浏览器边接受文件流(进制编码内容)边编译为token
  2. 按照w3c规则进行字符解析,生成对应的Tokens,最后转换为浏览器内核可以识别渲染的DOM节点
  3. 按照节点最后解析为对应的 DOM TREE、CSSOM TREE

需要注意的事:

  • DOM树构建过程可能会因为css、js而阻塞
  • DOM树构建与CSSOM树构建可以同时进行
  • 不可见标签也会出现在dom树中
  • CSSOM树构建过程可能会因为js而阻塞

构建渲染树(Render Tree)

浏览器根据DOM树和CSSOM树生成带有标签和样式信息的渲染树(Render Tree)。渲染树与DOM树不是一一对应的关系,不显示的节点不会出现在渲染树上。

布局(Layout)

根据渲染树提供的节点和样式,计算元素在视口中的确切的大小和位置。

绘制(paining)

将元素计算后的大小和位置渲染到页面上的过程。渲染树的绘制工作是浏览器调用UI后端组件完成的。

回流和重绘(reflow和repaint)

一些操作会引发元素位置或者大小的变化,这样浏览器需要重新进行Lauout计算(回流/重排),重排完成后,浏览器需要重新绘制(重绘)。

  • 第二次或多次布局(Lauout)就是回流/重排(reflow)
  • 第二次或多次绘制(paining)就是重绘(repaint)

如果是改变一些基础样式比如颜色,则不需要重排,只需要重绘即可。

  • 重绘:元素样式改变
    • 例如:color、visibility...
  • 回流:元素大小、位置发生变化
    • 例如:添加删除元素、视口大小改变...

重绘不一定会回流,但是回流一定会触发重绘

性能优化:减少DOM的回流

  1. 放弃传统操作dom的时代,基于vue/react开始数据影响视图模式(mvvm/mvc/virtual dom/dom diff....)
  2. 分离读写操作(现在的浏览器都有渲染队列的机制)
  3. 样式集中改变
  4. 缓存布局信息
  5. 元素批量修改
  6. 使用DocumentFragment将需要多次修改的DOM元素缓存,最后一次性append到真实DOM中渲染
  7. 变化多的元素脱离文档流,形成新的Render Layer,降低回流成本
  8. css3硬件加速(GPU加速)(会占用大量内存)

资源加载

浏览器自上而下读取代码,读取到资源文件

css

使用css有三种方式:使用link、@import、内联样式,其中link和@import都是导入外部样式。它们之间的区别:

  • link:浏览器会派发一个新等线程(HTTP线程)去加载资源文件,与此同时GUI渲染线程会继续向下渲染代码
  • @import:GUI渲染线程会暂时停止渲染,去服务器加载资源文件,资源文件没有返回之前不会继续渲染(阻碍浏览器渲染)
  • style:GUI直接渲染

另外外部样式如果长时间没有加载完毕,浏览器为了用户体验,会使用浏览器会默认样式,确保首次渲染的速度。所以css一般写在headr中,让浏览器尽快发送请求去获取css样式。

javascript

JavaScript执行线程与GUI渲染线程不能同时执行,这就意味着执行js代码势必会阻塞页面渲染。为了不阻塞页面的渲染,可以:

  • script标签放在页面的尾部,确保dom生成完再加载js
  • 尽可能使用defer、async

关于<script><script defer><script async>的区别(配合图片食用更佳)

  • <script>:立即停止页面渲染去加载资源文件,当资源加载完毕后立即执行js代码,js代码执行完毕后继续渲染页面
  • <script defer>:开辟新的线程去加载资源文件,当资源加载完毕后等待页面渲染,页面渲染完毕后再执行js代码
  • <script async>:开辟新的线程去加载资源文件,当资源加载完毕后立即执行js代码,js代码执行完毕后继续渲染页面(特别注意:多个async js执行顺序是按照加载完毕的顺序,非js请求顺序)

\ 阻塞页面渲染(GUI线程) 立即加载js资源 js加载完毕后立即执行 按照script标签顺序执行脚本
script
defer
async

补充:window.onload 和 DOMContentLoaded 的区别

onload:是页面资源加载完毕,包括图片、视频资源

DOMContentLoaded:DOM渲染完成

性能优化

了解这么多,最终还是要为了性能优化服务。除了已经提过的减少回流的优化外,还有:

  • 减少DOM树渲染的时间
    • HTML层级不要太深
    • 标签语义化(减少不标准语义化的特殊处理)
  • 减少CSS树渲染时间
    • 减少层级嵌套(选择器是从右向左解析的)(less、sass嵌套是个大坑,注意)
  • 减少资源加载时间
    • 利用浏览器并行加载资源次数、请求大小,不要太少也不要太多(6-7)
    • 一般会把css放在页面开始位置,提前请求
      • 使用link,不用@import
      • 如果css少,尽可能采用内嵌式
    • ssr 减少数据首页数据的请求
    • 使用骨架屏、loding(感官上的提高,不会实际提高速度)
    • 避免阻塞的js加载
      • js放在页面底部
      • 尽可能使用defer、async