Day12 X Writing High Performance CSS

CSS 是前端开发者不可不学的技术之一,没有了它就好像你做出来的网页都没有穿衣服一样,有点羞於见人呀!?

但你有想过 CSS 如果乱写也可能会影响到网页的效能吗?
叮咚!您今天的晚餐送来罗!是一份 Writing High Performance CSS 的大补帖!一起来看看会影响页面效能的 CSS 的眉眉角角吧 ?

在开始之前

其实浏览器解析 CSS 的速度非常快,所以今天所说的技巧带来的效能收益可能没有你想的那麽显着。相比撰写良好 CSS 带来的优化,大部分时间我们应该聚焦在减少网路请求、减少 JavaScript bundle size、图片最佳化等会非常显着影响页面效能或载入时间的优化技巧。

也因为 CSS 不太会对性能产生「巨大」的影响,所以在撰写 CSS StyleSheet 时应该始终以可读性与可维护性为原则。不过不能因为不会对性能产生「巨大」的影响,就自顾自的乱写一通,觉得样式有出来就好。还记得第一天建立的 mindset 吗?

效能优化是一段追求好还要更好的过程。

学会撰写对提升效能较有帮助的 CSS,尤其是在开发大型专案时.累积起来也可能有不少优化空间。相反的,如果一直不管品质,也许累积起来在某些情况下也会成为效能的瓶颈。

So, Why Not ?

CSS Selector

不同於我们阅读与撰写是从左至右的习惯,CSS Selector 的匹配是反过来「从右向左进行的」。(这点我居然到写程序的第二年才知道啊!!)

这也导致不同的 Selector 写法之间的效能也存在着一些差异。

例如:

#ironman .article h2 {
/*     Some Style */
}

使用这个 selector 需要先找到 DOM 中所有的 h2 元素,再 filter 掉祖先元素不是 .article 的,最後再 filter 掉祖先元素不是 #ironman 的元素。

这个 selector 相比於直接用 id selector 例如 #ironman-article-h2 会需要花更多的时间才能生成 render-tree。

不同 CSS Selector 的性能

  1. ID selector ID选择器
  2. Class Selector 类别选择器
  3. Element Selector 元素选择器
  4. General Sibling Combinator 兄弟选择器
  5. Child Combinator 子选择器
  6. Descendant Combinator 後代选择器
  7. Attribute Selector 属性选择器
  8. Pseudo Element/Class Selector 伪元素选择器

以上是常见的选择器,按照效能排列从上到下效能由高到低。
(注:ID Selector 与 Class Selector 效能其实差异不大)

CSS Selector 优化 Tips

我们现在已经知道 CSS Selector 是「从右到左」查找,也介绍了单个选择器的效能比对,接着就可以来看看一些被推荐的 CSS Best Practice 写法。

千万不要乱用万用字元 - *

#ironman * {
  color: yellow;
  font-size: 12px;
}

因为是「从右至左」去寻找,所以这个选择器会先遇到万用字元而匹配到页面上所有的元素,这是一个成本非常大的操作。

(有学过 SQL 的应该也知道 SELECT * FROM 是很耗资料库效能的语法)

所以一个 CSS 选择器的效能关键通常都在最右侧,这个最右侧的选择器又被称作「关键选择器」。

不是都用效能最好的 ID Selector 就好

前面有提到 ID Selector 的效能是最好的,「欸!那就全部都用 ID Selector 就好啦!今天可以下班罗!」等等等,这样我们得对页面上所有元素都指定 id attribute,况且 id 还必须是唯一不能重复的识别码,你觉得现实中这可能吗?嗯...看来是一个效能与最佳实践的取舍。

不过刚刚也有提到其实 Class Selector 的效能不会比 ID Selector 差太多,所以大部分的状况是建议使用 Class Selector,而不是 ID。不然你想想,如果是采用元件化开发,会有重复使用元件的状况,那重复 ID 反而成为了一个问题,虽然可以动态传入参数丢到 id attribute 里,但并不是那麽好维护。

ID Selector 适用於标识长久存在且唯一的元素,例如页面中的 nav bar 或 header,就适合使用 ID Selector:

#navbar {
  background-color: green;
}

避免在 ID 或是 Class Selector 前面限定元素类型

div#navbar {}
p.ironman {}

我以前并不觉得这样的写法有问题,不过我们用 CSS「从右至左」匹配元素的思路来分析看看。

首先看到 #navbar 而先去找到 id 为 navbar 的元素,注意,id 是不会重复的,所以照理来说已经找到我们要的元素了,不过因为发现左边有一个 div,所以还是要额外再去分析它,当然最後结果跟直接用 #navbar 去匹配是一样的,反而还额外做了一次判断。

Child Combinator 子选择器 优先於 Descendant Combinator 後代选择器

如果今天你想找包在 div 里面的 p tag(一层的父子关系),那麽 div>p 这个写法的效能会比 div a 这个写法还要好。

