浏览器渲染过程

196 阅读16分钟

解析渲染原理

渲染流程

开始每个⼦阶段都有其输⼊的内容;然后每个⼦阶段有其处理过程;最终每个⼦阶段会⽣成输出内容。

  1. 构建DOM树。

    • 从网络传给渲染引擎的HTML⽂件字节流⽆法直接被渲染引擎理解,所以将HTML转换为浏览器渲染引擎能够理解的结构即DOM树。

      输⼊内容是一个HTML字节流,经由HTML解析器解析,输出DOM树

    • DOM和HTML内容⼏乎是⼀样的,是表述HTML页面的数据结构,DOM保存在内存中,可以通过JavaScript来查询或修改其内容。

  2. 计算样式。CSS样式来源有三种。

    1. 从网络传给渲染引擎的CSS⽂件字节⽆法直接被渲染引擎理解,所以将CSS转换为浏览器渲染引擎能够理解的结构即CSSOM,该结构有查询、修改功能。

      输⼊内容是一个CSS⽂件,经由CSS解析器解析,输出CSSOM。CSSOM体现在DOM中就是document.styleSheets。

    2. 标准化属性值。

    3. 计算出DOM树中每个节点的最终具体样式,在计算过程中需要遵守CSS的继承和层叠两个规则。

  3. 计算布局Layout。

    1. 布局树基本上就是复制DOM树的结构,不同之处在于DOM树中那些不需要显示的元素会被过滤掉如head标签、script标签、display:none属性的元素,只包含可⻅元素。有的复杂结构的DOM元素可对应多个可见对象。
    2. 计算布局树中每个元素对应的⼏何位置。
  4. 分层和合成。将素材分解为多个图层的操作称为分层,最后将这些图层合并到⼀起的操作就称为合成。先分层再合成,通常⼀起使⽤。

    1. 分层Layer

      • 根据布局树的特点将其转换为分层树,树中每个节点都对应着⼀个图层,下⼀步绘制就依赖于分层树中节点。
      • 元素有了层叠上下⽂的属性或者超出部分需要被剪裁,满⾜任意⼀点就会被提升成为单独⼀层。定位属性的元素、定义透明属性的元素、使⽤CSS滤镜的元素等都拥有层叠上下⽂属性。
    2. 图层绘制Paint

      • 并不是真正绘出图⽚,⽽是把⼀个图层的绘制指令按照顺序组成绘制指定列表。
    3. 分块tiles。合成线程/非主线程。

      • 页面的内容都要⽐屏幕⼤得多,显⽰⼀个页面时,如果等待所有的图层都⽣成完毕,再进⾏合成的话,会产⽣⼀些不必要的开销,也会让合成图⽚的时间变得更久。合成线程会将每个图层分割为⼤⼩固定的图块,然后优先绘制靠近视⼝的图块,这样就可以⼤⼤加速页面的显⽰速度。分层是从宏观上提升了渲染效率,那么分块则是从微观层⾯提升了渲染效率。
      • 在⾸次合成图块的时候使⽤⼀个低分辨率的图⽚,然后合成器继续绘制正常⽐例的网⻚内容,当正常⽐例的网⻚内容绘制完成后,再替换掉当前显⽰的低分辨率内容。
    4. 光栅化raster并合成drawQuad。合成线程/非主线程。

      • 按照绘制列表中的指令⽣成图⽚,每⼀个图层对应⼀张图⽚,最终将⽣成的“⼀张”图⽚发送到显卡的后缓冲区。

        合成操作是在合成线程/非主线程上完成的,不会影响到主线程执⾏。这就是为什么经常主线程卡住了,但是CSS动画依然能执⾏的原因。

      • 渲染进程维护了栅格化的线程池,图块栅格化是在线程池内执⾏,将图块转换成位图。图块是栅格化执⾏的最⼩单位。栅格化过程都会使⽤GPU来加速⽣成,使⽤GPU⽣成位图的过程叫快速栅格化或者GPU栅格化,⽣成的位图被保存在GPU内存中。

    5. 显⽰display。页面内容被绘制到内存中,将内存内容显⽰在屏幕上。

渲染流程中事项

DOM/CSSOM作用

DOM作用

