回流

回流又名重排,指几何属性需改变的渲染。但感觉回流这个词比较高大上,后续统称回流吧。

可理解成,将整个网页填白,对内容重新渲染一次。只不过以人眼的感官速度去看浏览器回流是不会有任何变化的,若你拥有闪电侠的感官速度去看浏览器回流(实质是将时间调慢),就会发现每次回流都会将页面清空,再从左上角第一个像素点从左到右从上到下这样一点一点渲染,直至右下角最后一个像素点。每次回流都会呈现该过程,只是感受不到而已。

渲染树的节点发生改变,影响了该节点的几何属性,导致该节点位置发生变化,此时就会触发浏览器回流并重新生成渲染树。回流意味着节点的几何属性改变,需重新计算并生成渲染树,导致渲染树的全部或部分发生变化。

重绘

重绘指更改外观属性而不影响几何属性的渲染。相比回流,重绘在两者中会温和一些,后续谈到的CSS性能优化就会基于该特点展开。

渲染树的节点发生改变,但不影响该节点的几何属性。由此可见,回流对浏览器性能的消耗是高于重绘的,而且回流一定会伴随重绘,重绘却不一定伴随回流。

为何回流一定会伴随重绘呢?整个节点的位置都变了,肯定要重新渲染它的外观属性啊!

属性分类

以下对一些常用的几何属性和外观属性分类,其实同种分类的属性都有一些共同点,各位同学可自行感受。

  • 几何属性:包括布局、尺寸等可用数学几何衡量的属性

    • 布局:displayfloatpositionlisttableflexcolumnsgrid
    • 尺寸:marginpaddingborderwidthheight
  • 外观属性:包括界面、文字等可用状态向量描述的属性

    • 界面:appearanceoutlinebackgroundmaskbox-shadowbox-reflectfilteropacityclip
    • 文字:textfontword
如何理解回流重绘

有无更好的方式可帮助理解回流重绘呢?答案是有的。

某一天星巴克发行一套很有纪念价值的杯子,男同胞们为了买到心仪的杯子给女友当惊喜礼物,通宵达旦搬张板凳去星巴克门口排队。此时形成的队伍是有序的,毕竟大家都是文明人,不可能随便插队吧,先到先拿,这个道理谁都懂!

可是总有一些人不按常理出牌,别人排队排得那么辛苦,他一到来就仗着自己有钱有势人多马多,插队到最前面。若他插队成功,那么后面的人都要往后挪一位。此时队伍就要重新往后挪,甚至引发多人斗殴。但混乱的情况总会被控制下来,此时就得重新排队,而原先的队伍顺序经过这次斗殴就可能不按照原先的队伍顺序排队了。几何属性变了,就要重新排队,这个就是回流或重排。重新排队啊😂!

一位漂亮妹纸排队排得久肚子呱呱叫,就与另一位同伴交换,她去买早餐,而这位同伴代替她的位置。各位男同胞可能发现这位妹纸更漂亮了。没错,外观属性改变了,变漂亮了,但除了妹纸,其余人的位置和顺序都无发生变化,所以肯定不会发生上述重新排队的情况。外观属性变了,但几何属性没变,这个就是重绘。不用重新排队,还有漂亮妹纸看,大家都很乐意🤔!

性能优化
回流重绘在操作节点样式时频繁出现,同时也存在很大程度上的性能问题。回流成本比重绘成本高得多,一个节点的回流很有可能导致子节点、兄弟节点或祖先节点的回流。在一些高性能电脑上也许无什么影响,但回流发生在手机上(明摆说某些安卓手机),就会减缓加载速度增加电量消耗

在上一章中引出一个定向法则:回流必定引发重绘,重绘不一定引发回流,可利用该法则解决一些因为回流重绘而引发的性能问题。在优化性能前,需了解什么情况可能产生性能问题,以下罗列一些常见的情况。

流必定引发重绘,重绘不一定引发回流,可利用该法则解决一些因为回流重绘而引发的性能问题。在优化性能前,需了解什么情况可能产生性能问题,以下罗列一些常见的情况。

  • 改变窗口大小
  • 修改盒模型
  • 增删样式
  • 重构布局
  • 重设尺寸
  • 改变字体
  • 改动文字

