blog icon indicating copy to clipboard operation
blog copied to clipboard

从浏览器关键渲染路径聊起

Open creeperyang opened this issue 2 years ago • 0 comments
trafficstars

(一)浏览器关键渲染路径-CRP

关键渲染路径是浏览器将 HTML,CSS 和 JavaScript 转换为屏幕上的像素所经历的步骤序列。优化关键渲染路径可提高渲染性能。

我们这里只尽量精简描述这个渲染过程,更具体的可以去看MDN-关键渲染路径

image

在浏览器接收html时(浏览器收到数据的第一块时就可以开始解析收到的信息):

  1. 首先解析 HTML 并构建 DOM 树;

    1. 遇到非阻塞资源(如图片)会请求这些资源并且继续解析。
    2. 遇到阻塞资源如
    3. 需要重点提CSS资源:CSS不会阻塞 HTML 的解析,但会阻塞 JavaScript(CSS后面的JavaScript需要等CSS下载完成),因为 JavaScript 经常用于查询元素的 CSS 属性。
      • JavaScript后面的CSS不会阻塞本JavaScript执行,并且本JavaScript查询的CSS属性也是不包含后面CSS内容的。
    4. 预加载扫描器帮助提前下载资源。
  2. 构建 CSSOM 树

  3. Style(也叫构建Render树)

    1. 渲染树包括了内容和样式:DOM 和 CSSOM 树结合为渲染树(或者叫样式树)。为了构造渲染树,浏览器检查每个节点,从 DOM 树的根节点开始,并且决定哪些 CSS 规则被添加。
  4. Layout(布局)

    1. Layout 是确定渲染树中所有节点的宽度、高度和位置,以及确定页面上每个对象的大小和位置的过程。
    2. Reflow 回流是对页面的任何部分或整个文档的任何后续大小和位置的确定。
    3. 第一次确定节点的大小和位置称为布局。随后对节点大小和位置的重新计算称为回流。
  5. Paint

    1. 最后一步是将像素绘制在屏幕上。
    2. 还有一步叫 Compositing,即不同层的合成,但讨论CRP时,把它作为Paint的一部分即可。讨论Reflow/Repaint时可以单独拆出讨论。

CRP 流程整体比较简单直接,我们讨论其中比较有意思的几个细节:

1. script 的 defer VS async

具有 async 属性的脚本在完成下载后和 load 事件之前第一时间执行。

  • 不能保证多个async脚本的执行顺序(谁先加载完谁先执行);
  • 如果脚本下载完,解析器还在工作,意味着会中断DOM构建。 image

具有 defer 属性的脚本在 HTML 解析完全完成之后执行,但在 DOMContentLoaded 事件之前执行。

  • 可以保证顺序;
  • 不会阻塞解析器。

image

如果在加载过程中更早地运行脚本很重要,请使用 async。

2. 怎么优化CRP?

  1. 优化网络和资源本身(不局限于CRP):

    • 利用CDN,利用HTTP2等,优化网络速度;
    • dns-prefetchpreconnect 等优化dns、tcp建立速度;
    • 资源压缩,tree-shaking 等清除无用代码等等,减小资源体积;
    • http缓存。
  2. 通过异步、延迟加载或者消除非关键资源来减少关键资源的请求数量,如defer/async/preload/prefetch

    • defer/async 来消除js对DOM构建的阻塞;
    • 通过代码分割,异步加载非首屏部分等等。
  3. 通过区分关键资源的优先级来优化被加载关键资源的顺序,来缩短关键路径长度。

    • preload/prefetch等优化资源加载顺序;
    • 资源放在合适位置:关键资源如CSS等放在 <head>;js放在<body>底部。

3. Reflow/Repaint 以及相关的优化

任何样式更新、DOM增删等等,造成布局(Layout)需要重新计算就称为回流(Reflow)。而不影响布局的节点样式/几何属性变更就是重绘(Repaint)。

回流严重影响性能,需要优化:

  1. 将频繁重绘或者回流的节点独立为新图层:will-change/translate3d/translatez 等等;
  2. 将频繁重绘或者回流的节点布局改为position:absolute/fixed
  3. visibility 替换 display: nonetransform 替代 top 等;
  4. 避免频繁操作DOM/更新样式,尽量批量读取/操作DOM样式。

(二)浏览器进程/线程角度看渲染流程以及 event loop

现代浏览器一般是多进程架构,以Chrome为例,一般会有以下进程:

  • 一个 Browser 进程(可以兼作 GPU 进程或者 Renderer 进程,即没有独立的 GPU 或者 Renderer 进程);
  • 一个 GPU 进程;
  • 多个 Renderer 进程,每个 Renderer 进程对应一个页面。

Renderer 进程:

  • Renderer 线程:运行Blink,称之为内核主线程,负责JS的解析执行,HTML/CSS解析,DOM操作,排版,图层树的构建和更新等任务;
  • Compositor 线程:运行Layer Compositor;
  • 其它线程,包括 worker 线程等等。

从架构上就可以看出,当 Renderer 线程被 JS 阻塞(忙于执行JS)时,卡顿掉帧是必然的。为了达到60FPS,必须限制JS运行时间、减少复杂的样式更新。

一桢内浏览器会做什么?

image

上面的图其实也某种程度就是浏览器的 event loop 流程

下面是几点需要注意的细节:

  1. JS 和 UI更新在同一个线程,所以JS长时间运行会阻塞UI;
  2. 当页面invisible时,帧数会主动降低(可能1000毫米以上调用一次timer);
  3. 善用 requestAnimationFrame。

(三)参考

  1. 关键渲染路径
  2. 渲染页面:浏览器的工作原理
  3. requestAnimationFrame Scheduling For Nerds
  4. 高效加载第三方 JavaScript

creeperyang avatar Mar 20 '23 15:03 creeperyang