一样从右边的选择器开始看,两者都会先去搜寻页面上的所有 p tag,不过前者只会检查一个层级,看看这些 p tag 的父层有没有是 div tag 的。後者则会一层层往上查找,父层没有就往父层的父层查找。相比起来前者耗费的工会少许多。

善用 CSS 属性继承的特性

父元件的 CSS Properties 是会继承到子元件上的,所以如果子元件要沿用父元件的样式,建议使用继承的特性,而不是在每个子选择器都设定一次。

/* This is not good */
#ironman {}
#ironman > .kyle { color: blue; }
#ironman > .mo { color: blue; }

/* This is better */
#ironman { color: blue; }

非必要的情况,减少使用昂贵的 Attribute

所谓昂贵的属性指的是一些需要浏览器进行操作或计算的属性,它们需要耗费更多的浏览器性能。当页面重绘时,这些属性可能会降低浏览器的渲染效能。例如:

  • :nth-child
  • filter
  • opacity
  • box-shadow
  • border-radius

不过其实这些都是很常见且很重要的属性呢,所以并不是叫大家不要使用,而是建议当你的需求有其他方式可以达成时,可以思考一下是不是真的需要使用这些属性。

要用 CSS 来写动画时,通常会用 transition property 来指定哪些 CSS properties 会被转场效果影响、transition duration、transition timing function 与 transition delay。

举例来说你希望指定 class name 为 ironman 的元素在 hover 时 scale 会变 1.5 倍,你希望这个过程是有平滑的动画的,你可能会这样写:

.ironman {
    scale: 1; 
    transition: all .2s ease-in;
}

.ironman:hover {
    scale: 1.5;
}

但其实如果你已经明确知道 transition 的对象是 scale,你应该这样写

/* 不要用 all */
.ironman {
    scale: 1; 
    transition: scale .2s ease-in;
}

transition: all 的写法会让浏览器在特定情况下会额外去做一些判断,相较於 specific 的写法效能会比较差。

优化 Reflow & Repaint

在 Day008 的时候有提到 reflowrepaint,我们知道不同的改变样式的方式,是会触发不同渲染流程的,因此也是效能优化的一个方向。

减少 Reflow

Reflow 会导致浏览器重新计算页面元素的 Layout 并重新构建 Render Tree,这一过程会降低浏览器的渲染速度,因此如果非必要的状况下我们应该避免频繁触发会造成 Reflow 的操作。我们来看看修改哪些常见的 CSS Properties 会造成 Reflow:

  • 改变元素的 margin 或 padding
  • 改变元素的 width, height, position 的 left 或 top
  • 改变 font-size 或 font-family
  • 改变视窗大小

当然并不是说禁用这些属性,而是使用前可以想一下是否有别的方式可以完成需求,例如要使用 CSS 动画移动元素位置应该使用 transform 而不是去改变 margin。在 Day008 的时候有说过 transform 只会触发 compositing,而改变 margin 则会触发 reflow + repaint + compositing。

不同 layout 方式的 reflow 性能也不同,flex 在 reflow 时比起 inline-block 与 float 有更好的性能,在布局时可以优先考虑。

避免不必要 Repaint

基本上在更新页面时 repaint 是比较难去避免的,除非像是 tarnsform 这种 Compositing Only 的属性,不然举凡像是改变颜色,改变背景图片等操作虽然不用重新 layout,却还是会经过 repaint。

不过我们可以试着去「避免」触发不必要的 repaint,例如 Day008 有提到的一次一起读取完再一次一起修改,因为浏览器底层机制的关系只会触发一次 reflow 与之後的流程。或是避免使用多个 statement 修改 style,而是改用新增或移除 CSS class 的方式。

当使用者在滚动页面的时候,去触发 hover 事件是没什麽意义的事,可以在滚动的时候禁用 hover 事件,提升 frame rate 让页面的滚动更为流畅。要做到这件事可以透过 CSS 的 pointer-events property。来一个简易版 Demo:

.disable-hover,
.disable-hover * {
  pointer-events: none !important;
}
let timer;

window.addEventListener('scroll', function() {
  const bodyClassList = document.body.classList;
  
  // clear previous timeout function
  clearTimeout(timer);
    
  if (!bodyClassList.contains('disable-hover')) {
    // add the disable-hover class to the body element
    bodyClassList.add('disable-hover');
  }
        
  timer = setTimeout(function() {
    // remove the disable-hover class after a timeout of 500 millisecond
    bodyClassList.remove('disable-hover');
  }, 500);
  
}, false);

建议大家可以到 CSS Triger 这个网站逛逛,看看各个 CSS Property 会不会造成 reflow 与 repaint。

Extract Critical CSS

因为浏览器得下载与解析 CSS 後才能呈现页面,所以 CSS 是一个「Render-Blocking-Resource」。如果在网路状况不好或 CSS 档案很肥大的状况下,对於 CSS file 的请求可能会让页面的渲染时间大幅度增加。

