浏览器渲染流程总结

浏览器中的进程介绍

进程和线程的概念

进程:一个进程就是一个程序的运行实例。启动一个程序的时候,操做系统会为该程序建立一块内存,用来存放代码、运行中的数据和一个执行任务的主线程。html

线程:线程不能单独存在,须要由进程来启动和管理。算法

进程和线程的特色:canvas

  1. 进程中任一线程执行出错都会致使整个进程崩溃。
  2. 线程之间共享进程中的数据。
  3. 当一个进程关闭以后,操做系统会回收进程所占用的内存,即便其中有线程因执行不当致使内存泄露,进程退出时,这些内存也会被正确回收。
  4. 进程之间的内容相互隔离,使用进程间通讯(IPC)机制。
浏览器的发展历程
单进程浏览器时代

浏览器全部功能模块都运行在同一个进程里,这些模块包括网络、插件、js运行环境、渲染引擎和页面。带来的问题:浏览器

  • 不稳定:一个插件的意外崩溃会引发整个浏览器的崩溃。
  • 不流畅:全部页面的渲染模块、js执行环境及插件都运行在同一个网页线程中,同一时刻只有一个模块能够执行。
  • 不安全:插件可使用C/C++等代码编写,经过插件能够获取到操做系统的任意资源,页面运行插件意味着这个插件能够彻底操做你的电脑。页面脚本也能够经过浏览器漏洞获取系统权限引起安全问题。
多进程浏览器时代

早期多进程架构中,页面运行在单独的渲染进程中,插件也是单独的插件进程,进程之间经过IPC通讯。这就解决了单进程带来的问题:进程间相互隔离,不会互相影响,也不会互相阻塞,并且插件进程和渲染进程使用了安全沙箱,程序在沙箱中运行,不能在硬盘上读写数据。缓存

目前最新的Chrome进程架构图:安全

包括:1个浏览器(Browser)主进程、1个GPU进程、1个网络(NetWork)进程、多个渲染进程、多个插件进程。因为渲染进程和插件进程须要运行用户代码,所以安全起见,被隔离在沙箱中。各进程功能:性能优化

  • 浏览器进程。主要负责界面显示、用户交互、子进程管理,同时提供存储等功能。
  • 渲染进程:核心任务是将HTML、CSS和JavaScript转换为用户能够与之交互的网页,排版引擎 Blink 和 JavaScript 引擎 V8 都是运行在该进程中,默认状况下,Chrome 会为每一个Tab标签页建立一个渲染进程。
  • GPU 进程:最开始为了实现 3D CSS 的效果,随后网页、Chrome 的 UI 界面都选择采用 GPU 来绘制。
  • 网络进程:主要负责页面的网络资源加载。
  • 插件进程:主要是负责插件的运行,因插件易崩溃,因此须要经过插件进程来隔离,以保证插件进程崩溃不会对浏览器和页面形成影响。

多进程模型的缺点:服务器

  • 更高的资源占用。由于每一个进程都会包含公共基础结构的副本(如 JavaScript 运行环境),这就意味着浏览器会消耗更多的内存资源。
  • 更复杂的体系架构。浏览器各模块之间耦合性高、扩展性差等问题,会致使如今的架构已经很难适应新的需求了。
新页面的渲染进程策略

确切地说,有链接关系的同一站点(协议相同、根域名相同)会共用一个渲染进程。 如下方法可使两个标签页进行链接:markdown

  1. 在A标签页中使用a标签如<a href="./b.html" target="_blabk"> B </a>打开B标签页,此时B标签页中能够经过window.opener访问A的window。
  2. 经过window.open()方法打开的标签页。

这种有链接关系的标签页叫作浏览上下文组,Chrome浏览器会将浏览上下文组中属于同一站点的标签页分配到同一个渲染进程中。但也有例外的状况:网络

  1. a标签的ref属性如<a target="_blank" ref="noopener noreferrer">,其中noopener会将新开标签页的opener值设为null,noreferrer表示新开的标签页不要有引用关系。这就使得新开标签页和当前标签页不属于同一个浏览上下文组了。
  2. 若是当前标签页中的iframe和标签页属于不一样站点,iframe也会运行在单独的渲染进程中。

导航流程

从用户发出url请求到页面开始解析的过程叫作导航。

