彻底了解渲染引擎以及几点关于性能优化的建议

1,286 阅读7分钟

前言

在日常开发过程中,要编写性能足够优秀的代码,构造更加稳定的应用,我们不仅要对javascript本身的执行机制有深入的了解,更要对其宿主环境有更加深刻的认识,理解其工作原理以及组成结构,它可以帮助我们对web世界的运转模式有更高层级的认知。这次想要介绍的是浏览器的渲染引擎。

浏览器的构成

在具体介绍渲染引擎之前,我们先来看看浏览器的构成,看看渲染引擎在浏览器中扮演的是一个怎样的角色。关于浏览器的构成可以参见下图:


如上图所示,浏览器从外到内的组成包括了以下几个部分:

  1. User interface,即浏览器的视觉外观,具体包括其地址输入栏,前进后退键,书签菜单栏等等。
  2. Browser engine, 浏览器引擎,它主要处理User interface与render engine,即渲染引擎之间的交互。
  3. Rendering engine, 即本文介绍的重点-渲染引擎,它负责解析html以及css,并将解析后的内容渲染到屏幕上,完成web页面的展示。
  4. Networking, 浏览器的网络处理层,主要负责处理xhr之类的网络请求,之后我也会专门写一篇文章来详细介绍它。
  5. Javascript engine, 负责javascript的运行时处理,关于它我之前已经专门从内存管理和异步执行方面写了两篇文章,没有看过的可以参见我的专栏哦。
  6. Data persistence,数据持久化,即浏览器的本地数据存储,目前浏览器所支持的几种本地数据存储方式包括有localstorage,indexDB,webSQL以及FileSystem。
    了解了渲染引擎在整个浏览器中的角色作用后,我们回到渲染引擎本身,看看它是如何完成页面渲染的。

渲染过程

渲染引擎接收到网络层传递过来的页面文档内容后,大致的解析处理过程如下:

dom树构造

首先解析html来构成dom树,假设有如下html文档内容:

<html>
  <head>
    <meta charset="UTF-8">
    <link rel="stylesheet" type="text/css" href="theme.css">
  </head>
  <body>
    <p> Hello, <span> friend! </span> </p>
    <div> 
      <img src="smiley.gif" alt="Smiley face" height="42" width="42">
    </div>
  </body>
</html>

解析后,其dom树构造示意图如下:


可以看出dom树中每个节点的父子关系与html元素的父子关系保持一致。
dom树构造完成后还不能直接生成render tree,还需要cssom树的配合。

cssom树

cssom是指css object model,当浏览器在解析html时如果在head中遇到了连接到外部css文件的link标签,浏览器就会立刻发起请求获取该css文件的内容,需要注意的是css文件的获取和解析不会阻塞html的解析,但是script 标签的内容无论是下载还是执行都会阻塞html解析。假设页面中的css内容如下:

body { 
  font-size: 16px;
}

p { 
  font-weight: bold; 
}

span { 
  color: red; 
}

p span { 
  display: none; 
}

img { 
  float: right; 
}

浏览器会将其转换成如下的cssom树:


也许你会奇怪为什么css也会有这样的树形结构,这是因为浏览器在为某个dom对象计算最终的样式规则时,是先从最一般的规则开始,然后才是具体指定的规则。比如上述示例中,对于span标签,会先添加body中的font-size为16px的规则,然后才是它自己定义的规则,如果span标签包裹于某个p标签下还会添加display为none的规则,总结下来就是先apply父级规则,然后apply specific rule.

render树

上述两项工作完成后,通过dom树与cssom树的结合就可以生成render树,或许你会问render树到底是什么?为什么一定要先生成render树,而不能直接用dom树和cssdom树去做paint呢?有这样的疑问很好,所谓render树,它其实是拥有样式表现的可见元素按照其文档顺序构造而成的树形结构,生成它的目的是确保元素的渲染过程是严格按照文档流顺序以及样式规则进行的。rener tree的示意图如下:

layout

render树虽然构造完成,但是其中的节点还需要进行位置和尺寸的计算,这些数值的计算过程就是layout.
layout是一个递归的过程,它从根元素也就是html元素开始计算,位置计算的坐标系也是相对于根元素,html元素坐标为(0,0).后续的计算可能是局部更新也有可能是整体替换。 layout过程结束后就意味着每个节点都会获得它将在屏幕上展示的位置坐标,可以开始进行真正的渲染过程了。

painting

在这个阶段,浏览器就会把整个文档结构展示在页面上,与layout一样,painting也有局部更新和全部更新两种可能。这取决于你的dom操作机制。 painting是一个渐进的过程,为了更好的UX体验,渲染引擎不会等到所有html全部解析完成后才开始,而是先解析完成的部分先绘制,其余部分解析完成后再行绘制。
至此渲染引擎的整个执行流程已经结束,了解了渲染引擎的执行机制,下面我们就来看看可以从哪些方面入手去做页面的优化,以获得更好的用户体验。

关于性能优化

从渲染引擎的角度,我们可以从一下五个方面入手去做性能优化.

  1. javascript, 在js代码编写过程中我们需要更多的注意会引起视觉变化的操作,比如dom操作等,尤其是在单页应用中,这样的场景更加常见。关于javascript方面的优化,我的建议是:
  • 避免使用setTimeout或者setInterval这类定时器去操作视觉更新,因为它们的执行机制并不精准,有可能会离我们想要的时机相去甚远。
  • 将计算量大的操作交给web workers,因为js的执行会阻塞页面的更新以及对用户交互的响应.
  • 如果需要异步的操作dom,那么请选择用microtask的方式,比如mutationObserver。
  1. css,在css编写过程中要尽量减少选择器的复杂度,相比给某个元素确定其样式规则,元素选择器的计算要多消耗50%的时间。
  2. layout, 在layout过程中浏览器需要确定每个元素的坐标和尺寸,这意味着layout是一个计算密集型的过程,所以我们需要尽量减少重复触发layout。针对layout,我的优化建议是:
  • 减少对元素位置和尺寸有影响的属性的操作,比如width,height,left,top等等,这些操作会使浏览器重新进行layout.
  • 尽可能使用flexbox进行布局,它比传统的基于盒模型的布局有更好的性能优势。
  • 避免强制触发layout,浏览器对于dom操作和属性的变化是有原生优化机制的,它会等到合适的时机将多个操作集中执行以避免高频触发layout,但如果你在操作或更新了某个dom之后立即访问它的某些属性,比如offsetHeight这些,它就会立刻触发layout,我们要尽量避免这样的访问。

总结

这篇文章主要介绍了浏览器渲染引擎的执行机制,相对来说是一篇非常偏基础知识的文章,也是lz我最近对前端基础重新梳理回顾对一次思考总结,希望也会对你有帮助。