Critical CSS (内联首屏关键CSS) 是一个把 above-the-fold content(也就是页面初次载入且在滚动之前的萤幕可视区范围)的 CSS 提取出来并直接内联到 HTML 的技术。

不过 above-the-fold content 的范围并没有一个明确的界定值,因为每个装置的大小与萤幕尺寸都不会一样。

所谓将 CSS 内联到 HTML 大概是这个概念:

而不是引入外部 CSS 档案的方式。
这麽做的好处是让重要样式可以不必透过额外网路请求去抓取,可以使浏览器开始页面渲染的时间提前,减少 First Contentful Paint 的时间。

嗯...看起来挺好的,不过我想到了今天大家应该都深刻了解到看起来很棒的技术背後其实都躲着一些 trade off ?

Critical CSS 的限制是不适用於太肥大的 CSS,不然会延迟 HTML 其余部分的传输。这部分跟 TCP 的机制有关,直接看看 web.dev 文件的说法:

New TCP connections cannot immediately use the full available bandwidth between the client and the server, they all go through slow-start to avoid overloading the connection with more data than it can carry. In this process, the server starts the transfer with a small amount of data and if it reaches the client in perfect condition, doubles the amount in the next roundtrip. For most servers, 10 packets or approximately 14 KB is the maximum that can be transferred in the first roundtrip.

另外一个缺点是 inlined CSS 没有办法像外部 CSS 档案一样被浏览器快取。所以 Critical CSS 在使用上必须谨慎一点,建议可以去 lighthouse 检测看看页面是不是有严重的 render-blocking-resource 的问题。

至於到底要怎麽抽离出 Critical CSS,有一些 npm module 可以使用,例如 criticalcriticalCSSpentHouse,这些 module 也有对应的 plugins 可以整合到自动化工具例如 gulp 或 webpack 的 workflow 中,有兴趣的读者再自行研究罗。

Preload

还记得 Day00 提到的 <link rel="preload" /> 吗?是时候派上用场啦!

Preloading resources defined in CSS

像是在 CSS 中指定的 backgroumd-image 会等到浏览器载入且解析完 CSS 後才发现他们的存在并开始下载,但身为开发者的我们是事先知道会需要这个资源的,可以针对这个图片先做 preload。 其余类似的属性也可以这麽做。

Preloading CSS files

如果采用了上面介绍的 Critical CSS 的方法,网页的 CSS 被分成了两个类别:

  • Critical Inlined CSS or rendering the above-the-fold content
  • Non-Critical CSS

这些 Non-Critical CSS 就可以根据自己的需求适时使用 preload 来提早开始下载的时机。

本日小结

经过今天後,有没有觉得要写出高效能的 CSS 也是十分不容易的啊!
我自己觉得初学者比较容易误解的地方是 CSS 的匹配顺序并不是像一般阅读习惯的从左到右,而是「由右至左」,理解这个道理後再思考不同 Selector 的性能就会豁然开朗了!(我自己真的在学网页开发後过了一年多才知道这件事QQ)

今天也介绍了许多对性能有帮助的 CSS 技巧,不过有些技术要不要使用真的得依照实际状况谨慎考虑了,毕竟大多数提升性能的背後都有它们的 trade off 在。不过有些思维很值得记起来,例如在写 CSS 时去想一下 reflow repaint 的流程,下手前先思考一下有没有更好的解法。学会撰写 high performance 的 CSS,累积起来也会对性能有可观的帮助。

明天的主题严格上来说还是属於 CSS 的范畴,来看看怎麽利用 GPU 加速来提升网页的效能,我们明天见!

References & 含图片来源

https://domhabersack.com/high-performance-css
https://domhabersack.com/css-hierarchy-matching
https://iter01.com/13467.html
https://www.smashingmagazine.com/2016/12/gpu-animation-doing-it-right/
https://web.dev/extract-critical-css/
https://web.dev/first-contentful-paint/


<<:  YC SUS Group Session

>>:  Day27 数据的压缩

[Day5] 学 Bootstrap 是为了走更长远的路 ~ Flex 篇

前言 这几天写下来, 真的深深感受到我参加的是「自我挑战组」, 真的每天都在 自我挑战 跟天窗奋斗o...

Day 9:使用 Typora 发表你的第一篇 Hexo 文章

今天我们正式要使用 Markdown 撰写你的第一篇部落格文章啦! 用指令建立文章或草稿 .md 档...

大共享时代系列_000_Share

当队友如火如荼的产出铁人技术文章的时候,小人在下,左思右想,觉得自己的键(笔)记深不过这些技术咖时,...

Day 4 资料单向绑定

今天会来介绍 v-text与 v-html,在这之前会提到Mustache一下子 Mustache ...

Day26 - 针对 Metasploitable 3 进行渗透测试(7) - 利用 Meterpreter 後渗透

何谓後渗透 当恶意攻击者入侵企业之後,会从第一台入口点开始往内部进行攻击,有些企业会使用 Windo...