浏览器渲染机制
浏览器进程
renderer进程:
- 浏览器内核每个tab页面对应一个独立的renderer进程
- 内部有多个线程。
- 负责脚本执行,位图绘制,事件触发,任务队列轮询等
Broswer进程:
- 浏览器的主进程,负责主控,协调(浏览器大脑)
- 网络资源的管理,下载等(页面网络文件)
- 负责将renderer进程得到的存在内存中的位图渲染(显示)到页面
- 负责创建和销毁tab进程(renderer进程
- 负责与用户的交互
GPU进程:
负责3D绘制,只有当该页面使用了硬件加速才会使用它,来渲染(显示)页面。否则的话,不使用这个进程,而是用Browser进程来渲染(显示)页面
第三方插件进程:
每种类型的插件对应一个进程
renderer进程(浏览器渲染进程)
renderer进程是多线程进程
js引擎线程
- 也称js内核,解析js脚本,执行代码
- JS引擎一直等待任务队列中任务的到来,然后加以处理,一个Tab页(renderer进程)中只有一个JS线程在运行
- 与GUI线程互斥,即当js引擎线程运行时,GUI线程会被挂起,当js引擎线程结束运行时,才会继续运行GUI线程,所以如果JS执行的时间过长,要放在body下面,否则就会导致页面渲染加载阻塞。
- 由一个主线程和多个web worker线程组成,由于web worker是附属于主线程,无法操作dom等,所以js还是单线程语言(在主线程运行js代码
GUI渲染线程
- 负责渲染浏览器界面,用于解析html为DOM树,解析css为CSSOM树,布局layout,绘制paint
- 当界面需要重绘(Repaint)或由于某种操作引发回流(reflow)时,该线程就会执行
- 注意,GUI渲染线程与JS引擎线程是互斥的,当JS引擎执行时GUI线程会被挂起(相当于被冻结了),GUI更新会被保存在一个队列中等到JS引擎空闲时立即被执行
事件触发线程
- 当对应事件触发(不论是WebAPIs完成事件触发,还是页面交互事件触发)时,该线程会将事件对应的回调函数放入callback queue(任务队列)中,等待js引擎线程的处理
定时触发器线程
- setInterval与setTimeout在此线程中计时完毕后,把回调函数放入事件队列中
- 浏览器定时计数器并不是由JavaScript引擎计数的,(因为JavaScript引擎是单线程的, 如果处于阻塞线程状态就会影响记计时的准确),因此通过单独线程来计时并触发定时(计时完毕后,添加到事件队列中,等待JS引擎空闲后执行)
- 当setTimeout的定时的时间小于4ms,一律按4ms来算
http请求线程(异步)
- 每有一个http请求就开一个该线程
- 当检测到状态变更的话,就会产生一个状态变更事件,如果该状态变更事件对应有回调函数的话,则放入任务队列中
任务队列轮询线程
- 用于轮询监听任务队列,以知道任务队列是否为空
render树
html和css同时渲染不会造成堵塞,但render树是在dom树和cssdom树渲染完毕组成,css应该放在head头部越早引入,越早同时解析)
由DOM树与CSS树结合形成的渲染树(其中无法显示的元素,如script,head元素或diplay:none的元素,不会在渲染树中,也就最终不会被渲染出来),页面的布局,绘制都是以render树为依据。
回流与重绘
- 布局是页面首次加载时进行的操作,重新布局即为回流。当页面的某部分元素发生了尺寸、位置、隐藏发生了改变,页面进行回流。得对整个页面重新进行布局计算,将所有尺寸,位置受到影响的元素回流。
- 绘制是页面首次加载时进行的操作,重新绘制即为重绘。当页面的某部分元素的外观发生了改变,但尺寸、位置、隐藏没有改变,页面进行重绘。(同样,只重绘部分元素,而不是整个页面重绘)
- 回流的同时往往会伴随着重绘,重绘不一定导致回流。
- 回流导致的代价是大于重绘的
引起回流的原因
- 页面初始化渲染
- 调整窗口大小
- 改变字体
- 增加或者移除样式表
- 内容变化,比如用户在input框中输入文字
- ** CSS 伪类,比如 :hover (IE 中为兄弟结点伪类的**)
- 操作 class 属性
- 脚本操作 DOM
- 计算 width和 height属性
- 设置 style 属性的值
– 当获取一些属性时,浏览器为了返回正确的值也会触发回流,导致浏览器优化无效,有: 1. offset(top/bottom/left/right) 2. client (top/bottom/left/right) 3. scroll (top/bottom/left/right) 4. getComputedStyle() - 很多浏览器会对回流进行优化,一定时间段后或数量达到阕值时,做一次批处理回流。
频繁的回流与重绘会导致频繁的页面渲染,导致cpu或gpu过量使用,使得页面卡顿。
减少回流方法
- 不要通过父级来改变子元素样式,最好直接改变子元素样式,改变子元素样式尽可能不要影响父元素和兄弟元素的大小和尺寸
- 尽量通过class来设计元素样式,切忌用style多次操作单个属性
实现元素的动画,对于经常要进行回流的组件,要抽离出来,它的position属性应当设为fixed或absolute - 权衡速度的平滑。比如实现一个动画,以1个像素为单位移动这样最平滑,但reflow就会过于频繁,CPU很快就会被完全占用。如果以3个像素为单位移动就会好很多。
- 不要用tables布局的另一个原因就是tables中某个元素一旦触发reflow就会导致table里所有的其它元素reflow。在适合用table的场合,可以设置table-layout为auto或fixed,这样可以让table一行一行的渲染,这种做法也是为了限制reflow的影响范围。
- css里不要有表达式expression
- 减少不必要的 DOM 层级(DOM depth)。改变 DOM 树中的一级会导致所有层级的改变,上至根部,下至被改变节点的子节点。这导致大量时间耗费在执行 reflow 上面。
- 避免不必要的复杂的 CSS 选择器,尤其是后代选择器(descendant selectors),因为为了匹配选择器将耗费更多的 CPU。
- 尽量不要过多的频繁的去增加,修改,删除元素,因为这可能会频繁的导致页面reflow,可以先把该dom节点抽离到内存中进行复杂的操作然后再display到页面上。
- 请求如下值offsetTop, offsetLeft, offsetWidth, offsetHeight,scrollTop/Left/Width/Height,clientTop/Left/Width/Height,浏览器会发生reflow,建议将他们合并到一起操作,可以减少回流的次数。
- 使用硬件加速创建一个新的复合图层,当其需要回流时不会影响原始复合图层回流
硬件加速
我们在未开启硬件加速的时候是使用cpu来渲染页面,只有开启了硬件加速了,才会使用到GPU渲染页面。
在详细讲解硬件加速前,我们先来讲解一下简单图层和复合图层
- DOM中的每个结点对应一个简单图层 - 复合图层是各个简单图层的合并,一个页面一般来说只有一个复合图层,无论你创建了多少个元素,都是在这个复合图层中
- 其次,absolute、fixed布局,可以使该元素脱离文档流,但还是在这个复合图层中,所以他还是会影响复合图层的绘制,但不会影响重排当一个元素使用硬件加速后,会生成一个新的复合图层,这样不管其如何变化,都不会影响原复合图层。不过不要大量使用硬件加速,会导致资源消耗过度,导致页面也卡。
所以,使用了硬件加速后,会有多个复合图层,然后多个复合图层互相独立,单独布局、绘制。
使用硬件加速
- translate3d,translateZ
- opacity属性
硬件加速时请使用z-index
具体原理是这样的:
当一个元素使用了硬件加速,在其后的元素,若z-index比他大或者相同,且absolute或fixed的属性相同,则默认为这些元素也创建各自的复合图层。
所以我们人为地为这个元素添加z-index值,从而避免这种情况
浏览器页面的渲染流程
1.Browser进程下载html文件并将文件发送给renderer进程
2. renderer进程的GUI进程开始解析html文件来构建出DOM
3. 当遇到外源css时,Browser进程下载该css文件并发送回来,GUI线程再解析该文件,在这同时,html的解析也同时进行,但不会渲染(还未形成渲染树)
4. 当遇到内部css时,html的解析和css的解析同时进行
5. 继续解析html文件,当遇到外源js时,Browser进程下载该js文件并发送回来,此时,js引擎线程解析并执行js,因为GUI线程和js引擎线程互斥,所以GUI线程被挂起,停止继续解析html。直到js引擎线程空闲,GUI线程继续解析html。
6. 遇到内部js也是同理
7. 解析完html文件,形成了完整的DOM树,也解析完了css,形成了完整的CSSOM树,两者结合形成了render树
8. 根据render树来进行布局,若在布局的过程中发生了元素尺寸、位置、隐藏的变化或增加、删除元素时,则进行回流,修改
9. 根据render树进行绘制,若在布局的过程中元素的外观发生变换,则进行重绘
10. 将布局、绘制得到的各个简单图层的位图发送给Browser进程,由它来合并简单图层为复合图层,从而显示到页面上
11. 以上步骤就是html文件解析全过程,完成之后,如若当页面有元素的尺寸、大小、隐藏有变化时,重新布局计算回流,并修改页面中所有受影响的部分,如若当页面有元素的外观发生变化时,重绘
DOMContentLoaded和load事件
- DOMContentLoaded:当DOM加载完成触发
- load:当DOM,样式表,脚本都加载完时触发
DOMContentLoaded在load之前触发
css堵塞
首先,是在Browser进程中下载css文件,当下载完成后,发送给GUI线程。
其次,是在GUI线程中解析html及css,不过这两者是并行的。
由于css的下载和解析不会影响DOM树,所以不会堵塞html文件的解析,但会堵塞页面渲染。
这样的设计是非常合理的,如果css文件的下载和解析不会堵塞页面渲染,那么在页面渲染的途中或结束后发现元素样式有变化,则又需要回流和重绘
css位置
一般放在head中,因为css的解析不影响html的解析,所以越早引入,越早同时解析。
js堵塞
明确的是,js文件的下载和解析执行都会堵塞html文件的解析及页面渲染。
因为js脚本可能会改变DOM结构,若是其不堵塞html文件的解析及页面渲染的话,那么当js脚本改变DOM结构或元素样式时,会引发回流和重绘,会造成不必要的性能浪费,不如等待js执行完,在进行html解析和页面渲染。
如果你不想js堵塞的话,则使用async属性,这样就可以异步加载js文件,加载完成后立即执行。
宏任务和微任务
宏任务
- 主代码块和任务队列中的回调函数就是宏任务。
- 为了使js内部宏任务和DOM任务能够有序的执行,每次执行完宏任务后,会在下一个宏任务执行之前,对页面重新进行渲染。(宏任务 -> 渲染 -> 宏任务)
主代码块,setTimeout,setInterval,requestAnimationFrame等(任务队列中的所有回调函数都是宏任务)
微任务 - 在宏任务执行过程中,执行到微任务时,将微任务放入微任务队列中
- 在宏任务执行完后,在重新渲染之前执行
- 当一个宏任务执行完后,他会将产生的所有微任务执行完。
- process.nextTick,MutationObserver,Promise.then catch finally等