原文地址:blog.logrocket.com/how-css-wor…
译文地址: github.com/yued-fe/y-t…
译者: 马头
校对者:罗磊张运领何玮

CSS 是一种神奇又古怪的力量,他控制着网页上我们看到的一切。本质上他应该很简单,但是真要写出可拓展、高性能的 CSS 又没那么容易。

不管你觉得 CSS 是“必经之痛”还是被大家误解了,CSS 依旧是每个 web 开发者必须掌握的能力。对CSS的理解深浅将会决定你的网站是光鲜亮丽还是平淡无奇。

这篇文章是深入了解 CSS 系列文章的第一篇。通过掀开CSS神秘的面纱,我们能够更深入的理解这门语言,让我们可以写出更快、更简洁、更漂亮的 CSS,来适应我们的应用程序规模和复杂度的增长。

在这篇文章中,我们将要研究页面初次加载后,CSS 是如何被渲染到屏幕上的。

我们之所以关心 CSS 的渲染过程,归结为下面两点:

加载时间

如果你的网站加载太慢,就算它的内容很有价值,用户也很有可能会在它加载完成前就关闭网页。一些研究表明,如果页面加载时间超过 3s,超过 50% 的用户会离开。

基于用户对于加载时间的期望,作为 web 开发者,我们应当减少用户的等待时间。不幸的是,CSS 通常是导致加载时间增加的罪魁祸首,所以如果你对 CSS 是如何被渲染到像素的有细致的了解,将会帮助你优化这关键的几秒,提升用户的留存。

然而,什么是关键渲染路径?

想让网页加载更快,首先需要区分关键资源与非关键资源。也许你已经应用了某些策略,让一些图片使用懒加载,根据路由拆分 JavaScript 按需加载(感谢 webpack)。这些在页面初次渲染完成后才加载的资源,就是我们所说的非关键资源,这些资源不会影响页面初次渲染的时间。那些会影响初次渲染时间的资源,才是至关重要的。

关键渲染路径是从浏览器收到 HTML 第一个字节起到开始渲染像素所要经历的最少步骤。本质上,它是浏览器对关键资源进行处理、渲染、展示给用户的过程。这个过程大致如下:

  • 根据收到的 HTML 创建 DOM(文档对象模型)
  • 如果遇到 CSS(内联或者外链)则开始创建 CSSOM(CSS 对象模型—后面会详细说明)
  • 如果遇到 JavaScript(非异步)则停止 DOM 构建,等待 CSSOM 构建完毕后再解析和执行。这么做的原因是 JavaScript 可能会修改和访问 DOM 和 CSSOM。

研究的第二个步骤:CSS 是如何影响关键渲染路径的。对于 JavaScript,我们使用了诸如 tree-shake、路由拆分、懒加载各种手段进行优化,对于 CSS 的优化则经常被我们忽视,实际上,未优化的 CSS 能轻而易举地让你的加载时间增加。

HTML 和关键渲染路径

既然我们的重点是 CSS,我们不会花太多篇幅在 DOM 构建上。然而,CSS 是一个样式标记语言,我们要知道它是如何和 DOM 工作的。

DOM 是包含页面所有 HTML 节点的类树形结构。每一个节点包含了 HTML 元素的数据(比如属性、id、class)。如果 HTML 元素有子节点,它会指向这些子节点。比如,下图中的 HTML 将会构建出右边的 DOM 结构。会发现 HTML 的缩进和 DOM 结构的十分相似。

从关键渲染路径这个角度来看,我们认为 HTML 是一个阻碍渲染的关键资源。在未解析完 HTML 之前,任何内容都无法被渲染。

创建 CSS对象模型

当浏览器解析到一个 CSS 样式表(不管是内联还是外链)时,需要解析文本,使之可以用于样式排布和绘制。这种数据结构就是 CSSOM。

CSSOM 长什么样子?下图中的 CSS 将会构建出右边的 CSSOM

本质上,我们通过解析所有的 CSS 选择器并将它插入到树中对应的位置。如果是一个单独的选择器,将会被添加到树的根节点下。嵌套的选择器将会被添加到嵌套的节点下。CSS 解析器将会从右往左遍历选择器来保证其正确性。

解析 CSS 到 CSSOM 和通过 HTML 构建 DOM 一样,是一个阻碍渲染的过程。如果不等待 CSSOM 的构建而开始渲染,将会导致用户短暂看到没有样式的内容,然后被应用上 CSS 的样式,这不是一个好的用户体验。