很多同学可能不知,回流重绘其实与浏览器的事件循环有关,以下源自对HTML文档的理解。

  • 浏览器刷新频率为60Hz,即每16.6ms更新一次
  • 事件循环执行完成微任务
  • 判断document是否需更新
  • 判断resize/scroll事件是否存在,存在则触发事件
  • 判断Media Query是否触发
  • 更新动作并发送事件
  • 判断document.isFullScreen是否为true(全屏)
  • 执行requestAnimationFrame回调
  • 执行IntersectionObserver回调
  • 更新界面

上述就是浏览器每一帧中可能会做到的事情,若在一帧中有空闲时间,就会执行requestIdleCallback回调。

回到正题,通过定向法则回流必定引发重绘,重绘不一定引发回流可知道,尽量减少回流重绘,就是CSS性能优化中一个很好的指标。

如何减少和避免回流重绘

使用visibility:hidden替换display:none

从以下四方面对比display:none和visibility:hidden,display:none简称DN,visibility:hidden简称VH。

  • 占位表现

    • DN不占据空间
    • VH占据空间
  • 触发影响

    • DN触发回流重绘
    • VH触发重绘
  • 过渡影响

    • DN影响过渡不影响动画
    • VH不影响过渡不影响动画
  • 株连效果

    • DN后自身及其子节点全都不可见
    • VH后自身及其子节点全都不可见但可声明子节点visibility:visible单独显示

两者的占位表现、触发影响和株连效果就能说明VH代替DN的好处,从两者区别中就能找出恰当的答案了。

使用transform代替top

top是几何属性,操作top会改变节点位置从而引发回流,使用transform:translate3d(x,0,0)代替top,只会引发图层重绘,还会间接启动GPU加速

避免使用Table布局

牵一发而动全身用在Table布局身上就很适合了,可能很小的一个改动就会造成整个<table>回流,有兴趣的同学可用Chrome Devtools的Performance调试看看,在此就不演示了。

通常可用<ul><li><span>等标签取代<table>系列标签生成表格。

避免规则层级过多

浏览器的CSS解析器解析css文件时,对CSS规则是从右到左匹配查找,样式层级过多会影响回流重绘效率,建议保持CSS规则在3层左右。

避免节点属性值放在循环里当成循环变量

for (let i = 0; i < 10000; i++) {
    const top = document.getElementById("css").style.top;
    console.log(top);
}

每次循环操作DOM都会发生回流,应该在循环外使用变量保存一些不会变化的DOM映射值。

const top = document.getElementById("css").style.top;
for (let i = 0; i < 10000; i++) {
    console.log(top);
}

动态改变类而不改变样式

不要尝试每次操作DOM去改变节点样式,这样会频繁触发回流。

更好的方式是使用新的类名预定义节点样式,在执行逻辑操作时收集并确认最终更换的类名集合,在适合时机一次性动态替换原来的类名集合。有点像vue的依赖收集机制,不知这样描述会不会更容易理解。

将频繁回流重绘的节点设置为图层

将回流重绘生成的图层逐张合并并显示在屏幕上。可将其理解成Photoshop的图层,若不对图层添加关联,图层间是不会互相影响的。同理,在浏览器中设置频繁回流或重绘的节点为一张新图层,那么新图层就能够阻止节点的渲染行为影响别的节点,这张图层里怎样变化都无法影响到其他图层。

设置新图层有两种方式,将节点设置为<video><iframe>,为节点添加will-changewill-change是一个很叼的属性,在第12章变换与动画中会详细讲解。

使用requestAnimationFrame作为动画帧

动画速度越快,回流次数越多,上述有提到浏览器刷新频率为60Hz,即每16.6ms更新一次,而requestAnimationFrame()正是以16.6ms的速度更新一次。所以可用requestAnimationFrame()代替setInterval()

属性排序

在进入属性排序这个话题前,先来看看以下两段CSS代码。

.elem {
    width: 200px;
    background-color: #f66;
    align-items: center;
    color: #fff;
    height: 200px;
    justify-content: center;
    font-size: 20px;
    display: flex;
}
.elem {
    display: flex;
    justify-content: center;
    align-items: center;
    width: 200px;
    height: 200px;
    background-color: #f66;
    font-size: 20px;
    color: #fff;
}