从输入url到页面展现的流程:

  1. 用户输入:用户在地址栏输入内容后,地址栏会判断是搜索内容仍是请求的url,若是是搜索内容则使用默认搜索引擎来合成带搜索关键字的url;若是是url则会根据规则加上协议合成完整的url。浏览器开始加载,标签页的图标进入加载状态,此时页面仍是以前的页面内容。
  2. 请求资源:浏览器进程经过IPC把url请求发送给网络进程,网络进程来发起真正的请求流程。
    • 查找缓存:若有,则直接返回资源给浏览器进程,如无则进入网络请求流程。
    • 创建链接:进行DNS解析获取IP地址,创建TCP链接和TLS链接(若是使用HTTPS的话)。
    • 发起请求:链接创建后,构建请求行、请求头等信息,并将相关Cookie等数据附加到请求头中,而后向服务器发送构建的请求信息。
    • 接收响应:网络进程接收到服务器发来的响应后开始解析响应内容。若是发现状态码是301或302,则从响应头的Location字段读取重定向地址,再发起新的请求。
    • 处理响应数据类型:根据Content-Type判断响应体的数据类型,若是是application/octet-stream则表示数据是字节流类型,会按下载类型处理,该请求会被提交给浏览器的下载管理器,导航流程就此结束。若是是text/html类型,则会开始准备渲染进程。
  3. 准备渲染进程:浏览器进程从网络进程中的得知是html类型后,会为该请求选择或建立一个渲染进程。
  4. 提交文档:浏览器进程向渲染进程发送“提交文档”的消息,渲染进程收到后,和网络进程创建传输数据的“管道”。等响应体数据传输完成后,渲染进程会返回“确认提交”的消息给浏览器进程,浏览器进程更新界面状态:安全状态、地址栏的url、前进后退的历史状态,更新页面。

确认提交后,导航流程结束,开始进入渲染流程。

渲染流程

渲染进程会先建立一个空白页面,叫作解析白屏。以后开始页面解析和子资源加载。

一 构建DOM树(Parse HTML)——产出DOM树

因为浏览器没法理解HTML,所以须要将HTML解析为浏览器能理解的结构——DOM树。DOM是保存在内存中的树结构,DOM是生成页面的基础数据结构,给JavaScript脚本提供一套查询和改变文档结构、样式、内容的接口,也是一道安全防御线,一些不安全的内容在DOM解析阶段就被排除了。

渲染引擎内部有个HTML解析器的模块,负责将HTML字节流转换为DOM结构。解析过程是渐进的,网络进程加载了多少数据,HTML解析器就解析多少数据。渲染进程从以前和网络进程创建的数据管道的一端动态接收字节流,并将其解析为DOM。

解析过程

  1. 经过分词器将字节流转换为Token,分为Tag Token和文本Token。

  2. 将Token解析为DOM节点,而后将节点添加到DOM树中。HTML解析器维护了一个Token栈结构,若是是StartTag Token则入栈,并建立一个DOM节点添加到DOM树中(HTML解析器最初会默认建立一个根为document的空DOM结构);若是是文本Token则生成文本节点添加到DOM树中;若是是EndTag Token则检查栈顶是不是相同标签的StartTag Token,是则出栈。

JavaScript和CSS对DOM构建的影响

解析过程当中遇到了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树的构建与否取决于脚本执行的时机。

二 样式计算(Recalculate Style)——产出ComputedStyle

样式计算是为了计算出DOM节点中每一个元素的具体样式,这个阶段大致能够分为三个步骤:

  1. 构建CSSOM。CSS的来源有三种:经过link标签引用的外部CSS、style标签、元素的style属性。因为浏览器也没法直接理解纯文本的CSS,所以渲染引擎须要先将其转为浏览器能够理解的结构——CSSOM(能够经过document.styleSheets访问)。最终会把全部不一样来源的样式都包含进styleSheets中。CSSOM的做用一个是为JavaScript提供操做样式表的接口,一个是为布局树的合成提供基础的样式信息。
  2. 标准化样式属性值:将例如2em、blue、bold之类的数值转换为渲染引擎容易理解的、标准化的计算值。最终解析成相似32px、rgb(255,0,0)、700这样的标准值。
  3. 计算DOM树中每一个节点的具体样式:须要使用两个规则:
    • 继承:每一个DOM节点都会包含父节点中能够继承的属性,例如body的font-size。
    • 层叠:定义了合并多个来源的属性值的算法,计算结果保存在ComputedStyle的结构内。
三 布局(Layout)——产出布局树

此阶段用来计算DOM树中可见元素的几何位置。有两个步骤:

  1. 建立布局树:遍历DOM树,用全部可见节点构建一颗布局树,不可见的节点如head或display:none的元素都会被过滤掉。这个过程的样式查询会使用到ComputedStyle。

  1. 布局计算:计算布局树中节点的坐标位置,把布局计算的结果从新写回布局树中。所以布局树既是输入内容,也是输出内容,这也是布局阶段一个不合理的地方,Chrome团队正在重构布局代码,下一代布局系统叫LayoutNG,将会尝试分离输入和输出,简化布局算法。
四 分层(Update Layer Tree)——产出图层树

因为页面中可能包含不少复杂的效果如3D变换、页面滚动、z-index的z轴排序等,为了不每次改动都要引起整个页面重排或重绘,会将页面分红多个图层,并生成一颗对应的图层树(LayerTree),层树中的每一个节点都对应一个图层,这些图层按照必定顺序叠加在一块儿(层合成composite)构成了最终的页面图像。

一般,并不是布局树的每一个节点都占用一个图层,没有图层的节点将会从属于父节点的图层,最终每一个节点都会直接或间接包含在一个图层中。处于相同的z轴坐标空间时,就会造成一个图层(或渲染层RenderLayers)。

