浏览器组件
- 界面控件(User Interface) :包括地址栏,前进后退,书签菜单等窗口上除了网页显示区域以外的部分
- 浏览器引擎(Browser engine) :查询与操作渲染引擎的接口
- 渲染引擎(Rendering engine) :负责显示请求的内容。比如请求到的HTML,它会负责解析HTML、CSS 并将结果显示到窗口中
- 网络(Networking) :用于网络请求,如HTTP请求。它包括平台无关的接口和各平台独立的实现
- UI后端(UI Backend) :绘制基础元件,如组合框与窗口。它提供平台无关的接口,内部使用操作系统的相应实现
- JS解释器(JavaScript Interpreter) :用于解析执行JavaScript代码
- 数据存储持久层(Data Persistence) :浏览器需要把所有数据存到硬盘上,如cookies。新的HTML5规范规定了一个完整(虽然轻量级)的浏览器数据库
web database
浏览器打开一个页面时的相关进程
最新的Chrome浏览器包括:1个浏览器主进程,1个GPU进程,1个网络进程,多个渲染进程,和多个插件进程
浏览器进程: 负责控制浏览器除标签页外的界面,包括地址栏、书签、前进后退按钮等,以及负责与其他进程的协调工作,同时提供存储功能GPU进程:负责整个浏览器界面的渲染。Chrome刚开始发布的时候是没有GPU进程的,而使用GPU的初衷是为了实现3D CSS效果,只是后面网页、Chrome的UI界面都用GPU来绘制,这使GPU成为浏览器普遍的需求,最后Chrome在多进程架构上也引入了GPU进程网络进程:负责发起和接受网络请求,以前是作为模块运行在浏览器进程里面的,后面才独立出来,成为一个单独的进程插件进程:主要是负责插件的运行,因为插件可能崩溃,所以需要通过插件进程来隔离,以保证插件崩溃也不会对浏览器和页面造成影响渲染进程:负责控制显示tab标签页内的所有内容,核心任务是将HTML、CSS、JS转为用户可以与之交互的网页,排版引擎 Blink 和 JS 引擎 V8 都是运行在该进程中,默认情况下Chrome会为每个Tab标签页创建一个渲染进程
渲染进程中的线程
我们平时看到的浏览器呈现出页面过程中,大部分工作都是在渲染进程中完成,包括我们的ui渲染引擎和js引擎。
GUI渲染线程:负责渲染页面,解析html和CSS、构建DOM树、CSSOM树、渲染树、和绘制页面,重绘重排也是在该线程执行。在Javascript引擎运行脚本期间,GUI渲染线程都是处于挂起状态的,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。JS引擎线程:一个tab页中只有一个JS引擎线程(单线程),负责解析和执行JS,处理页面中用户的交互,以及操作DOM树、CSS样式树。它GUI渲染线程不能同时执行,只能一个一个来,如果JS执行过长就会导致阻塞掉帧 GUI渲染线程与JS引擎线程互斥计时器线程:指 setInterval 和 setTimeout,因为JS引擎是单线程的,所以如果处于阻塞状态,那么计时器就会不准了,所以需要单独的线程来负责计时器工作异步http请求线程: XMLHttpRequest连接后浏览器开的一个线程,比如请求有回调函数,异步线程就会将回调函数加入事件队列,等待JS引擎空闲执行事件触发线程:主要用来控制事件循环,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,由于JS的单线程关系所有这些事件都得排队等待JS引擎处理。
浏览器页面渲染过程
1、解析HTML文件,构建DOM Tree
自上而下的深度遍历过程,会将当前结点的所有子节点构建完成后,进行当前结点的下一个兄弟结点的构建,直到将所有的标签都遍历完成。遇到任何样式(link、style)与脚本(script)都会阻塞(外部样式不阻塞后续外部脚本的加载)
2、解析CSS,构建CSSOM Tree(css规则树)
给各个元素添加对应样式信息,计算在浏览器中占据的空间大小
3、将DOM Tree和CSSOM Tree合并,构建Render tree(渲染树)
结合DOM数和CSSOM树,生成一颗渲染树的过程称为Attachment。是页面可视化元素按照其显示顺序组成的树,可以让浏览器按照正确的顺序绘制内容。
4、生成布局(flow),浏览器在屏幕上“画出”渲染树中所有的结点
根据Render Tree进行结点信息计算。浏览器渲染页面默认采用流式布局,从根(对应于HTML文档的元素)开始,然后递归遍历部分或所有的框架层次结构,为呈现器计算几何信息。
5、将布局绘制(paint)在屏幕上,显示出整个页面
根据计算好的信息绘制整个页面(painting)
在浏览器渲染中重绘(repaint)和回流(reflow)
回流(reflow):当渲染树的一部分必须更新并且节点的尺寸发生了变化,浏览器会使渲染树中受到影响的部分失效,并重新构造渲染树。
重绘(repaint):是在一个元素的外观被改变所触发的浏览器行为,浏览器会根据元素的新属性重新绘制,使元素呈现新的外观。比如改变某个元素的文字颜色、背景色等。
重绘不一定会引发回流,回流必然导致重绘。
JS阻塞渲染
在解析html文件生成dom树的过程中,如果遇到<script>标签,会去下载对应的js,并执行。在js执行完毕前,html的继续解析过程将阻塞,当然dom树的生成也没有完成。
16ms渲染帧
- 屏幕刷新率:屏幕每秒钟刷新的图片数,一般设备60张/s。
- 帧数(fps):显卡每秒能提供的图片数。 理论上来说,显卡每秒提供的图片数等于屏幕每秒需要的图片数时是比较理想的状态。假设刷新率为60/s,显卡提供的多于60,就会舍弃部分图片,少于60就会造成卡顿,看上去不连续。所以按每秒提供60张图算,一帧的时间大概是16ms。
任意一帧的生成方式,有重排、重绘和合成三种方式。
-
相较于重排和重绘,合成操作的路径就显得非常短了,并不需要触发布局和绘制两个阶段,如果采用了 GPU,那么合成的效率会非常高。
-
合成分为分块、分层和合成。
-
分层体现在生成布局树后,渲染引擎会根据布局树的特点将其转换为层树(Layer Tree)。 层树中的每个节点都对应着一个图层,下一步的绘制阶段就依赖于层树中的节点。例如一些css动画,每次动画都要重启渲染主线程,重排,重绘,开销未免也太大了些。将有动画的元素分层后,每一帧只要改动动画层就好,css的will-change属性就是提前告诉渲染引擎,让这个元素单独一层的。这就是css动画效率高于js动画的根本原因,但单独一层需要用到内存,所以单独层的数量不能太多,否则对内存是会有较大影响的。
-
通常情况下,页面的内容都要比屏幕大得多。利用分块机制,可以优先渲染离显示内容近的部分,特别是在滑轮滑动场景中,就不需要重启渲染主线程,只需重新拼一下图块就好了。
-
合成操作是在合成线程上完成的,这也就意味着在执行合成操作时,是不会影响到主线程执行的。
一个完整帧的过程:
在一帧数据的渲染过程中,当js执行时间过长,导致阻塞,一个帧的执行时间就会超过16ms,这时就会产生丢帧。一般像合成操作,是不会开启渲染主线程的,渲染时间就会比较短,requestIdleCallback可用的空闲时间也会比较多。
我们可以发现,浏览器一帧里回调的执行顺序为:
- 用户事件:最先执行,比如click等事件。
- js代码:通常处理定时器回调js。
- 在渲染前执行,scroll/resize 等事件回调。
- 在渲染前执行,requestAnimationFrame回调。
- 渲染界面:html解析、css的计算布局绘制等都是在这里完成(回流、重绘)。
- requestIdleCallback执行回调:如果前面的那些任务执行完成了,一帧还剩余时间,那么会调用该函数。
Event-loop事件循环
首先明确一点,这个玩意实际是比较复杂的,对于大多数前端来说是黑盒般的存在。
按照最新的规范,已经没有宏任务这一说了,官方说法叫Task(宏任务)和job(微任务)。
Task -> Jobs -> UI渲染
1.Task有很多不同优先级的任务队列(queue),高优先级queue空了,再执行低queue里的task。
2.Jobs的执行,可以看作Task的一部分,每执行完一个Task后,都会执行完job队列里的所有job。
3.Task:
- HTML parsing时执行的的js主代码块(第一个Task)
- 用户交互产生的I/O(按钮点击,文件上传等都算)
- setTimeout
- requestAnimationFrame
4.Job
- Promise
- Object.observe
- MutationObserver
结合帧渲染看Event-loop
- 首先UI渲染和事件循环并没有必然关系,一个Task+jobs才是真正意义上的一轮事件循环,Task -> Jobs -> UI渲染这个流程并不完全正确,浏览器是非常智能的,如果Task执行时间短,两个UI渲染间是完全可以插入多个Task的。
- 60fps时,一帧16ms,在这16ms中,如果浏览器发现UI渲染不会带来视觉上的更新,且没有requestAnimationFrame回调时。这一帧大概率是不会进行主流程的UI渲染,而是通过合成的方式绘制渲染图。
- 结合上图,16ms中最先执行的是用户交互回调,也就是说交互Task队列优先级很高。比如用户一次点击,由于冒泡,里层、外层两个回调函数都要执行,它们属于两个Task,这两个Task按优先级里层先执行,然后是jobs。然后外层Task+jobs。一般情况下,交互队列的Task任务在此阶段都会被执行完,才会往下走。以此看来,UI渲染前已经经历了多轮loop循环了。
- 接下来是Times,也就是setTimeout的Task队列执行,这个队列优先级低于交互队列,自然后面一点,但这个Task队列是不一定会被清空的,会根据每个Task的执行时间让浏览器自行决定。
- 再后面的就被划分到渲染部分了,也就是说只有触发了UI渲染才会执行后面的这部分,比如scroll和resize的回调,其实屏幕视图应该早就随着用户的操作而变化了,这里只是触发回调而已,因为一秒最多60帧,所以其实它们相当于自带了防抖/节流。从上文得知滑轮滚动更新用户视图主要靠的的分块和合成,绝大多数情况下都是不用启动渲染主线程的(大多数情况下都是没Task和滚动回调的)。而且scroll的回调也是属于一种Task,因为如果回调中产生了jobs,也会在Task执行完后立马清空jobs队列。
- requestAnimationFrame,它只会在UI渲染前执行,但是如果前面的js耗时太长,浏览器为了尽快更新UI可能选择不执行本轮requestAnimationFrame,也就是说它的执行次数是小于等于UI渲染次数的。一般来说有了requestAnimationFrame回调,每帧都会进行UI渲染,不会选择跳过,所以它是非常适合用来做动画的。同样的它也是一种Task,因为同样会清空Jobs。你在一帧中写了两个requestAnimationFrame回调,他会执行两轮标准的事件循环。
- requestIdleCallback,是为了在一帧中空闲的时候去做一些事,优先级很低。它是在页面渲染帧提交完成后才可能进行的,但是它有个特性,在浏览器空闲时(没有交互,滚动等导致画面改变的操作时),它的执行时间可能被延长到50ms(实际就是为了更好的执行它,稍微降帧了)。当滚动滑轮时,哪怕是合成渲染这种,浏览器也会保持16ms一帧来给用户好的体验,那么它的时间就不会有50ms了。