若不特别指明,可能各位同学觉得这两段代码无异样,顶多就是属性顺序不同。但仔细观察两段代码,就会发现第一段代码好像无依据地随便排列,而第二段代码好像按照某些规范顺序排列。

属性排序指按照预设规范排列属性。提供一个预设的约定规范,依据该规范以一定的顺序排列所有属性。

曾经笔者也是随机排列属性顺序,想到什么写什么,反正能实现就行。但反过来看,随意真的好吗,每次维护代码都需反复确认某个属性是否已经存在,混乱的属性排序让笔者有时无法在脑海里构思出更好的排版。所以笔者下意识去了解和认识属性排序,利用一些约定规范合理管理我的CSS代码。

想法

笔者有一个想法,就是按照回流重绘的原理,涉及到几何属性外观属性,结合盒模型规范和从外到里进行属性排序。综合太极图的哲学思想,将一些回流的几何属性排在最前面,毕竟这些属性决定了节点的布局、尺寸等和本质有关的状态,有了这些状态才能派生出节点更多的外观属性,逐一构成完整的节点。

好比一座摩天大楼的构筑过程,从打桩(存在)、搭设(布局)、主体(尺寸)、砌体(界面)、装修(文字)、装潢(交互)到验收(生成一个完整的节点),每一步都基于前一步作为基础才能继续下去。

太极图哲学思想:太极生两仪,两仪生四象,四象生八卦,从无极生太极开始,一直通过物质派生而构筑一个完整的立体结构,这一过程又显然是一个立体并包含位次顺序的四维关系

理解

假设编写一个节点样式,先声明display还是width呢?display决定了该节点的开始状态,是none,还是block,还是inline,还是其他。若先声明width,万一后续声明display:inline表示该节点是行内元素,行内元素无法显式声明宽高,那么width不是白白浪费了?所以推荐声明display在首位,毕竟它声明了该节点最开始的状态,有还是无

排序

根据上述想法和理解,笔者将属性排序按照布局 → 尺寸 → 界面 → 文字 → 交互的方式顺序定义。把交互属性放到后面是因为transformanimation会让节点重新生成新图层,上述有提到新图层不会对其他图层造成影响。

布局属性

  • 显示:display visibility
  • 溢出:overflow overflow-x overflow-y
  • 浮动:float clear
  • 定位:position left right top bottom z-index
  • 列表:list-style list-style-type list-style-position list-style-image
  • 表格:table-layout border-collapse border-spacing caption-side empty-cells
  • 弹性:flex-flow flex-direction flex-wrap justify-content align-content align-items align-self flex flex-grow flex-shrink flex-basis order
  • 多列:columns column-width column-count column-gap column-rule column-rule-width column-rule-style column-rule-color column-span column-fill column-break-before column-break-after column-break-inside
  • 格栅:grid-columns grid-rows

尺寸属性

  • 模型:box-sizing
  • 边距:margin margin-left margin-right margin-top margin-bottom
  • 填充:padding padding-left padding-right padding-top padding-bottom
  • 边框:border border-width border-style border-color border-colors border-[direction]-<param>
  • 圆角:border-radius border-top-left-radius border-top-right-radius border-bottom-left-radius border-bottom-right-radius
  • 框图:border-image border-image-source border-image-slice border-image-width border-image-outset border-image-repeat
  • 大小:width min-width max-width height min-height max-height

界面属性

  • 外观:appearance
  • 轮廓:outline outline-width outline-style outline-color outline-offset outline-radius outline-radius-[direction]
  • 背景:background background-color background-image background-repeat background-repeat-x background-repeat-y background-position background-position-x background-position-y background-size background-origin background-clip background-attachment bakground-composite
  • 遮罩:mask mask-mode mask-image mask-repeat mask-repeat-x mask-repeat-y mask-position mask-position-x mask-position-y mask-size mask-origin mask-clip mask-attachment mask-composite mask-box-image mask-box-image-source mask-box-image-width mask-box-image-outset mask-box-image-repeat mask-box-image-slice
  • 滤镜:box-shadow box-reflect filter mix-blend-mode opacity,
  • 裁剪:object-fit clip
  • 事件:resize zoom cursor pointer-events touch-callout user-modify user-focus user-input user-select user-drag