在渲染引擎中,DOM有三个层⾯的作⽤。

  • 从页面的视⻆来看,DOM是⽣成页面的基础数据结构。
  • 从JavaScript脚本视⻆来看,DOM提供给JavaScript脚本操作的接⼝,通过这套接⼝JavaScript可以对DOM结构进⾏访问,改变⽂档的结构、样式、内容。
  • 从安全视⻆来看,DOM是⼀道安全防护线,⼀些不安全的内容在DOM解析阶段就被拒之⻔外了。

CSSOM作用

  • 提供给JavaScript操作样式表的能⼒。
  • 为布局树的合成提供基础的样式信息。

构建DOM树流程

HTML解析器HTMLParser负责将HTML字节流转换为DOM结构。网络进程加载了多少数据,HTML解析器便解析多少数据。

在实际⽣产环境中,HTML源⽂件中既包含CSS和JavaScript,⼜包含图⽚、⾳频、视频等⽂件,处理过程复杂。

  1. 通过分词器将字节流转换为⼀个个Token,分为Tag Token、⽂本Token,其中Tag Token⼜分为StartTag、EndTag。

    V8编译时会将JS源码转换为Token,Token再转换为AST,和这种情况类似。

  2. 将Token解析为DOM节点。

  3. HTML解析器维护⼀个Token栈结构,遇到开始标签Token入栈,将DOM节点添加到DOM树中,遇到结束标签Token出栈。

    HTML解析器开始⼯作时,会默认创建⼀个根为document的空DOM结构,将⼀个StartTag document的Token压⼊栈底。

CSS/JS影响构建DOM树/布局树

  • CSS。没有、有包括行内、内嵌、外部引入。

    • 只有CSS时,CSS本身不影响DOM⽣成。但若是CSS+JS,CSS影响DOM⽣成。所以综合两种情况则CSS可能影响DOM生成
    • CSS影响CSSOM生成,所以CSS影响布局树生成
    • 预解析线程提前下载文件时,不管CSS⽂件和JavaScript⽂件谁先到达,都要先等到CSS⽂件下载完成并⽣成CSSOM,然后再执⾏JavaScript脚本,最后再继续构建DOM,构建布局树。
  • JS。没有、有包括内嵌script标签、外部引入script标签。

    • 渲染引擎在遇到JavaScript脚本时。

      • 因为JavaScript有修改CSSOM的能⼒,所以在执⾏JavaScript之前都会先下载CSS⽂件,解析为CSSOM,再执⾏JavaScript脚本。
      • 因为JavaScript可能修改当前已经⽣成的DOM结构,所以HTML解析器暂停DOM解析,JavaScript引擎介⼊执⾏script标签中脚本。脚本执⾏完成之后,HTML解析器恢复解析过程,继续解析后续内容,直⾄⽣成最终的DOM。如果js获取的dom还没有生成,则js代码不会对后续dom产生作用。
      • ⾃从网⻚中引⼊了JavaScript,就可以操作DOM树中任意⼀个节点,例如隐藏/显⽰节点、改变颜⾊、获得或改变⽂本内容、为元素添加事件响应函数等等, ⼏乎可以“为所欲为”了。
    • 内嵌script标签

    • 外部引入script标签。需要先下载JavaScript代码,其他流程同上。

      下载JavaScript⽂件阻塞DOM解析,下载⾮常耗时,会受到网络环境、JavaScript⽂件⼤⼩等因素影响。

      当渲染引擎收到字节流之后,会开启预解析线程分析HTML⽂件中包含的CSS、JavaScript等⽂件,提前下载好这些⽂件,同时开始下载任务,下载时间按照最久的那个⽂件。

    • JS影响DOM⽣成,优化办法:

      • ⽤CDN来加速JavaScript⽂件的加载。
      • 压缩JavaScript⽂件的体积。
      • 如果JavaScript⽂件中没有操作DOM相关代码,可以通过async 或defer将JavaScript脚本设置为异步加载。
    • JS影响DOM生成,所以JS影响布局树生成

加载阶段/页面首次渲染

发起URL请求到首次显示页面内容,在视觉上经历的三个阶段。

  1. 第一个阶段:请求发出去之后到提交数据阶段,这时页面展⽰出来的还是之前页面的内容。影响该阶段的因素主要是网络、服务器处理。
  2. 第二个阶段:提交数据之后渲染进程会创建⼀个空⽩页面,通常把这段时间称为解析白屏,包括解析HTML、下载CSS、下载JavaScript、⽣成CSSOM、执⾏JavaScript、生成DOM、⽣成布局树,准备⾸次渲染。瓶颈主要体现在下载CSS文件、下载JavaScript文件、执行JavaScript。
  3. 第三个阶段:等⾸次渲染完成之后,就开始进⼊完整页面的⽣成阶段了,页面会⼀点点被绘制出来。

