浏览器的发展史:
1991年 | Berners Lee 建立了第一代网络浏览器 World Wide,只支持显示文本图片。 |
1993年 | Micsoic,支持文本和图像 。 |
1994年 | 创建Micsoic的团队发布网景浏览器,支持简单的html没有js,css。 同年,opera发布。 |
1995年 | 微软发布IE1.0,IE2.0。于此,浏览器大战正式打响 。 |
1996年 | IE3.0和windows集成在一起。而此时网景的市场份额已经达到了80%。 |
1998年 | 网景工程师成立了Mozilla基金会。 |
1999年 | IE市场份额达到了99%。 |
2003年 | 苹果发布safari并包括在所有的苹果操作系统中。 |
2004年 | 由网景工程师成立的Mozilla基金会发布了火狐1.0版本。第二次浏览器大战打响 |
2005年 | 苹果开源了safari浏览器的内核webkit |
2008年 | 谷歌以苹果开源项目webkit作为内核创建了一个新的项目Chromium,在该项目基础上发布了自己的浏览器产品Chrome |
2015年 | 由于IE逐渐掉队,微软放弃IE推出基于webkit的edge浏览器作为IE的替代品 |
Chorme目前后来居上。
浏览器结构:
浏览器内核:
IE => Trident。
FireFox => Gecko。
Safari => Webkit,并开源。
Chrome/Opera/Edge =>Blink,基于Webkit改造优化,开源。
进程与线程
进程:
是操作系统进行资源分配和调度的基本单位,可以申请和拥有计算机资源,进程是程序的基本执行实体。
线程:
是操作系统能够进行运算调度的最小单位。一个进程可以并发多个多个线程,多个线程可以并发执行不同的任务。
换一种说法,当我们启动某个程序时,就会创建一个进程来执行任务代码,同时会为该进程分配内存空间,该应用程序的状态都保存在该内存空间里,当应用关闭时,该内存空间就会被回收,进程可以启动更多的进程来执行任务,由于每个进程分配的内存空间是独立的,如果两个进程间需要传递某些数据,则需要通过进程间通信管道IPC来传递。
很多应用程序都是多进程的结构,这样是为了避免某一进程卡死,由于进程间相互独立,这样不会影响到整个应用程序。
进程可以将任务分成更多细小的任务,然后通过创建多个线程并行执行不同的任务,同一进程下的线程之间是可以直接通信共享数据的。
早期浏览器是但进程的,该进程的特点:不稳定,不安全,不流畅。
Chrome浏览器的进程:
浏览器进程:
负责控制Chrome浏览器除标签外的用户界面,包括地址栏,书签,后退和前进按钮,以及负责与浏览器的其他进程协调工作。
网络进程:
负责发起接受网络请求。
GPU进程:
负责整个浏览器界面的渲染。
插件进程:
负责控制网络使用的所有插件,例如flash,这里的插件并不是指Chrome市场里安装的扩展。
渲染器进程:
用来控制显示tab标签内的所有内容。
缓存进程
浏览器在默认情况下会为每个标签页都创建一个进程。
Chrome浏览器工作过程
当你在地址栏输入地址时,浏览器进程的OI线程会捕捉你的输入内容。如果访问的是网址,则UI线程会启动一个网络线程来请求DNS进行域名解析,接着开始连接服务器获取数据,如果你的输入不是网址而是一串关键词,浏览器就知道你是要搜索,于是就会使用默认设置的搜索引擎来搜索。
网络线程获取数据之后会发送什么样的事情?
1.当网络线程获取数据后,会通过SafeBrowing来检查站点是否是恶意站点。如果是,则会提示一个警告页面,告诉你这个站点有安全问题,浏览器会阻止你的访问,当然你也可以强行继续访问。SafeBrowsing是谷歌内部的一套站点安全系统,通过检测该站点的数据来判断是否安全,比如通过查看该站点的IP是否在谷歌的黑名单之内。
2.当返回数据准备完毕并且安全检测通过时,网络线程会通知UI线程我就要准备好了,该你了。然后UI线程会创建一个渲染器进程(Renderer Thread)来渲染页面。
浏览器进程通过IPC管道传递给渲染器进程,正式进入渲染流程。
渲染器进程接到的数据也就是html,渲染器进程的核心任务是将html,css,js,image等资源渲染成用户可以交互的web页面。
3.渲染器进程的主线程将html进行解析,构造Dom数据结构。
Dom也就是文档对象模型,是浏览器对页面在其内部的表示方法,是web开发程序员可以通过JS与之交互的数据结构和API。
html首先通过tokeniser标记化,通过词法分析将输入的html内容解析成多个标记,根据识别后的标记进行Dom树构造。
在Dom树构造过程中会创建document对象,然后以document为根节点不断修改,向其中添加各种元素。
html代码中往往会引入一些额外资源,比如图片,css,js脚本等。图片和css这些资源需要通过网络下载或者从缓存中直接加载,这些资源不会阻碍html解析,因为他们不会影响Dom的生成。
但当HTML解析过程中遇到script标签,就会停止解析流程,转而去加载解析并且执行JS.
为什么不直接跳过JS的加载和执行,等html解析完后再加载运行JS呢?
这是因为浏览器并不知道JS执行是否会改变当前页面的HTML结构。如果JS代码里用了document.write方法来修改html,之前的html解析就没有任何意义了。这也是问什么要把script标签放在合适的位置,或者使用async或defer属性来异步加载执行。
4.在html解析完成后,我们就会获得一个Dom Tree,但我们还不知道Dom树上的每个节点应该长什么样子,主线程需要解析css并确定每个Dom节点的计算样式,即使你没有自定义的css的样式,浏览器也会有自己的默认样式表,比如h2字体要比h3字体大。
5.在知道Dom结构和每个节点的样式后,我们需要知道每个节点需要放在哪个位置,也就是节点的坐标以及该节点需要占用多大的区域,这个阶段被称为layout布局布局,主线程通过遍历Dom和计算好的样式来生成Layout Tree,Layout Tree上的每个节点都记录了x,y坐标和边界尺寸。
这里需要注意的一点是Dom Tree和Layout Tree并不是一一对应的,设置了display:none 的节点不会出现在Layout Tree 上。而在before伪类中添加了content值的元素,content里的内容会出现在Layout Tree 上,不会出现在Dom树里。这是因为Dom树是通过HTML解析获得,并不关系样式,而Layout Tree是根据Dom树和技术好的样式来生成的,Layout Tree是和最后展示在屏幕上的节点是对应的。
6.为了在屏幕上展示正确的层级,主线程遍历Layout tree 创建一个绘制记录表(Paint Record),该表记录了绘制的顺序,这个阶段被称为绘制(paint)。
7.Chorme最早使用了一种简单的方式。只栅格化用户可视区域(Viewport)的内容。当用户滚动页面时,再栅格化更多的内容来填充缺失的部分,会导致显示延迟。
随着不断地升级,现在的Chorme使用了一种更为复杂的栅格化流程,叫做合成(Composisting ),合成是一种将页面的各个部分分成多个图层,分别对其进行栅格化,并在合成器线程( Compositor Thread )单独进行合成页面的技术,简单来说就是页面所有的元素按照某种规则进行分图层,并把图层都栅格化好了,然后只需要把可视化的内容组合成每一帧展示给用户即可。
8.主线程遍历Layout Tree 生成 Layer( 图层 )Tree。当 Layout Tree 生成完毕和绘制顺序确定以后,主线程就将这些信息传递给合成器线程。
9.合成器线程将每个图层栅格化,由于一层可能像页面的整个长度一样大,因此合成器线程将他们切分成许多图块,然后将每个图块发送给栅格化线程(Raster Thread),栅格化线程栅格化每个图块,并将他们存储在GPU内存中。
10.当图画栅格化完成后,合成器线程将收集称为 “draw quads” 的图块信息, 这些信息里记录了图块中的位置和在页面的哪个位置绘制图块信息,根据这些信息合成器线程生成了一个合成器帧,然后合成器Frame帧通过IPC传送给浏览器进程。
11.接着浏览器进程将合成器帧传送给GPU,然后GPU渲染展示到屏幕上。
12.当你滚动页面会生成一个新的合成器帧,新的帧再传送给GPU,然后再次渲染到屏幕上。
总结:
浏览器进程中的网络线程请求获取到html数据后,通过IPC将数据传给渲染器进程的主线程。主线程将html解析构造DOM树,然后进行样式计算,根据DOM树和生成好的样式生成Layout Tree,通过遍历Layout Tree生成绘制顺序表,接着遍历Layout Tree 生成Layer Tree,然后主线程将Layer Tree和绘制顺序信息一起传给合成器线程,合成器线程按规则进行分图层,并把图层分成更小的图块(title)传给栅格化线程进行栅格化,栅格化完成后,合成器线程会获得栅格化传过来的 “draw quads” 图块信息,根据这些信息合成器线上合成一个合成器帧,然后将该合成器帧通过IPC传回给浏览器进程。浏览器进程再传到GPU进行渲染,之后就展示到你的屏幕上了。
当我们改变一个元素的尺寸属性时,会重新进行样式计算(Computed Style) 布局(Layout)绘制(Paint) 以及后面的所有流程,这种行为我们称为重排,当我们改变某个元素的颜色属性时,不会重新触发布局,但还是会触发样式计算和绘制,这就是重绘。
我们可以发现重排和重绘都会占用主线程,还有另外一个东西运行在主进程上——JS。他们都在主线程运行,就会出现抢占执行时间的问题。
如果你写了一个不断重排重绘的动画,浏览器则需要在每一帧都运行样式,计算布局和绘制的操作。我们知道当页面以每秒30帧的刷新率时,才不会让用户感觉到页面卡顿。如果你在运行动画时还有大量的JS任务需要执行。因为布局,绘制和JS执行都是在主线程运行的,当在一帧的时间内布局和绘制结束后,如果还有剩余时间,JS就会拿到JS没有及时归还的主线程,导致下一帧动画没有按时渲染,就会出现页面动画卡顿。
优化手段:
法一:
requestAnimationFrame(),这个API帮助我们解决这个问题,requestAnimationFrame() 这个方法会在每一帧被调用,通过API回调,我们可以把JS运行任务分成一些更小的任务块(分到每一帧),在每一帧时间用完前暂停JS执行,归还主线程。
React的渲染引擎React Filter 用到了api来做了很多优化。
法二:
因为栅格化不占用主线程,只有合成器线程和栅格线程中运行,无需和JS抢主线程,反复重绘,重排可能导致掉帧,这是因为有可能JS执行阻塞了主线程,css中有个动画属性transform,通过该属性实现的动画不会经过布局和绘制,而是直接运行在合成器线程和栅格线程,同时节省了很多运算时间(方便实现负责的动画)。
Position transform:translate(x,y)
top,bottom,left,right
Scale transform:scale(x,y)
width,height
Rotation transform:rotate(deg)