浏览器(browser application)是专门用来访问和浏览万维网页面的客户端软件,也是现代计算机系统中应用最为广泛的软件之一,其重要性不言而喻。前端工程师作为负责程序页面显示的工程师,需要直接与浏览器打交道。
主要组件包括:
浏览器内核分为两个部分:渲染引擎和js引擎,由于js引擎越来越独立,内核就倾向于只指渲染引擎,负责请求网络页面资源加以解析排版并呈现在屏幕上。
默认情况下,渲染引擎可以显示html、xml文档以及图片,它也可以依靠插件显示其他文件类型,如:pdf、markdown。
从资源的下载到最终页面展现,渲染流程可以简单的理解成一个线性串联的变换过程的组合,原始输入为URL地址,最终输出为页面Bitmap,中间依次经历了Loader、Parser、Layout和Print模块
渲染核心
【Loader】
Loader模块负责处理所有的HTTP请求以及网络资源的缓存,相当于是从URL输入到Page Resource输出的变换过程。HTML页面中通产有外链的JS/CSS/Image资源,为了不阻塞后续解析过程,一般会有两个IO管道同时存在,一个负责主页面下载,一个负责各种外链支援的下载
虽然大部分情况下不同资源可以并发下载异步解析(如图片资源可以在主页面解析显示完成后再被显示),但JS脚本可能会要求改变页面,因此有保持执行顺序和下载管道后续处理的阻塞是不可避免的
【Parser】
解析HTML
Parser模块主要负责解析HTML页面,完成HTML文本到HTML语法树再到文档对象树(Document Object Model Tree,DOM Tree)的映射过程
HTML语法树生成是一个典型的语法解析过程,可以分成两个子过程:词法解析和语法解析
词法解析按照词法规则(如正则表达式)将HTML文本分割成大量的标记(token),并去除其中无关的字符(如空格)。语法解析按照语法规则(如上下文无关文法)匹配Token序列生成语法树,通常有自上而下和自下而上两种匹配方式。
浏览器内核中对HTML页面真正的内部表示并不是语法树,而是W3C组织规范的文档对象模型(Document Object Model,DOM)。DOM也是树形结构,以Document对象为根。DOM节点基本和HTML语法树节点一一对应,因此在语法解析过程中,通常直接生成最终的DOM树。
解析CSS
页面中所有的CSS由样式表CSSStyleSheet集合构成,而CSSStyleSheet是一系列CSSRule的集合,每一条CSSRule则由选择器CSSStyleSelector部分和声明CSSStyleSeclaration部分构成,而CSSStyleDeclaration是CSS属性和值的key-Value集合。
CSS解析完毕后会进行CSSRule的匹配过程,即寻找满足每条CSS规则Selector部分的HTML元素,然后将其Declaration部分应用于该元素。实际的规则匹配过程会考虑默认和继承的CSS属性、匹配的效率及规则的优先级等因素。
解析Javascript
Javascript一般由单独的脚本引擎解析执行,它的作用通常是动态地改变DOM树(比如为DOM节点添加事件响应处理函数),即根据事件(timer)或事件(event)映射一棵DOM树到另一棵DOM树。
简单的说,经过了Parser模块的处理,内核把页面文版转换成了一棵节点带CSS Style、会想i也能够自定事件的Styled DOM树。
【layout】
Layout过程就是排版,它包含两大过程
创建渲染树
布局树(或者叫渲染树、Render Tree)和DOM树大体能一一对应,两者在内核中同时存在但作用不同。DOM树是HTML文档的对象表示,同时也作为Javascript操纵HTML的对象接口。Render树是DOM树的排版表示,用以计算可视DOM节点的布局信息(如宽、高、坐标)和后续阶段的绘制显示。
并非所有DOM节点都可视,也就是并非所有DOM树节点都会对应生成一个Render树节点。例如head标签(HRMLHeadElement)不表示任何排版区域,因而没有对应的Render节点。同时,DOM树可视节点的CSS Style就是其对应Render树节点的Style。
计算布局
布局就是安排和计算页面中每个元素大小位置等几何信息的过程。HTML采用流式布局模型,基本的原则是页面元素在顺序遍历过程中依次按从左至右、从上至下的排列方式确定各自的位置区域。
一个HTML元素对应一个CSS盒子模型描述的方块区域,HTML元素分成两个基本类型,Inline和Block。Inline元素不会换行,按从左至右来布局。Block元素的出现意味着需要从上至下换到下一行布局。除了这种基本的顺序按照元素的Inline和Block来进行流式布局之外,还有特殊指定的一些布局方式,如Absolute/Fixed/Relative三种定位布局以及浮动布局。
简单情况下,布局可以顺序遍历一次Render树完成,但也需要迭代的情况。当祖先元素的大小位置依赖于后代元素或者互相依赖时,依次遍历就无法完成布局,如Table元素的宽高未明确指定而下某一子元素Tr指定其高度为父Table高度的30%的情况。
经过Layout阶段的处理,把带Style的DOM树变换成包含布局信息和绘制信息的Render树,接下来的显示工作就交由Paint模块进行操作了。
Paint
Paint模块负责将Render树映射成可视的图形,它会遍历Render树调用每个Render节点的绘制方法将其内容显示在一块画布或者位图上,并最终呈现在浏览器应用窗口中成为用户看到的实际页面。每个节点对应的大小位置等信息都已经由Layout阶段计算好了,节点的内容取决于对应的HTML元素,或者文本,或者图片,或者时UI控件。
通常情况下,布局和绘制是相当耗时的操作。如果DOM树每次略有改动都要重新布局和绘制一次,效率会相当低下。因此,一般浏览器内核都会事先一种增量布局和增量绘制的方式。当一个DOM树节点(或者它的子节点)内容或者样式发生变化时,内核会确定其影响范围,在布局阶段会标记出该节点布局影响的其他节点(比如可能时子节点),在绘制阶段则会标记出一个Dirty区域并通知系统重绘。
按照HTL相关规范,页面元素的CSS属性也规定了其绘制顺序,如根据不同Layer必须按照顺序绘制,否则覆盖叠加效果会出现错误,如元素的边框轮廓和内容背景的绘制次序也有规定。
使用浏览器上网时,首先会在地址栏输入一个网址,浏览器会依据网址向服务器发送资源请求,服务器解析请求,并将相关数据资源传送回给浏览器,这些数据源包括Page的描述文档、图片、Javascript脚本、CSS等。此后,浏览器引擎会对数据进行解码、解析、排版、绘制等操作,最终呈现出完整的页面。Loader是浏览器的排头兵,负责资源加载的工作。
Loader在浏览器中承上启下,一方面他作为网络模块的客户,通过网络模块来加载资源;另一方面它为Parser模块加载页面的内容,控制着浏览器后续的解析以及绘制过程。
Loader有两条资源加载路径:主资源加载和派生资源加载路径。这两类资源的加载过程颇有不同,比如对资源加载失败的处理,主资源下载失败会有报错提示,而派生资源如图片下载失败,往往只显示一个占位。
在地址栏输入新地址或者已经打开的页面中点击链接,都会触发主资源的加载流程,随着主资源在HTTP协议的传输下分段到达,浏览器的Parser模块解析主资源的内容,生成派生资源对应的DOM结构,然后根据需求触发派生资源的加载流程。主资源的加载是立刻发起的,而派生资源则可能会为了优化网络,在队列中等待。
主资源和派生资源的加载还有一个区别,在Android4.2版本中主资源是没有缓存的,而派生资源是有缓存机制的。这里的缓存指的是Menory Cache,用于保存原始数据(比如CSS、JS等),以及解码过的数据,通过Memory Cache可以节省网络请求和图片解码的时间。
浏览器在加载主资源后,主资源会被解码,然后进行解析,生成DOM树(文档对象模型)树。在解析过程中,如果遇到
缓存在浏览器中也得到了广泛的应用,对提高用户体验起到了重要作用。在浏览器中,主要存在三种类型的缓存:Page Cache、Memory Cache、Disk Cache。这三类Cache的容量都是可以配置的,比如限制Memory Cache最大不超过30MB,Page Cache缓存的页面数量不超过5个
【内存缓存】
Memory Cache,顾名思义内存缓存,其主要作用为缓存页面使用各种派生资源。在使用浏览器浏览网页时,尤其是浏览一个大型站的不同页面时,经常会遇到网页中包含相同资源的情况,应用Memory Cache可以显著提升浏览器的用户体验,减少无谓的内存、时间以及网络带宽开销。
【页面缓存】
Page Cache,页面缓存。用来缓存用户访问用户访问过的网页DOM树、Render树等数据。设计页面缓存的意图在于提供流畅的页面前进、后退等浏览体验。几乎所有的现代浏览器都支持页面缓存的功能。
如果浏览器没有页面缓存,用户点击链接访问新页面时,原页面的各种派生资源、Javascript对象、DOM树节点等占据内存统统被回收,此后当用户点击后退按钮以浏览原页面时,浏览器必须要重新从网络下载相关资源,然后进行解码、解析、布局、渲染一系列操作,最后才能为用户呈现出页面,这无疑增加了用户的等待时间,影响了用户的使用体验。
所有的派生资源加载时都会与Memory Cache关联,如果Memory Cache中有资源的备份且条件合适,则可以直接从Memeory Cache中加载。而Page Cache只会在用户点击前进或后退按钮时才会被查询,如果页面符合缓存条件并被缓存了,则直接从Page Cache中加载。即使某个需要被加载的页面在Page Cache中有备份,但若触发加载的原因时用户在地址栏输入url或点击链接,则页面仍然通过网络加载。也就是说Page Cache并不是主资源的通用缓存。
【磁盘缓存】
Disk Cache,磁盘缓存。现代的浏览器基本都有磁盘缓存机制,为了提升用户的使用体验,浏览器将下载的资源保存到本地磁盘,当浏览器下次请求相同的资源时,可以省去网络下载资源的时间,直接从本地磁盘中去除资源即可。
磁盘缓存即我们常说的web缓存,分为强缓存和协商缓存,他们的区别在于强缓存不发请求到服务器,协商缓存会发请求到服务器。
网页解析可以将浏览器整体看作是以恶搞网页处理模块,这个模块的输入是网络上接收到的字节流形式的网页内容。
输出三棵树型逻辑结构:
- DOM树
- Render树
- RenderLayer树
浏览器的解析过程就是将字节流形式的网页内容构建成DOM树、Render树以及RenderLayer树的过程。
浏览器的解析对象是网页内容,网页内容包括以下三个部分:
HTMl文档被解析生成DOM树,由DOM节点创建Render树节点时,会触发CSS匹配过程,CSS匹配的结果是RenderLayer实例,这个实例由Render节点持有,保存了Render节点的排版布局信息。CSS的解析过程即是CSS语法在浏览器的内部表示过程,解析的结果是得到一系列的CSS规则。CSS的匹配过程主要依据CSS选择器的不同优先级进行,高优先级选择器优先适用。根据网页上定义的Javascript脚本的不同属性,Javascript脚本的下载和执行实际会有所不同。Javascript脚本的执行是由渲染引擎转交给JS引擎执行的。下面分别看一下HTML、CSS、Javascript的具体解析和执行。
【DOM树构建】
DOM(Document Object Model,文档对象模型),是中立于平台和语言的接口。它允许和脚本动态地访问和更新文档的内容结构和样式。DOM是页面上数据和结构的一个树型表示,使用DOM接口可以对DOM树结构进行操作。DOM规范只是定义了编程接口,没有对文档的表示方式做任何限制。以树型结构表示DOM文档是比较普遍的实现方式。这个树形结构就称为DOM树。DOM树是DOM文档中的节点按照层次组织构成的。以HTML文档为例,每一个标签都对应着DOM树上的一个节点。由于树形结构表示,这些节点之间的关系也是通过父子或兄弟维系的。
渲染引擎解析HTML文档的过程就是将字节流形式的网页内容解析成DOM Tree、Render Tree、RenderLayer Tree三棵树的过程。这个过程可以分为解码、分词、解析、建树四个步骤:
【Render树构建】
Render树用于表示文档的可视信息,记录了文档每个可视元素的布局及渲染方式。Render树与DOM树是同时创建的。
HTML页面通过CSS控制页面布局,所以RenderObject需要知道自身的CSS属性,CSSStyleSelector负责为元素提供RenderStyle。RenderObject包含自身的RenderStyle的引用。CSSStyleSelector是在CSS解析过程中生成的,Render节点创建后,就会被绑定到Render树上。
当前Render节点的父节点负责将当前Render节点插入到合适的位置,当父节点设置好当前Render节点的前后兄弟节点后,当前Render节点就绑定到了Render树上。
RenderObject是Render树所有节点的基类,作用类似于DOM树的Node类。这个类存储了绘制页面可视元素所需要的样式及布局信息,RenderObject对象及其子类都知道如何绘制自己。事实上绘制Render树的过程就是RenderObject按照一定顺序绘制自身的过程。DOM树上的节点与Render树上的节点并不是一一对应的。只有DOM树的根节点及可视节点才会创建对应的RenderObject节点。
【Render Layer】
RenderLayer树以层位节点组织文档的可视信息,网页上的每一层对应一个RenderLayer对象。RenderLayer树可以看作Render树的稀疏表示,每个RenderLayer树的节点都对应着一棵Render树的子树,这棵树上所有Render节点都在网页的同一层显示。
RenderLayer树是基于RenderObject树构建的,满足一定条件的RenderObject才会建立对应的RenderLayer节点,下面是RenderLayer节点的创建条件:
当满足这些条件之一时,RenderLayer实例被创建。RenderObject节点与RenderLayer节点是多对一的关系,即一个或多个RenderObject节点对应一个RenderLayer节点。这一点可以理解位网页的一层中可包含一个或多个可视节点。RenderLayer树的根节点是RenderView实例。
RenderLayer的一个重要用途是可以在绘制时实现合成加速,即每一个RenderLayer对应系统的一块后端存储,这样在网页内容发生更新时,可以只更新有变化的RenderLayer,从而提高渲染效率。
【CSS解析】
CSS解析过程即是将原始的CSS文件中包含的一系列CSS规则表示成渲染引擎中相应规则类的实例的过程
解析选择器和解析属性值的过程都可能执行多次。渲染引擎位解析出来的选择器创建一个CSSSelector实例,由于可能存在多个选择器,渲染引擎使用CSSSelectorList类保存所有的选择器,并为解析出来的每个属性值对创建CSSPropenty实例。
CSS文件解析完成后,CSS规则都保存在了CSSRuleList实例中,这些规则会创建在Render节点的过程中使用到。Node节点通过调用CSSStyleSelector实例的StyleForElement()函数为Render节点创建RenderStyle实例。有了RenderStyle实例才可以创建RednerObject实例。RenderStyle描述了RenderObject的排版布局信息,也就是匹配后的样式信息。
CSS规则匹配过程就发生在CSSStyleSelector创建RenderStyle实例的过程中。CSSStyleSelector负责从CSSRuleList中找到所有匹配相应元素的样式属性的Property-Value。
CSS规则匹配时按照选择器类型的优先级进行的,不同类型的选择器具有不同的优先级。常用选择器类型的优先级如下:
ID选择器 > 类型选择器 > 标签选择器 > 相邻选择器 > 子选择器 > 后代选择器
所有匹配上元素的CSSStyleRule都会放入一个结果数组中。渲染引擎会对所有存入数组中的规则按照选择器的优先级进行排序,高优先级规则优先使用,最终使用的规则会用来创建RenderStyle实例。RenderStyle实例由RenderObject对象持有,RenderObject就是根据RenderStyle中包含的信息,继续宁自身绘制排版。
【JS执行】
Javascript是一种解释型的动态脚本语言,需要专门的Javascript引擎执行。Android 4.2版本的WebKit采用的Javascript执行引擎为V8,V8是由Google支持的开源项目。它的设计目的就是追求更高的性能,最大限度地提高Javascript的执行效率。与JavascriptCore等传统引擎不同,V8把Javascript代码直接编译成机器码运行,比起传统“中间代码+解释器”的引擎,性能优势非常明显。JS代码通常保存在独立的JS文件中,通过script标签引用到HTML文档中。
DOM树创建过程中遇到script标签时会创建HTMLScriptElement实例。HTMLScript-Element的父类ScriptElement中包含了对JS脚本的所有处理,包括下载、缓存、执行等。根据script标签的不同属性,JS脚本加载后的执行时机会有所不同。如果script标签中使用了async属性,JS脚本加载过程不会阻塞文档解析,脚本加载完成后会立即执行。如果sript标签中使用了defer属性,JS脚本加载过程不会阻塞文档解析,当脚本的执行要等得到文档解析完成之后。对于外部引用的脚本文件,从脚本下载到脚本执行完,文档解析过程会一直被阻塞。
硬件加速WebKit渲染引擎的渲染方式分为软件加速和硬件加速,这两种渲染方式都可以分成两个大的过程:一是得到网页的绘制信息;二是将网页绘制信息转换成像素并上屏。
得到网页绘制信息的过程需要遍历RenderLayer树,将RenderLayer树包含的网页绘制信息先记录下来,等到渲染时使用。记录网页绘制信息这一步对渲染引擎而言,就是绘制的过程,渲染引擎本身并不知道绘制命令是否有被真正执行。
【软件渲染】
软件渲染的流程可概括为一下三步:
软件渲染是下年但,网页内容直接绘制到一块图形缓冲区,内存占用更少。不足之处在于,由于网页内容绘制在同一块图形缓冲区上,更新网页内容时需要全部更新,无法局部更新。
【硬件加速】
相较于软件渲染,硬件渲染实现比较复杂,网页内容需要绘制到一块SkBitmap上,再通过图形缓冲区上传给GPU,需要更多内存。
硬件渲染是指网页各层的合成是通过GPU完成的 ,它采用分块渲染的策略,分块渲染是指:网页内容被一组Tile覆盖,每块Tile对应一个独立的后端存储,当网页内容更新时,只更新内容有变化的Tile。分块策略可以做到局部更新,渲染效率更高。
硬件渲染的过程分为以下5步:
开启阴间加速,即合成加速,回味需要单独绘制的每一层创建一个GraphicsLayer
合成加速情况下,每一层网页内容都对应一个后端存储,这块后端存储,这块后端存储由平台实现,Android4.2平台提供的后端存储GraphicsLayerAndroid。开始记录网页绘制命令时,RenderLayerCompositor负责控制RenderLayer的遍历,RenderLayer包含的绘制信息最终记录在其后端存储上,即GraphicsLayerAndroid包含的PicyurePile实例中。
一个RenderLayer对象如果需要后端存储,他会创建一个RenderLayerBacking对象,该对象负责RenderLayer对象所需要的各种存储 。理想情况下,每个RenderLayer都可以创建自己的后端存储,事实上不是所有RenderLayer都有自己的RenderLayerBacking对象。如果以恶搞RenderLayer对象被像样的创建后端存储,那么将该RenderLayer称为合成层(Compositing Layer)
哪些RenderLayer可以是合成层?如果一个RenderLayer对象具有一下特征之一,那么他就是合成层:
所以,进行硬件加速的渲染流程如下图所示
重绘回流重绘和回流在页面渲染过程中非常重要的两个概念。页面生成以后,脚本操作、样式表变更,以及用户操作都可能触发重绘和回流。
【回流】
回流reflow时firefox里的术语,在chrome中称为重排relayout。
回流是指窗口尺寸被修改,发生滚动操作,或者元素位置相关属性被更新时会触发布局过程,在布局过程中要计算所有元素的位置信息。由于HTML使用的是流式布局,如果页面中的一个元素的尺寸发生了变化,则其后续的元素位置都要跟着发生变化,也就是重新进行流式布局的过程,所以被称之为回流。
前面介绍过渲染以前宁生成的3个树:DOM树、Render树、RenderLayer树。回流发生在在Render树上。常说的脱离文档流,就是指脱离渲染树Render Tree。
触发回流包括如下操作:
offsetTop\offsetLeft\offsetWidth\offsetHeight\scrollTop\scrollLeft\scrollWidth\scrollHeight\clientTop\clientLeft\clientWidth\clientHeight\getComputedStyle()\currentStyle()
改变元素的一些样式
触发回流一定会触发后续的重绘操作,而且对于一个元素的回流,可能会影响到父级元素。比如子元素浮动后,父元素会出现高度塌陷的情况。所以,性能优化的重点在于尽量只触发小规模的重绘,尽量不触发回流。
【重绘】
重绘是指与视觉相关的样式的样式属性更新时会触发绘制过程,在绘制过程中要重新计算视觉信息,使元素呈现新的外观。
由于元素的重绘repaint只发生在渲染层RenderLayer上。所以,如果要改变元素的视觉属性,最好让该元素称为一个独立的渲染层RenderLayer
下面以元素显示为例,进行说明。实现元素显示隐藏的方式有很多
如果对一个元素使用硬件加速渲染,如具有CSS 3D属性 ,则不会进行重绘和回流,如果使用硬件渲染的元素过多,会造成GPU的传输压力。
【性能优化】
下面举出一些减少回流次数的方法
因此,需要将多次重绘的元素独立为render layer渲染层,如设置absolute,可以减少重绘范围;对于一些机型动画的元素,可以进行硬件加速渲染,从而避免重绘和回流。