白屏时间和首屏时间

白屏时间First Paint:指浏览器从响应用户输入网址地址,到浏览器开始显示内容的时间。

白屏时间 = 页面开始展示的时间点 - 开始请求的时间点

优化方案:预渲染

首屏时间First Contentful Paint:指浏览器从响应用户输入网络地址,到首屏内容渲染完成的时间。

首屏时间 = 首屏内容渲染结束时间点 - 开始请求的时间点

页面优化/渲染流程优化

页面优化,其实就是要让页面更快地显⽰和响应。页面在它不同的阶段,所侧重的关注点是不⼀样的,所以需要分析⼀个页面⽣存周期的不同阶段。

  • 加载阶段,是指从发出请求到渲染出完整页面的过程,影响到这个阶段的主要因素有网络、JavaScript脚本。
  • 交互阶段,主要是从页面加载完成到用户交互的整合过程,影响到这个阶段的主要因素是JavaScript脚本。
  • 关闭阶段,主要是用户发出关闭指令后页面所做的⼀些清理操作。

加载阶段/页面首次渲染

并⾮所有的资源都会阻塞页面的⾸次绘制,⽐如图⽚、⾳频、视频等⽂件就不会阻塞页面的⾸次渲染;⽽⾸次请求的HTML资源⽂件、CSS⽂件、JavaScript会阻塞⾸次渲染,因为在构建DOM的过程中需要HTML和JavaScript⽂件,在构造渲染树的过程中需要⽤到CSS⽂件。阻塞网⻚⾸次渲染的资源称为关键资源

优化方法:

  • 减少关键资源个数。关键资源个数越多,⾸次页面的加载时间就会越⻓。

    • 将CSS、JavaScript改成内联形式。

    • 如果JavaScript代码没有DOM或者CSSOM的操作,则可以改成sync或者defer属性,变成⾮关键资源。

    • 对于CSS,如果不是在构建页面之前加载的,link属性加上取消阻⽌显现的标志,变成⾮关键资源。

      对于⼤的CSS⽂件,通过媒体查询属性将其拆分为多个不同⽤途的CSS⽂件,特定的场景下动态加载

  • 降低关键资源⼤⼩。通常情况下,所有关键资源的内容越⼩,其整个资源的下载时间也就越短,那么阻塞渲染的时间也就越短。

    • 压缩CSS、JavaScript资源,移除HTML、CSS、JavaScript⽂件中不必要的注释内容。可通过webpack等⼯具实现。
  • 降低关键资源的RTT次数。

    往返时延RTT/Round Trip Time:当使⽤TCP协议传输⼀个⽂件时,由于TCP的特性,这个数据并不是⼀次传输到服务端的,⽽是需要拆分成⼀个个数据包来回多次进⾏传输的。RTT是这⾥的往返时延。它是网络中⼀个重要的性能指标,表⽰从发送端发送数据开始,到发送端收到来⾃接收端的确认,总共经历的时延。通常1个HTTP的数据包在14KB左右,所以⼩于14KB的文件1个RTT就可以解决,0.1M的页面就需要拆分成8个HTTP数据包传输,也就是说需要8个RTT。

    • 减少关键资源的个数和减少关键资源的⼤⼩搭配来实现。
    • 使⽤CDN来减少每次RTT时⻓。

交互阶段优化

即优化渲染进程渲染帧的速度,因为在交互阶段,帧的渲染速度决定了交互的流畅度。所以优化原则就是让单个帧的⽣成速度变快。

⼤部分情况下,⽣成⼀个新的帧都是由JavaScript通过修改DOM或者CSSOM来触发的。还有另外⼀部分帧是由CSS来触发的。

