深入浏览器的渲染原理
网页的解析过程
- 在浏览器输入一个域名后,会通过DNS服务器解析成相应的IP地址
- 通过IP地址访问对应的服务器
- 目标服务器会返回相关文件
- 浏览器在解析html文件的时候,遇到引入的css样式以及引入的script文件,都会从静态服务器中再次下载
浏览器渲染流程
解析一 :解析HTML文件,生成DOM Tree
- 浏览器在拿到服务器返回的index.html文件后
- 会解析HTML,将其解析成为DOM Tree
在解析CSS文件的时候,不会影响DOM Tree的解析
解析二 :生成CSS规则
-
在解析过程中,如果遇到CSS的link元素,那么会由浏览器下载对应的CSS文件
- 下载CSS文件不会影响DOM Tree的生成
-
浏览器在下载完CSS文件之后,就会解析CSS,生成相应的规则树
- 我们可以称之为CSSOM
解析三 :构建Render Tree
-
当有了DOM Tree 和CSSOM Tree 就可以将两个结合,一同构建Render Tree了
-
注意:
- link元素不会阻塞DOM Tree的构建过程,但是在理论上会阻塞Render Tree的构建
- Render Tree和DOM Tree并不是一一对应的关系,比如某些元素设置成了dispaly:none,就不会在Render Tree中显示
解析四 :布局(layout)和绘制(paint)
-
在得到Render Tree之后,就会在上面运行布局,用来计算每个节点的几何体
- 渲染树会记录显示哪些节点以及其他样式,但是不记录每个节点的尺寸、位置等信息
- 布局是确定每个节点的高度、宽度和位置信息
-
将每个节点绘制到屏幕上
- 在绘制阶段,浏览器将布局阶段计算的每个frame转为屏幕给上实际的像素点
- 包括将元素的可见部分进行绘制,比如文本、颜色、边框等
回流和重绘解析(性能优化)
-
理解回流reflow
- 第一次确定了节点的大小和位置,称之为布局
- 在之后的操作,引起了节点大小、位置修改重新计算,称之为回流
-
引起回流的情况
- 比如DOM结构发生了变化(添加或者移除节点)
- 改变了布局(修改了width、height、padding、font-size)
- 修改了窗口尺寸
-
理解重绘(repaint)
- 第一次渲染内容称之为绘制
- 之后重新渲染称之为重绘
-
引起重绘的原因
- 比如修改背景颜色,文字颜色,边框颜色,样式等
- 总之对布局以及大小没有修改
-
两者之间的关系
- 回流一定会引起重绘,所以回流是十分消耗性能的事
-
避免回流的方法
-
修改样式时,尽量一次性修改:通过cssText修改,比如通过添加class属性进行
-
避免进行频繁的DOM操作:我们可以将多次DOM操作,通过DocumentFragment转化成一次DOM操作
-
尽量避免通过getComputedStyle获取尺寸,位置等信息
-
对某些元素使用position中的absolute或者fixed定位
- 此种方法也会引起回流,但是开销相对会小,不会对其他的元素造成影响
-
合成和性能优化(性能优化)
特殊解析-composite合成
- 首先要了解图层,在Chrome浏览器中,F12可以查看当前界面的图层
-
默认情况下,标准流中的内容都会被绘制在同一个图层之中(layer)
-
而一些特殊的属性,会创建一个新的合成层,并且新的合成层可以利用GPU来加速绘制
- 因为每个合成层都是单独渲染的
-
以下属性可以形成新的合成层
- 3D transforms
- viedo、canvas、iframe
- opacity动画转换时
- position:fixed
- will-change
-
注意:分层可以提高性能,但是是以内存管理为代价,因此不应该作为web性能优化策略而过度使用!!
defer和async
script元素处理
- 通过以上学习,我们知道浏览器在解析HTML文件的时候,遇到link的时候,不会影响解析DOM
- 但是遇到script元素的时候,首先下载JavaScript,浏览器就会停止加载DOM Tree
- 只有等待JavaScript脚本执行结束后,才会继续解析HTML,构建DOM树
-
那么为什么会出现以上现象?
- 因为js作用之一就是操作DOM,并且可以修改DOM
- 如果我们等到DOM树构建完成且渲染完之后,再执行JavaScript,会造成严重的回流和重绘,影响页面的性能
- 所以会在遇到script元素时候,优先下载和执行JavaScript代码,再继续构建DOM树
-
在现代页面开发中,这种形式会带来新的问题
- 在目前开发模式中(Vue、React)脚本往往比HTML页面更重,处理时间会更长
- 所以会造成页面的解析阻塞,在脚本下载、执行完成之前,用户在界面上什么都看不到
-
为了解决当前的问题,script元素提供了两个属性defer和async
defer (下载完,等待DOM Tree的构建完成再执行)
-
告诉浏览器不要等待脚本的下载,而是继续解析HTML,构建DOM Tree
- 在遇到 有defer属性的script的脚本,浏览器会下载脚本,但是不会阻塞DOM Tree的构建
- 如果脚本提前下载好了,会在DOM Tree构建完成之后,DOMContentLoaded事件之前执行defer中的代码
- 所以 DOMContentLoaded中的代码总是会等待defer中的代码执行完之后再执行
- 另外 多个defer脚本会按照顺序依次执行
- 从某种角度来说,defer可以提高页面的性能,推荐写在head元素中
async (下载完,立即执行)
- 让一个脚本完全独立
- 浏览器遇到async不会阻塞DOM Tree的构建(与defer类似)
- 但是 async属性的脚本,在下载完成之后会立即执行,不能保证其执行的时机
- 同时,多个async的脚本在一起的时候,不能保证其时机
- 因此,defer里面的脚本,通常可以写一些DOM操作,并且对脚本执行顺序有要求的代码;而async中写一些与DOM无关,且不用保证执行时机的代码