浏览器渲染机制(记录)

浏览器进程

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请求就开一个该线程
  • 当检测到状态变更的话,就会产生一个状态变更事件,如果该状态变更事件对应有回调函数的话,则放入任务队列中

任务队列轮询线程

  • 用于轮询监听任务队列,以知道任务队列是否为空

js总体机制

宏任务和微任务

宏任务

  • 主代码块和任务队列中的回调函数就是宏任务。
  • 为了使js内部宏任务和DOM任务能够有序的执行,每次执行完宏任务后,会在下一个宏任务执行之前,对页面重新进行渲染。(宏任务 -> 渲染 -> 宏任务)
    主代码块,setTimeout,setInterval,requestAnimationFrame等(任务队列中的所有回调函数都是宏任务)

微任务

  • 在宏任务执行过程中,执行到微任务时,将微任务放入微任务队列中
  • 在宏任务执行完后,在重新渲染之前执行
  • 当一个宏任务执行完后,他会将产生的所有微任务执行完。
  • process.nextTick,MutationObserver,Promise.then catch finally等
console.log('script start')

setTimeout(function() {
    
    
  console.log('setTimeout')
}, 0)

new Promise(resolve => {
    
    
  console.log('Promise')
  resolve()
})
.then(function() {
    
    
  console.log('promise1')
})
.then(function() {
    
    
  console.log('promise2')
})

console.log('script end')
// script start => Promise => script end => promise1 => promise2 => setTimeout

以上代码虽然 setTimeout 写在 Promise 之前,但是因为 Promise 属于微任务而 setTimeout 属于宏任务,所以会有以上的打印。

很多人有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务。

Event loop 顺序

  • 执行同步代码,这属于宏任务
  • 执行栈为空,查询是否有微任务需要执行
  • 执行所有微任务
  • 必要的话渲染 UI
  • 然后开始下一轮 Event loop,执行宏任务中的异步代码

通过上述的 Event loop 顺序可知,如果宏任务中的异步代码有大量的计算并且需要操作 DOM 的话,为了更快的 界面响应,我们可以把操作 DOM 放入微任务中。

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布局,可以使该元素脱离文档流,但还是在这个复合图层中,所以他还是会影响复合图层的绘制,但不会影响重排当一个元素使用硬件加速后,会生成一个新的复合图层,这样不管其如何变化,都不会影响原复合图层。不过不要大量使用硬件加速,会导致资源消耗过度,导致页面也卡。
     所以,使用了硬件加速后,会有多个复合图层,然后多个复合图层互相独立,单独布局、绘制。

    使用硬件加速
  1. translate3d,translateZ
  2. 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文件,加载完成后立即执行。

猜你喜欢

转载自blog.csdn.net/weixin_39308542/article/details/107197215
今日推荐