浏览器渲染总体来说分为以下几步
- 浏览器通过HTTP或者HTTPS协议,先服务端
请求页面
- 把请求回来的HTML解析成
DOM树
- 把CSS解析成
CSSOM树
- 把DOM树和CSSOM树组合在一起,生成
渲染树(RENDER TREE)
- 通过渲染树计算出
布局(layout)
- 渲染引擎会遍历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树。
简单总结下:
- 浏览器边接受文件流(进制编码内容)边编译为token
- 按照w3c规则进行字符解析,生成对应的Tokens,最后转换为浏览器内核可以识别渲染的DOM节点
- 按照节点最后解析为对应的 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的回流
- 放弃传统操作dom的时代,基于vue/react开始数据影响视图模式(mvvm/mvc/virtual dom/dom diff....)
- 分离读写操作(现在的浏览器都有渲染队列的机制)
- 样式集中改变
- 缓存布局信息
- 元素批量修改
- 使用DocumentFragment将需要多次修改的DOM元素缓存,最后一次性append到真实DOM中渲染
- 变化多的元素脱离文档流,形成新的Render Layer,降低回流成本
- 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