文字属性

  • 模式:line-height line-clamp vertical-align direction unicode-bidi writing-mode ime-mode
  • 文本:text-overflow text-decoration text-decoration-line text-decoration-style text-decoration-color text-decoration-skip text-underline-position text-align text-align-last text-justify text-indent text-stroke text-stroke-width text-stroke-color text-shadow text-transform text-size-adjust
  • 字体:src font font-family font-style font-stretch font-weight font-variant font-size font-size-adjust color
  • 内容:overflow-wrap word-wrap word-break word-spacing letter-spacing white-space caret-color tab-size content counter-increment counter-reset quotes page page-break-before page-break-after page-break-inside

交互属性

  • 模式:will-change perspective perspective-origin backface-visibility
  • 变换:transform transform-origin transform-style
  • 过渡:transition transition-property transition-duration transition-timing-function transition-delay
  • 动画:animation animation-name animation-duration animation-timing-function animation-delay animation-iteration-count animation-direction animation-play-state animation-fill-mode

在此已经整合了95%的属性,可满足95%的属性排序。其他未列入的属性,可根据自身使用习惯添加。当然笔者的属性分类只提供参考。

配置
纯粹靠在编码过程中按照约定规范排列属性肯定是有难度的,也不方便频繁修改代码。每次编码时都记住这些属性排序估计也挺费脑力的,这么多属性,肯定使用工具自动化处理啊!推荐一个自动排列属性的网站Csscomb,在学前准备那章已经安装了VSCode的Csscomb,接下来就配置一键排序。

该插件貌似只有存档,主软件包已经无维护者了,后续估计也不会再更新
官网内容已经不复存在,以前是一步一步显示配置再选择适合自己的配置,最终生成一个json文件。配置详情请戳这里,以下的全局配置也是依据文档处理的,当然你也可对工作区设置。

打开VSCode,Window系统选择ctrl+, → 用户 → 右上角第二个图标(打开设置),Mac系统选择cmd+, → 用户 → 右上角第二个图标(打开设置),在json文件里插入以下配置。


{
    "csscomb.formatOnSave": true, // 保存代码时自动格式化
    "csscomb.preset": {
        "always-semicolon": true, // 分号结尾
        "block-indent": "\t", // 换行格式
        "color-case": "lower", // 颜色格式
        "color-shorthand": true, // 颜色缩写
        "element-case": "lower", // 元素格式
        // "eof-newline": false, // 结尾空行
        "leading-zero": false, // 保留前导零位
        // "lines-between-rulesets": 0, // 规则间隔行数
        "quotes": "double", // 引号格式
        "remove-empty-rulesets": true, // 剔除空规则
        "space-between-declarations": "\n", // 属性换行
        "space-before-closing-brace": "\n", // 后花括号前插入
        "space-after-colon": " ", // 冒号后插入
        "space-before-colon": "", // 冒号前插入
        "space-after-combinator": " ", // 大于号后插入
        "space-before-combinator": " ", // 大于号前插入
        "space-after-opening-brace": "\n", // 前花括号后插入
        "space-before-opening-brace": " ", // 前花括号前插入
        "space-after-selector-delimiter": "\n", // 逗号后插入
        "space-before-selector-delimiter": "", // 逗号前插入
        "strip-spaces": true, // 剔除空格
        "tab-size": true, // 缩进大小
        "unitless-zero": true, // 剔除零单位
        "vendor-prefix-align": false, // 前缀缩进
        "sort-order": [] // 属性排序
    }
}

sort-order是一个数组,由于属性太多就不一一插入了,按照上述分类好的排序逐个插入即可,"sort-order":["display", "visibility", ...]。配置详情请戳这里。

配置完成后,若觉得每次保存时格式化CSS代码会影响编辑器性能,可为Csscomb配置快捷键,在有需时再格式化CSS代码。Window系统选择ctrl+K+S → 用户 → 右上角第一个图标(打开键盘快捷方式)Mac系统选择cmd+K+S → 用户 → 右上角第一个图标(打开键盘快捷方式),在json文件里插入以下配置。

[{
    "key": "ctrl+alt+c", // "cmd+alt+c"
    "command": "csscomb.execute"
}]

全选代码或选择局部代码,执行ctrl/cmd+alt+c,自动格式化代码且自动排列属性,一个字,爽🤔!