进程:一个进程就是一个程序的运行实例。启动一个程序的时候,操做系统会为该程序建立一块内存,用来存放代码、运行中的数据和一个执行任务的主线程。html
线程:线程不能单独存在,须要由进程来启动和管理。算法
进程和线程的特色:canvas
浏览器全部功能模块都运行在同一个进程里,这些模块包括网络、插件、js运行环境、渲染引擎和页面。带来的问题:浏览器
早期多进程架构中,页面运行在单独的渲染进程中,插件也是单独的插件进程,进程之间经过IPC通讯。这就解决了单进程带来的问题:进程间相互隔离,不会互相影响,也不会互相阻塞,并且插件进程和渲染进程使用了安全沙箱,程序在沙箱中运行,不能在硬盘上读写数据。缓存
目前最新的Chrome进程架构图:安全
包括:1个浏览器(Browser)主进程、1个GPU进程、1个网络(NetWork)进程、多个渲染进程、多个插件进程。因为渲染进程和插件进程须要运行用户代码,所以安全起见,被隔离在沙箱中。各进程功能:性能优化
多进程模型的缺点:服务器
确切地说,有链接关系的同一站点(协议相同、根域名相同)会共用一个渲染进程。 如下方法可使两个标签页进行链接:markdown
<a href="./b.html" target="_blabk"> B </a>
打开B标签页,此时B标签页中能够经过window.opener访问A的window。这种有链接关系的标签页叫作浏览上下文组,Chrome浏览器会将浏览上下文组中属于同一站点的标签页分配到同一个渲染进程中。但也有例外的状况:网络
<a target="_blank" ref="noopener noreferrer">
,其中noopener会将新开标签页的opener值设为null,noreferrer表示新开的标签页不要有引用关系。这就使得新开标签页和当前标签页不属于同一个浏览上下文组了。从用户发出url请求到页面开始解析的过程叫作导航。
从输入url到页面展现的流程:
确认提交后,导航流程结束,开始进入渲染流程。
渲染进程会先建立一个空白页面,叫作解析白屏。以后开始页面解析和子资源加载。
因为浏览器没法理解HTML,所以须要将HTML解析为浏览器能理解的结构——DOM树。DOM是保存在内存中的树结构,DOM是生成页面的基础数据结构,给JavaScript脚本提供一套查询和改变文档结构、样式、内容的接口,也是一道安全防御线,一些不安全的内容在DOM解析阶段就被排除了。
渲染引擎内部有个HTML解析器的模块,负责将HTML字节流转换为DOM结构。解析过程是渐进的,网络进程加载了多少数据,HTML解析器就解析多少数据。渲染进程从以前和网络进程创建的数据管道的一端动态接收字节流,并将其解析为DOM。
经过分词器将字节流转换为Token,分为Tag Token和文本Token。
将Token解析为DOM节点,而后将节点添加到DOM树中。HTML解析器维护了一个Token栈结构,若是是StartTag Token则入栈,并建立一个DOM节点添加到DOM树中(HTML解析器最初会默认建立一个根为document的空DOM结构);若是是文本Token则生成文本节点添加到DOM树中;若是是EndTag Token则检查栈顶是不是相同标签的StartTag Token,是则出栈。
解析过程当中遇到了script标签时,HTML解析器暂停工做,JavaScript引擎介入,执行脚本,此时只能访问位于script之上已经构建了的DOM。访问后面的元素会返回null,执行操做会报错。脚本执行完,HTML解析器恢复解析。
若是不是内嵌脚本,而是经过src加载的脚本(有src属性的脚本会忽略标签内的代码),会等待下载完成后执行,期间HTML解析器一直是暂停的状态。不过好在Chrome浏览器的预解析操做会在渲染引擎收到字节流以后开启一个预解析线程分析HTML文件中包含的JavaScript、CSS等相关文件而后提早下载这些文件。
在有src属性加载脚本的script标签中添加async属性代表这个脚本是异步的,在后台加载,HTML解析器继续工做,等脚本加载完成后当即打断HTML解析器(若是还没解析完的话)开始执行脚本。 添加defer属性也会异步加载,可是会等到HTML解析完毕,在DOMContentLoaded事件以前执行。这两个属性对无src属性的脚本不会生效。
对于CSS,因为不直接参与DOM构建,原本是不会阻塞DOM树的构建的。可是若是页面有同步执行的脚本时,由于执行前是不知道脚本有没有操做CSSOM的,所以渲染引擎为了不脚本执行出错的可能性,直接假定脚本会依赖CSSOM。因而下载CSS文件并解析成CSSOM,再执行脚本。期间HTML解析器一直是暂停的状态,直到脚本执行完毕才继续工做。
页面无脚本时CSS不会阻塞DOM树的构建:
页面有同步脚本时CSS会阻塞DOM树的构建:
页面有异步脚本时,会继续构建DOM树,脚本执行前须要等待CSS。是否继续渲染则须要看CSS是否就绪,若是CSS还在加载中则须要等待。
若是是defer脚本,CSS加载完就开始渲染,只是DOMContentLoaded和load事件会延迟触发;
若是是async脚本,DOMContentLoaded正常触发,CSS加载完就开始渲染,load事件会等待脚本执行完再触发:
总之CSS阻塞DOM树的构建与否取决于脚本执行的时机。
样式计算是为了计算出DOM节点中每一个元素的具体样式,这个阶段大致能够分为三个步骤:
此阶段用来计算DOM树中可见元素的几何位置。有两个步骤:
因为页面中可能包含不少复杂的效果如3D变换、页面滚动、z-index的z轴排序等,为了不每次改动都要引起整个页面重排或重绘,会将页面分红多个图层,并生成一颗对应的图层树(LayerTree),层树中的每一个节点都对应一个图层,这些图层按照必定顺序叠加在一块儿(层合成composite)构成了最终的页面图像。
一般,并不是布局树的每一个节点都占用一个图层,没有图层的节点将会从属于父节点的图层,最终每一个节点都会直接或间接包含在一个图层中。处于相同的z轴坐标空间时,就会造成一个图层(或渲染层RenderLayers)。
渲染引擎会为一些特定的节点建立单独的图层:
若是一个元素覆盖在一个拥有独立图层的元素上,而且这个元素的z-index值更大,那这个元素也会被提高到单独的图层中。
像上面那种隐式建立图层的状况,若是不加注意,极端场景下可能会建立大量独立图层,也就是层爆炸,会占用GPU和大量的内存资源,严重损耗页面性能。浏览器也有相应的对策,浏览器的层压缩机制,会将隐式建立的多个图层压缩到一个图层中。
z-index:3这个元素是一个独立的图层,其后z-index值更大的三个元素被压缩进了一个图层中:
可是不少特定状况下,浏览器是没法进行层压缩的。例如若是页面的某一部分使用transform和animation制做动画效果,因为可能产生动态交叠的状况,隐式建立图层在不须要交叠的状况下也能发生,致使页面中全部z-index高于它的节点都被提高到单独的图层中,使得页面产生了大量的独立图层。消除的办法就是增大z-index的值,或者合理优化页面元素的结构。
获得图层树以后,渲染引擎会对图层树中的每一个图层进行绘制,把一个图层的绘制拆分红不少小的绘制指令,再把这些指令按照顺序组成一个待绘制列表:
图层绘制阶段输出的内容就是这些待绘制列表。能够在开发者工具中查看一个图层的绘制列表,在区域2拖动进度条能够重现列表的绘制过程:
当图层的绘制列表准备好以后,渲染进程中的主线程会把该绘制列表提交给合成线程。
一般页面内容都比屏幕大得多,若是等待全部图层都绘制完毕再进行合成的话,会产生一些没必要要的开销,也会让合成图片的时间变得更久。合成线程会将图层划分为固定的图块(tile),大小一般是256x256或512x512。若是这个图层很是大,会优先处理视口(屏幕上页面的可见区域)附近的图块。
渲染进程维护了一个栅格化的线程池,全部图块的栅格化都是在这个线程池内执行。 栅格化就是按照绘制列表的指令生成图片,每一个图层都对应一张图片,合成线程将这些图片合成为“一张”图片。图块是删格化执行的最小单位。
一般,栅格化过程都会使用GPU来加速生成,这个过程叫作快速栅格化或GPU栅格化,渲染进程把生成位图的指令发送给GPU,生成的位图保存在GPU内存中。
但即便只绘制优先级最高的图块,也要耗费很多时间,由于会涉及到一个很关键的因素——纹理上传(存储在共享内存中的位图将做为纹理上传到GPU,最后由GPU将多个位图进行合成),这是由于从计算机内存上传到GPU内存的操做会比较慢。Chrome采起的策略是,在首次合成图块的时候使用一个低分辨率的图片,分辨率减小一半,纹理就减小了四分之三。在首次显示页面内容的时候,将这个低分辨率的图片显示出来,而后合成器继续绘制正常比例的网页内容,绘制完成后再替换掉当前显示的低分辨率内容。
因为合成操做在合成线程上完成,不会影响主线程,这就是为何常常主线程卡住了但CSS动画依然能执行的缘由。而使用JavaScript实现动画时,会牵涉到整个渲染流水线,绘制效率底下。涉及到一些可使用合成线程来处理CSS特效或动画的状况,可使用will-change来提早告诉渲染引擎,让它为该元素准备独立的图层,而且开启GPU加速。
例如will-change:transform,opacity;
告诉渲染引擎将会对该元素作一些特效变换,渲染引擎会为该元素单独分配一个图层,当这些变换发生时,渲染引擎会经过合成线程直接去处理变换,因为没有涉及到主线程,大大提高了渲染效率。因此CSS动画比JavaScript动画高效。
但这样它占用的内存也会增长,由于从层树开始,后面每一个阶段都会多一个层结构,这些都须要额外的内存。
一旦全部图块都被栅格化,合成线程就会生成一个绘制图块的指令——“DrawQuard”,而后将该指令提交给浏览器进程。浏览器进程中的viz组件接收到指令后,将页面内容绘制到内存中,最后再将内存中的页面显示在屏幕上。
每一个显示器都有固定的刷新频率,一般是60Hz,即每秒更新60张图片,更新的图片都来自于显卡的前缓冲区。显示器每秒固定读取60次前缓冲区中的图像来显示。显卡中的GPU合成新的图像,保存到后缓冲区。而后系统将显卡的先后缓冲区互换,来让显示器读取最新的图像。一般,显卡的更新频率和显示器的刷新频率是一致的,但有时在一些复杂场景中,显卡处理一张图片的速度变慢,就形成了视觉上的卡顿。
页面上的动画效果例如滚动,渲染引擎经过渲染流水线生成新的图片发送到显卡的后缓冲区,要想实现流畅的动画效果,渲染引擎须要每秒更新60张图片到显卡的后缓冲区。每一副图片称为一帧,每秒更新了多少帧称为帧率,若是滚动过程当中1秒更新了60帧,帧率就是60Hz(或60FPS)。若是一次动画中,渲染引擎生成某些帧的时间太久,用户就会感受到卡顿。
渲染流程结束后,渲染进程会发送一个消息给浏览器进程,浏览器进程中止标签图标上的加载动画。
几个和性能优化相关的概念:
若是修改了元素的几何位置属性,如改变宽高,会引起从新布局及以后一系列流程,这个过程叫作重排。
调用如下DOM API,为了保证结果的准确性,浏览器会触发重排以获取最新的信息:
不改变几何位置信息,只改变元素的如背景颜色等信息,引起从新绘制及以后一系列流程,这个过程叫作重绘。
若是既不要布局也不要绘制的属性,渲染引擎会跳过布局和绘制,只执行后续的合成操做,这个过程叫作合成。例如使用CSS的transform来实现动画效果,能够避开重排和重绘阶段,在非主线程上执行合成动画的操做,无需占用主线程的资源,大大提升了效率。
所以在执行效率方面:合成 > 重绘 > 重排。在性能优化方面能够依据这个原则来调整方案。