背景
资料:docs.google.com/presentatio…
我们作为工程师,一名开发者,我认为时刻保持好奇心是一件很重要的事情,前端工程师们,不知你们是否好奇过 我们写出的代码究竟是如何变成屏幕上五彩斑斓的像素点的?
本文就是来解决上面的这个问题的。
本文目的
但是,浏览器的渲染机制其实是一个很大的研究课题,过去的一个月我做了很多研究,找了很多资料,废了很多心血,越发觉得其中的很多细节可以消耗我很多很多时间,我也仅仅是了解到皮毛,文章中也会有我个人的理解,也可能存在错误,如果有大佬看到,也希望多多指导和批评,我也会积极采纳。
本文的目的是用通俗的语言,为大家介绍浏览器渲染的完整过程,并不会对每一步进行细致深入的讲解,(即让大家读懂是我最大的目标)。所以,阅读本文你可以得到什么呢:
- 浏览器渲染的大致流程
- 得到一个和别人喝咖啡的时候的聊天话题
- 和我变成朋友(这是最关键的一点是吧,嘿嘿)
勘误
流程图 中的主进程 -> 主线程。(画图的时候没注意,抱歉。。。老朽在此认罪)
goal渲染的目标
首先,本文名称是浏览器渲染机制,那我们第一步就是要了解,浏览器渲染的目标是什么?
看上图,我们首先要了解一个content的概念,content指的是网页中这个区域,上面的标签栏,地址栏都不属于content。
所以,我们知道了整个渲染过程我们的输入是前端的代码,输出是content区域的像素点。好,这样我们渲染的目标就很明确了,即:把前端写出的代码转换成网页content区域的像素点!
下面,我们开始,为中间这个RENDER管道,一点一点的加上内容,让它丰满起来。
Blink(浏览器渲染机制之主线程做了啥)
DOM
往后的阅读,希望大家跟着我的思路,跟着思考,我会解释的很通俗,不要中途离开,拉钩哈。(拉完勾之后,说好的阅读完就要阅读完,一句话,一个字没有读,都不是阅读完~ )
DOM阶段介绍
首先,大家去想,我们的输入是code,但是中间过程的处理是,需要一个数据结构的,
你这个输入的code说白了只是冰冷的字符串,我们第一步要做的肯定是把它转换成有温度的数据结构。
<div> <p> hello </p> <p> world </p> </div>
所以,这个数据结构应该是什么样的呢?我们知道,其实html标签其实是可以嵌套的层次结构,比如很常见的一个div里丢了两个p标签。所以,用一个树形结构,对其进行表示再合适不过了。
这个树形结构也就如上图所示,他也有一个名字:DOM(Document Object Model)。
那么DOM有什么用呢:
- DOM是页面的内部表示
- DOM也为js提供了查询和修改的api接口
DOM阶段总结
这一步,浏览器干了什么事情,解析了html,把html转化成了DOM。
输入:html代码
输出:DOM
目前流程图:
STYLE
STYLE阶段介绍
大家现在想一想,现在我们有了DOM,但是这个DOM只是html转化出来的数据结构,上面是没有样式信息的,而我们要得到的数据结构一定是带有样式信息并且能表示标签嵌套层级的数据结构。所以我们在Style这一步,目的是为了可以得到能与DOM有效结合的css结构!!
这一步和DOM的意思也差不多,我们会把css代码转换成CSSOM。所以我们模仿一下前面DOM的话,那么CSSOM有什么作用呢?
- 能表示css结构的层级
- 为js提供可以操作css的API,允许用户动态的读取和修改css样式。
下一步,浏览器会再计算出computedStyle,DOM树中的每一个节点都有对应的computedStyle。
STYLE阶段总结
这一步,浏览器干了什么事情,把css解析,转化成了CSSOM,又转换成了和DOM节点一一对应的computedStyle。
输入:css
输出:computedStyle
目前流程图:
LAYOUT
LAYOUT阶段介绍
首先,我们回顾一下,我们前面的操作得到了啥,我们得到了DOM,它可以表示标签的嵌套结构,又得到了computedStyle,它存储着样式信息并可以和DOM节点一一对应。于是,我们想法就很简单,把DOM和computedStyle结合起来。于是乎,就有了LAYOUT这个阶段。
layout布局算法的输入就是DOM和computedStyle。
这一步主要就是确定元素的几何形状,坐标和尺寸。其中有以下几点要处理:
- 【块流】最简单的情况下,布局按dom的顺序,垂直的排列一个有一个块,也叫块流(block flow)
- 【行内流】还有文本和行内元素比如span,会在一行从左到右流动,叫做行内流,inline-flow。
- 【字体】布局也要计算字体。(使用HarfBuzz的文本形状库)
- 【包围矩形】布局可以为一个元素计算多种类型的边界矩形。 例如,当存在溢出时,布局将计算边框框的矩形和布局溢出矩形。如果节点的溢出是可滚动的,布局还计算滚动边界并为滚动条保留空间。最常见的可滚动DOM节点是document节点本身(dom树的根)。
- 【复杂布局】比如表格,或者由内容包围的浮动元素
最后这个阶段会生成Layout Tree,这个Layout Tree与DOM相关联,但是不是一一对应的,比如:
- 一个容器里面有了 div和span,span是行内元素,div是块级元素,于是,为了保证容器里面只有块级盒子,所以会在span的布局对象外面包裹一层匿名的块级盒子。这就是一个LayoutObject没有对应的DOM节点的情况。(如上图anonymous LayoutBlock)
- 以及如果一个盒子的计算属性中的display属性是none,那么它也没有对应的LayoutObject。
LAYOUT阶段总结
这一步,我们干了什么事情,我们拿到了上一步的结果(DOM和computedStyle),通过布局算法,将两者进行了结合,我们得到了含有样式信息,布局信息的layout tree。但是注意这个layout tree和DOM不是一一对应的。
输入:DOM,computedStyle
输出:layout tree
目前流程图:
paintlayer tree
前面我们得到了Layout Tree,但是现在有一个问题,现在这个东西只能无脑的让文档流靠后的元素覆盖前面的元素。然而浏览器还有个层叠上下文就是决定元素间相互覆盖关系(比如z-index)的东西。这使得文档流中位置靠前位置的元素有可能覆盖靠后的元素。
于是PaintLayer Tree诞生了!!!PaintLayer 这棵树主要用来实现层叠上下文,以保证dom重叠时也能用正确的顺序合成页面,这样才能正确的展示元素的重叠以及半透明元素等等。
我们在此,只需要,为了处理层叠上下文,于是有了paintlayer tree,就ok了。
于是现在的流程图变成了这样:
comp.assign
comp.assign阶段介绍
好的,经过前面几个步骤,我们拿到了一个具有样式属性又可以表示嵌套结构的的数据结构了。所以,我们现在可以想一些额外的问题了?
我们的页面是静态的么?是不是有可能经常变动? 是
所以,如果变动会导致整个渲染过程重新运行一遍,是不是开销巨大? 是
所以,这个问题是不是要解决? 是的,必须要解决!!!(大喊,并有哭腔)
于是我们引入了图层合成加速的概念。什么是图层合成加速?
- 图层合成加速是把整个页面按照一定规则划分成多个图层,在渲染的时候,只要操作必要的图层,其他的图层只要参与合成就好了,以这种方式提高了渲染的效率
合成层作用:
- 合成层的绘制,渲染会交给GPU处理,比CPU更快
- repaint时,只用repaint自身即可
于是为了页面变动时的性能,我们引入了comp.assign这个阶段,他会把整个页面按照一定规律划分成多个图层。
comp.assign阶段总结
这一步,我们干了什么事情,我们为了页面变动时不重新运行整个渲染的管道,把整个页面拆分成了很多图层。
输入:paintlayer tree/layout tree
输出:GraphicsLayer(图层) Tree
目前流程图:(我们的内容也越来越充实了)
Prepaint
Prepaint阶段介绍
在描述属性的层次结构这一块,之前的方式是使用图层树(GraphicsLayer Tree)的方式,如果父图层具有矩阵变换(平移、缩放或者透视)、裁剪或者特效(滤镜等),需要递归的应用到子节点,这在极端情况下会有性能问题。 于是,就有了 属性树这个概念。合成器提供了变换树,裁剪树,特效树等。每个图层都有若干节点id,分别对应不同属性树的矩阵变换节点、裁剪节点和特效节点。 在发生变换的时候,可以直接对这些节点进行操作。
Prepaint阶段总结
目的:对这些变换进行单独处理以提高性能。
做了什么:对这些上述变换做了单独处理,构建了 property trees(属性树,注意是trees)
输入:paintlayer tree/layout tree
输出:Property trees
目前流程图:
Paint
Paint阶段介绍
paint阶段,我就简单的一笔带过,就是我们去遍历上一阶段结果(即:GraphicsLayers)并把绘制的操作放在一个数据结构里面(DisplayItem)
Paint阶段也是 blink和cc(合成器组件)对接的阶段,会对DisplayItem List再处理为 cc:layer list 并和prepaint阶段产生的Property trees提交给 合成器线程。
在此我引用了官方的流程图,大家可以以此回顾,我们之前做了什么。
from layout | v +------------------------------+ | LayoutObject/PaintLayer tree |-----------+ +------------------------------+ | | | | PaintLayerCompositor::UpdateIfNeeded() | | CompositingInputsUpdater::Update() | | CompositingLayerAssigner::Assign() | | GraphicsLayerUpdater::Update() | PrePaintTreeWalk::Walk() | GraphicsLayerTreeBuilder::Rebuild() | PaintPropertyTreeBuider::UpdatePropertiesForSelf() v | +--------------------+ +------------------+ | GraphicsLayer tree |<------------------| Property trees | +--------------------+ +------------------+ | | | |<-----------------------------------+ | | LocalFrameView::PaintTree() | | LocalFrameView::PaintGraphicsLayerRecursively() | | GraphicsLayer::Paint() | | CompositedLayerMapping::PaintContents() | | PaintLayerPainter::PaintLayerContents() | | ObjectPainter::Paint() | v | +---------------------------------+ | | DisplayItemList/PaintChunk list | | +---------------------------------+ | | | |<--------------------------------------------------+ | PaintChunksToCcLayer::Convert() | v | +--------------------------------------------------+ | | GraphicsLayerDisplayItem/ForeignLayerDisplayItem | | +--------------------------------------------------+ | | | | LocalFrameView::PushPaintArtifactToCompositor() | | PaintArtifactCompositor::Update() | +--------------------+ +--------------------------+ | | v v +----------------+ +-----------------------+ | cc::Layer list | | cc property trees | +----------------+ +-----------------------+ | | +-------------+--------------+ | to compositor v
Paint阶段总结
做了什么:处理GraphicsLayers,把绘制操作放在DisplayItem中。因为Paint阶段也是主线程最后一个步骤,也要与合成器线程对接,所以会把数据再处理为 cc:layer list并和prepaint阶段产生的Property trees(也会处理为cc property trees)提交给 合成器线程。
输入:GraphicsLayer Tree,Property trees
中间产物:DisplayItem List
输出: cc:layer list,cc property trees
目前流程图:(一下子就变大了嘿)