浏览器组成
用户界面 :展示除标签页窗口之外的其他用户界面内容
浏览器引擎:在用户界面和渲染引擎之间传递数据,具有数据存储持久层(存储cookie等各种数据)
渲染引擎(内核):负责渲染用户请求的页面内容,包括许多小的模块,如网络模块、js解释器
主流浏览器内核
IE | Firefox | Safari | Chrome | Opera |
---|---|---|---|---|
Trident | Gecko | Webkit | Webkit-->Blink | Blink |
进程(process)与线程(thread)
当启动一个程序时,就会创建一个进程来执行任务代码,同时为该进程分配内存空间,该应用程序的状态都保存在该内存空间里,应用程序关闭则回收内存空间。可以启用多个进程来执行任务,每个进程相互独立,一个进程出现故障,不会影响其他进程,数据通过进程间通信管道IPC来传递。
进程可以将任务再分为更小的线程,多个线程并行执行不同的任务,同一进程下的多个线程是可以通信共享数据的。
-
多进程结构
浏览器进程
控制浏览器除标签页外的用户界面,包括地址栏、书签栏、前进后退按钮,以及负责与浏览器的其他进程协调工作缓存进程
网络进程
发起接受网络请求GPU进程
负责整个浏览器页面的渲染插件进程
负责网站使用的所有插件,如flash(并不是浏览器商店安装的扩展)渲染器进程(Render Thread)
控制显示tab标签内所有内容,浏览器默认情况下会为每个标签页都创建一个进程(由进程模式决定,可修改),核心任务就是把html、css、js、image等资源渲染为用户可以交互的web页面
HTML是如何渲染到页面上的(chrome为例)
后面有整个过程的简单总结
1.渲染前阶段
当你在浏览器地址栏输入地址时,浏览器进程的UI线程会捕捉输入内容,若输入为网址,UI线程则会启动一个网络线程发送请求进行 DNS域名解解,开始连接服务器,服务器将数据返回。
网络线程获取到数据后,(会经过谷歌内部的一套站点安全系统SafeBrowsing来检查站点是否为恶意站点)当返回数据准备完毕时,网络线程会通知UI线程已经准备好。然后UI线程会创建一个渲染器进程来渲染页面,浏览器进程通过IPC管道将数据传递给渲染器进程,进入渲染流程。
2.渲染阶段
2.1. 生成DOM结构树
渲染器进程接收到html数据,在内存中开辟一块栈内存(stack)来给代码的执行提供环境。渲染器进程解析执行html数据,构造DOM数据结构,HTML首先经过Tokeniser标记化,通过词法分析将输入的html内容解析成多个标记,根据识别后的标记,进行DOM树构造,构造过程中创建document对象,然后以document为根节点的DOM树不断进行修改,向其中添加各种元素。
html代码中往往会引入一些额外资源,图片、css这些资源需要通过网络下载或者从缓存中直接加载,这些资源不会阻塞html的解析,因为其不会影响DOM的生成,但当遇到script标签,将停止html解析流程,转而去加载解析并执行js,因为js有可能会改变当前的页面结构(所以script标签要放在合适的位置,或使用async或defer属性来异步加载执行)。
2.2 生成CSS样式树
主线程再去解析css并确定每个DOM节点的计算样式生成CSS样式树,即使没有提供自定义css样式,浏览器也会有自己默认的样式表。
2.3 Layout布局,生成Render Tree
在得到dom树结构和节点样式之后,还需要知道每个节点要放置在页面上的哪个位置,即坐标和边框尺寸。主线程通过遍历dom树和css样式树生成Layout Tree(Render Tree)。
DOM Tree 和 Layout Tree并不是一一对应的,如:设置了
display:none
的节点不会显示在Layout Tree上,而在before
属性添加了content
的元素,content中内容会出现在Layout Tree上,而不会出现在DOM Tree中,因为DOM是通过html解析获得,并不关系样式,Layout Tree则是根据DOM和计算好的样式生成,Layout Tree和最后显示在屏幕上的节点是对应的。
2.4 绘制
经过前面步骤后,我们已经知道的每个节点的位置、大小和样式,下面还需要指定以什么样的顺序绘制(paint)节点,比如z-index属性会影响节点的绘制层级关系,若我们按照DOM的层级结构绘制,则会导致错误的渲染。因此主线程遍历Layout Tree创建一个绘制记录表(paint record),记录节点绘制的顺序,此阶段成为绘制。
主线程遍历Layout Tree生成Layer Tree并确定绘制顺序后,将这些信息传递给合成器线程,合成器线程按照某种规则分图层,将每个图层栅格化,由于一层可能像页面的整个长度一样大,因此合成器线程将它们切分为许多图块(titles)发送给栅格化线程(raster thread)进行栅格化,之后存储在GPU内存中。
当图块栅格化完成后,合成器线程将收集“draw quads”图块信息,这些信息里记录了图块在内存中的位置和在页面上的绘制位置,根据这些信息合成器线程生成了一个合成器帧,然后合成器帧通过IPC传送给浏览器进程,接着浏览器进程将合成器帧传送给GPU渲染展示到屏幕上。
2.5 当页面发生变化时
页面滚动时,则会生成一个新的合成器帧传给GPU,再次渲染到屏幕上。
当改变尺寸位置属性时,会重新进行样式计算,布局绘制以及后面的所有流程,这种行为称为重排(回流)。
当改变颜色等其他属性时,不会重新触发布局,但依旧会触发计算样式,称为重绘。
总结:
- 浏览器进程中的网络线程获取到html数据后,通过IPC通信管道传递给渲染进程的主线程。
- 主线程:
解析html文档构造DOM树,然后进行样式计算,根据DOM树和计算好的样式生成layout tree,通过遍历layout tree生成绘制顺序表,接着遍历layout tree生成layer tree,然后主线程将layer tree和绘制信息一起传递给合成器线程。- 合成器线程:
按规则进行分图层,并把图层分为更小的图块(tiles)传递给栅格化线程。- 栅格化线程:
将图块进行栅格化,栅格化完成后获得“draw quads”图块信息,传回给合成器线程。- 合成器线程:
根据图块信息合成了一个合成器帧,将该合成器帧通过IPC传回给浏览器进程。- 浏览器进程再将合成器帧传递给GPU进行渲染,最后展示到页面上。
性能优化
重排和重绘都会占用主线程,同时js也运行在主线程,就会出现抢占时间的问题。
当屏幕刷新率为每秒60帧(16ms)时才不会感到卡顿,若运行动画时还有大量的js任务要执行,当在一帧的时间内布局和绘制结束后还有剩余时间,js就会拿到主线程的使用权,而当js执行时间过长时就会导致在下一帧开始js没有及时归还主线程,导致下一帧动画没有按时渲染出现卡顿。
setInterval定时器动画的缺点:
- 任务被放在异步队列,只有当主线程 任务执行完后才会执行异步队列中的任务,因此执行事件总比设置的事件要晚。
- 定时器设定的事件间隔不一定与屏幕刷新率相同,若屏幕刷新率为每秒60帧,而定时器每秒执行60次以上,那么必然会丢帧,引起卡顿。
1. requestAnimationFrame()
此方法要求浏览器在下次重绘之前调用指定的回调函数更新动画,刷新频率为每秒60帧,会在每一帧准时调用。通过api的回调,可以把JS运行任务分割成一些更小的任务块,在每一帧时间用完前暂停js执行,归还主线程。
使用:
function move(){
var box = document.querySelector('.box');
box.style.left = box.offsetLeft + 20 + 'px';
if(box.offsetLeft < 700){
window.requestAnimationFrame(move);
}
}
move();
返回值为一个整数,请求 ID ,是回调列表中唯一的标识。是个非零值,没别的意义。你可以传这个值给window.cancelAnimationFrame()
以取消回调函数。
2. CSS3硬件(GPU)加速
transform/opcity/filter...属性会触发硬件加速,不会引发重排和重绘。
通过transform属性实现的动画直接运行在合成器线程和栅格化线程,不会受到主线程中js执行的影响。更重要的是,transform动画不经过重绘重排和样式计算,所以节省了很多运算时间,方便实现复杂的动画。
(GPU加速 标准方法:will-change
will-change: transforn; 提前告诉浏览器将关于transform的属性在单独的层变换)
3. 分离读写操作(浏览器的渲染队列机制)
box.style.width = '200px';
box.style.height = '200px';
box.style.margin = '10px';
console.log(box.clientWidth);
上述代码只触发一次回流,但若将console.log(box.clientWidth);
插入到写入样式之间,则会触发两次,会触发刷新渲染队列的还有offTop、offWidth、clientTop、clientWidth、scrollTop、scrollWidth、getComputedStyle、currentStyle
...
4.样式集中改变
box.style.cssText = 'width:20px;height:20px';
box.className = 'box';
5. 元素批量添加
- 使用文档片段:内存中临时保存多个平级子元素的虚拟父元素,通过createDocumentFragment()创建,其并不在DOM树中,所以将子元素插入到文档片段时不会引起页面回流
var frag = document.createDocumentFragment();
for(var i = 0;i < 5;i++){
var li = document.createElement('li');
frag.appendChild(li);
}
box.appendChild(frag);
- 模板字符串拼接
var str = '';
for(var i = 0;i < 5;i++){
str += '';
}
box.innerHTML = ` ${str} `
6. 避免table布局和css的JavaScript表达式
参考视频:《浏览器是如何运作的?》