渲染引擎会为一些特定的节点建立单独的图层:

  1. 知足如下特殊条件:
  • 3D transforms:translate3d、translateZ 等
  • video、canvas、iframe 等元素
  • 经过 Element.animate() 实现的 opacity 动画转换
  • 经过 СSS 动画实现的 opacity 动画转换
  • position: fixed
  • 具备 will-change 属性
  • 对 opacity、transform、fliter、backdropfilter 应用了 animation 或者 transition
  1. 能够滚动的overflow元素:元素的内容超出容器时,若是设置了overflow使得内容能够滚动,则分别会为容器、滚动的所有内容、滚动条建立各自的图层。这样滚动时,各部分就不会由于重排/重绘而相互影响。

隐式建立图层(合成层)

若是一个元素覆盖在一个拥有独立图层的元素上,而且这个元素的z-index值更大,那这个元素也会被提高到单独的图层中。

层爆炸和层压缩

像上面那种隐式建立图层的状况,若是不加注意,极端场景下可能会建立大量独立图层,也就是层爆炸,会占用GPU和大量的内存资源,严重损耗页面性能。浏览器也有相应的对策,浏览器的层压缩机制,会将隐式建立的多个图层压缩到一个图层中。

z-index:3这个元素是一个独立的图层,其后z-index值更大的三个元素被压缩进了一个图层中:

可是不少特定状况下,浏览器是没法进行层压缩的。例如若是页面的某一部分使用transform和animation制做动画效果,因为可能产生动态交叠的状况,隐式建立图层在不须要交叠的状况下也能发生,致使页面中全部z-index高于它的节点都被提高到单独的图层中,使得页面产生了大量的独立图层。消除的办法就是增大z-index的值,或者合理优化页面元素的结构。

五 图层绘制(Paint)——产出图层的待绘制列表

获得图层树以后,渲染引擎会对图层树中的每一个图层进行绘制,把一个图层的绘制拆分红不少小的绘制指令,再把这些指令按照顺序组成一个待绘制列表:

图层绘制阶段输出的内容就是这些待绘制列表。能够在开发者工具中查看一个图层的绘制列表,在区域2拖动进度条能够重现列表的绘制过程:

六 栅格化(raster)——产出合成后的位图

当图层的绘制列表准备好以后,渲染进程中的主线程会把该绘制列表提交给合成线程。

一般页面内容都比屏幕大得多,若是等待全部图层都绘制完毕再进行合成的话,会产生一些没必要要的开销,也会让合成图片的时间变得更久。合成线程会将图层划分为固定的图块(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)。若是一次动画中,渲染引擎生成某些帧的时间太久,用户就会感受到卡顿。

渲染流程结束后,渲染进程会发送一个消息给浏览器进程,浏览器进程中止标签图标上的加载动画。

总结

  1. 渲染进程将HTML解析为为DOM树结构。
  2. 渲染引擎将CSS样式表解析为CSSOM,并计算出DOM节点的样式。
  3. 建立布局树,并计算元素的布局信息。
  4. 对布局树进行分层,并生成分层树。
  5. 为每一个图层生成绘制列表,并将其提交到合成线程。
  6. 合成线程将图层分红图块,并在光栅化线程池中将图块转换成位图。
  7. 合成线程发送绘制图块命令DrawQuad给浏览器进程。
  8. 浏览器进程根据DrawQuad消息生成页面,并显示到显示器上。

几个和性能优化相关的概念:

重排

若是修改了元素的几何位置属性,如改变宽高,会引起从新布局及以后一系列流程,这个过程叫作重排。

调用如下DOM API,为了保证结果的准确性,浏览器会触发重排以获取最新的信息:

  • offsetTop、offsetLeft、offsetWidth、offsetHeight(元素包含边框的宽/高)
  • scrollTop、scrollLeft、scrollWidth、scrollHeight(元素包含内边距的包含不可见的滚动内容的宽/高)
  • clientTop、clientLeft、clientWidth、clientHeight(元素包含内边距的宽/高)
  • window.getComputedStyle(elem)、IE里的currentStyle
  • getBoundingClientRect()
重绘

不改变几何位置信息,只改变元素的如背景颜色等信息,引起从新绘制及以后一系列流程,这个过程叫作重绘。

合成

若是既不要布局也不要绘制的属性,渲染引擎会跳过布局和绘制,只执行后续的合成操做,这个过程叫作合成。例如使用CSS的transform来实现动画效果,能够避开重排和重绘阶段,在非主线程上执行合成动画的操做,无需占用主线程的资源,大大提升了效率。

所以在执行效率方面:合成 > 重绘 > 重排。在性能优化方面能够依据这个原则来调整方案。

References

  1. 极客时间:《浏览器渲染原理与实践》
  2. 浏览器合成与渲染层优化:mp.weixin.qq.com/s/knmQ1XRwt…
相关文章
相关标签/搜索