优化方法:

  • 减少JavaScript脚本执⾏时间,不要⼀次霸占太久主线程。

    • 是将⼀次执⾏的函数分解为多个任务,使得每次的执⾏时间不要过久。
    • 采⽤Web Workers。Web Workers是主线程之外的⼀个线程,在Web Workers中可以执⾏JavaScript脚本,不过Web Workers中⽆法通过JavaScript来访问DOM,所以我们可以把⼀些和DOM操作⽆关且耗时的任务放到WebWorkers中去执⾏。
  • 避免强制同步布局。正常情况下通过DOM接⼝执⾏添加元素或者删除元素等操作后,需要重新计算样式和布局,这些操作都是在另外的任务中异步完成的,避免当前的任务占⽤太⻓的主线程时间。如果在添加元素或者删除元素后获取元素的宽度⾼度等信息会触发强制同步布局

    • 尽量不要在修改DOM结构时再去查询⼀些相关值。
  • 避免布局抖动,在⼀次JavaScript执⾏过程中,多次执⾏强制布局和抖动操作。⽐强制同步布局更糟糕。如果在for循环语句重复执⾏计算样式和布局,触发布局抖动

    • 尽量不要在修改DOM结构时再去查询⼀些相关值。
  • 合理利⽤CSS合成动画。CSS动画在合成线程上执⾏,主线程被JavaScript或者⼀些布局任务占⽤时并不影响。

    • 如果能让CSS处理动画,就尽量交给CSS来操作。
    • 如果能提前知道对某个元素执⾏动画操作,那就最好将其标记为will-change,这是告诉渲染引擎需要将该元素单独⽣成⼀个图层。
  • 避免频繁地垃圾回收。垃圾回收操作发⽣时占⽤主线程,影响到其他任务的执⾏,严重的话还会让用户产⽣掉帧、不流畅的感觉。

    • 避免频繁地创建临时对象,避免产⽣临时垃圾数据,优化储存结构,尽可能避免⼩颗粒对象的产⽣。

显示图像

显示器作用

显⽰器用于如何显示图像,每秒固定读取60次显卡中前缓冲区中的图像,并将读取的图像显⽰到显⽰器上。

  • 每个显⽰器都有固定的刷新频率,通常是60HZ即每秒更新60张图⽚,图⽚都来⾃于显卡中前缓冲区。

显卡作用

显卡的职责就是合成新的图像,并将图像保存到后缓冲区中,⼀旦显卡把合成的图像写到后缓冲区,系统就会让后缓冲区和前缓冲区互换,这样就能保证显⽰器能读取到最新显卡合成的图像。通常情况下,显卡的更新频率和显⽰器的刷新频率是⼀致的。

帧 VS 帧率

渲染流⽔线⽣成的每⼀副图⽚称为⼀,把渲染流⽔线每秒更新了多少帧称为帧率,⽐如滚动过程中1秒更新了60帧,那么帧率就是60Hz/60FPS。

如何生成一帧图像/图片

有重排、重绘和合成三种方式。按照效率推荐合成方式优先,若实在不能满⾜需求则退后⼀步使⽤重绘或者重排的方式。

动画效果

当你通过滚动条滚动页面,或者通过⼿势缩放页面时,屏幕上就会产⽣动画的效果。之所以你能感觉到有动画的效果,是因为在滚动或者缩放操作时,渲染引擎会通过渲染流⽔线⽣成新的图⽚,并发送到显卡的后缓冲区。所以动画效果就是产生新的图片,产生图片的三种方式见上。

动画卡顿是因为渲染引擎⽣成某些帧的时间过久。

CSS动画与JS动画

写Web应⽤时,可能经常需要对某个元素做⼏何形状变换、透明度变换或者⼀些缩放操作,如果使⽤JavaScript来写这些效果,会牵涉到整个渲染流⽔线,所以JavaScript的绘制效率会⾮常低下。此时可以使⽤ will-change来告诉渲染引擎你会对该元素做⼀些特效变换。

CSS动画:
.box {
    will-change: transform, opacity;
}
  • 这段代码提前告诉渲染引擎box元素将要做⼏何变换和透明度变换操作,这时候渲染引擎会将该元素单独实现⼀层,等这些变换发⽣时,渲染引擎会通过合成线程直接去处理变换,这些变换并没有涉及到主线程,这样就⼤⼤提升了渲染的效率。这也是CSS动画⽐JavaScript动画高效的原因
  • 但是凡事都有两⾯性,每当渲染引擎为⼀个元素准备⼀个独⽴层的时候,它占⽤的内存也会⼤⼤增加,因为从层树开始,后续每个阶段都会多⼀个层结构,这些都需要额外的内存,所以你需要恰当地使⽤ will-change。

渲染进程中的不同线程

JS引擎线程和GUI渲染线程互斥。因为JS可以操作DOM,防止渲染过程中DOM被修改,GUI渲染线程在JS引擎线程后执行,GUI渲染线程放在任务队列的宏任务中。