渲染树

浏览器用构建好的 CSSOM 和 DOM 来创建一个渲染树。简单来说,渲染树包含了需要渲染像素到屏幕上的所有信息。浏览器将 DOM 和 CSSOM 整合到一起,同时移除对渲染输出没有影响的内容。

浏览器首先会移除那些不可见的元素。包括 <head> <script> <meta> 这些标签,以及有 hidden 属性的 HTML 元素。这些元素虽然在其他地方有用到,但是并不会渲染到页面上,基于这个原理,浏览器渲染时能够确保渲染树上的所有节点都是可见的。

接下来,遍历 CSSOM,找到与渲染树上节点相匹配的 CSS 选择器。任何匹配到的 CSS 规则将会被应用到该节点上。

然而有一个 CSS 规则例外:display: none;它将会匹配到的节点从渲染树上完全移除,这样保证了只保留可见元素。其他隐藏元素的方法,如 opacity: 0;将不会从渲染树中移除,只是进行渲染却不显示。

当我们拥有了这个渲染树,一切准备就绪!在我们整合完 CSSOM 和 DOM 到渲染树后,渲染树就只包含了那些需要被渲染的信息,浏览器就可以使用它进行安全精确的渲染,这些信息没有冗余,也没有缺失。

冲刺阶段:布局和绘制

配备了完整的渲染树,浏览器已经可以开始渲染像素到屏幕上了。关键渲染路径的最后阶段包括两个步骤:布局和绘制。

布局是浏览器通过 CSS 规则计算 marginpaddingwidthposition,从而得到元素的位置和所需的空间的过程。在计算布局的时候,由于元素的位置、宽度、高度是由其父元素计算而来,浏览器从渲染树的顶端向下遍历。

如果你对 CSS 盒子模型很熟悉的话,本质上就是浏览器在页面上绘制了一系列 CSS 盒子(如果你想要了解盒子模型,可以阅读这篇)。

然而,要注意这个时候页面上还没有显示任何内容。想象成仅仅是在视窗上绘制了轮廓线,等待开始填充。

布局之后就是绘制阶段,然后我们就可以看到内容被渲染到页面上!这就是首像素渲染时间。浏览器遍历非布局的 CSS 规则并且填充 CSS 盒子。如果你用了多个图层,浏览器会保证其绘制到正确的图层。

请记住,一些 CSS 属性对页面负载有很大影响(比如,radial-gradient 比纯色渲染就更为复杂)。如果你在绘制过程中发现一些闪跳,减少这种渲染代价高的 CSS 规则可以显著提高网站的性能。

为什么要关心关键渲染路径中的 CSS?

你可以花尽可能多的时间来优化网站的 FPS(每秒的渲染帧数),使它看起来更好,或者通过 A-B test 来获得更高的转化率。但是如果你的用户在页面加载完成前离开了,这些都将变成无用功。

如果你在尝试提高页面加载速度,知道浏览器需要哪些步骤才能渲染出第一像素是至关重要的。既然浏览器在解析全部 CSS 之前会阻碍渲染,那么可以在 HTML 文档中去掉那些不会在首次页面渲染中使用到的 CSS 文件。这么做可以大幅度降低浏览器构建 CSSOM 和渲染树的时间。

那些在初次加载中并非必要的 CSS 可以被认为是非关键资源,可以通过懒加载,在用户看到初次渲染页面之后再加载(如果你的页面是一个单页应用,这将会特别重要,传送那些还看不到页面的 CSS 对性能有很大影响)。

理解 CSSOM 是如何构建的另一个好处,是可以对选择器性能有更深入的了解。因为嵌套的选择器必须检查 CSSOM 上的父节点,所以尽量避免使用嵌套的选择器,可以提升 CSSOM 的性能。然而,我想说的是,在大多数的应用程序中,它并不会成为性能的瓶颈,相对于重写 CSS 选择器,还有其他更值得优化的地方。

和其他 web 性能相关的问题一样,在修改 CSS 之前,你最好可以分析下加载时间。如果你在使用 Chrome,打开工具栏切换到 Perfomarnce 标签下。你可以通过 Recalculate Styles、 Layout 和 Paint 这些事件,看到 CSSOM 构建、排版、绘制所需的时间。然后你可以根据瓶颈来针对性的开始优化。

查看更多分享,请关注阅文集团前端团队公众号: 阅文集团前端团队(yuewen_YFE)