03-基础篇:回流重绘
juejin_logo copyCreated with Sketch.

前言

上章梳理了浏览器三大核心内容:渲染引擎渲染过程兼容性。其中渲染过程的回流重绘CSS中很重要的概念。了解与认识它们,可编写性能更好的代码。

有些同学说,怎么不开发完毕再找时间优化代码?试问有多少同学在项目开发完毕会拿出空余时间重构或优化代码。何必不在编码时对代码实现一次完美的优化?接着隆重介绍本章两位主角。

回流

回流又称重排,指改变几何属性的渲染。感觉“回流”较高大上,后续统称回流吧。

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

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

重绘

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

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

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

属性分类

以下对一些常见几何属性外观属性分类,其实同种分类的属性都有一些共同点,大家可自行感受。推荐一个属性渲染状态可视化的网站CssTriggers,可查看每个属性在渲染时产生的影响。

  • 几何属性:包括布局、尺寸等可用数学几何衡量的属性
    • 布局: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:nonevisibility:hiddendisplay:none简称DNvisibility: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 DevtoolsPerformance调试看看,在此就不演示了。

通常可用<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依赖收集机制,不知这样描述会不会更易理解。

大家可研究强大的classList,它能满足我所说的需求。

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

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

设置新图层有两种方式,将节点设置为<video><iframe>,为节点声明will-changewill-change是一个很叼的属性,第12章会详细讲述。

使用requestAnimationFrame作为动画帧

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

属性排序

在进入属性排序的话题前,先来看看以下代码。

.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;
}

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

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

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

曾经有一个著名的网站CSSTricks做了一份属性排序的调查问卷,调查结果如下。

调查问卷

  • A:随意排序占39%
  • B:根据类型排序占45%
  • C:根据单行代码长度排序占2%
  • D:根据属性字母排序占14%

发现B选项占比最多,很可惜,这份调查问卷都是针对国外开发者,所以我在自己的微信粉丝群中发起了调查问卷,结果还是B选项占比最多。

调查问卷

因为人数过少,怕可信度不高,所以我又去Github中随机寻找200个国内项目,通过一个周末的时间细心对比了所有css文件,统计出以下结果。

  • A:随意排序占38%
  • B:根据类型排序占58%
  • C:根据长度排序占1%
  • D:根据字母排序占3%

结果还是B选项占比最多,不过这也说明不了什么问题。毕竟CSS编码的灵活性比JS编码更高,随意也是一件不错的事情。可能就是在维护代码时眼花缭乱,问了一位编码很随意的同事(每次开发项目时都把Lint关掉,搞到每次Commit代码都手忙脚乱),他如实说出了随便排列属性顺序经常会重复编写某些属性,导致属性冗余。

其实属性排序有很多优点。

  • 突出CSS艺术之美
  • 防止属性重复编写
  • 快速定位到问题代码
  • 锻炼无视图架构网页的能力
  • 快速在脑海中构思排版与布局
  • 提高代码的可读性与可维护性

很多开发者都会给属性做排序,可见大家对属性排序都是持有肯定的态度,只在排序方式中会有一定的分歧。根据长度排序根据字母排序是较简单易用的排序方式,但忽略了属性间的关联性。根据类型排序又会分为很多种,主要还是围绕着盒模型

  • 根据类型排序
  • 根据长度排序
  • 根据字母排序

属性排序并不会影响样式的功能与性能,只是让代码看起来更简洁更规范。

想法

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

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

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

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

排序

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

布局属性

  • 显示:displayvisibility
  • 溢出:overflowoverflow-xoverflow-yscroll-behaviorscroll-snap-align
  • 浮动:floatclear
  • 定位:positionleftrighttopbottomz-index
  • 列表:list-stylelist-style-typelist-style-positionlist-style-image
  • 表格:table-layoutborder-collapseborder-spacingcaption-sideempty-cells
  • 弹性:flex-flowflex-directionflex-wrapjustify-contentalign-contentalign-itemsalign-selfflexflex-growflex-shrinkflex-basisorder
  • 多列:columnscolumn-widthcolumn-countcolumn-gapcolumn-rulecolumn-rule-widthcolumn-rule-stylecolumn-rule-colorcolumn-spancolumn-fillcolumn-break-beforecolumn-break-aftercolumn-break-inside
  • 格栅:grid-columnsgrid-rows

尺寸属性

  • 模型:box-sizing
  • 边距:marginmargin-leftmargin-rightmargin-topmargin-bottom
  • 填充:paddingpadding-leftpadding-rightpadding-toppadding-bottom
  • 边框:borderborder-widthborder-styleborder-colorborder-colorsborder-<direction>-<param>
  • 圆角:border-radiusborder-top-left-radiusborder-top-right-radiusborder-bottom-left-radiusborder-bottom-right-radius
  • 框图:border-imageborder-image-sourceborder-image-sliceborder-image-widthborder-image-outsetborder-image-repeat
  • 大小:widthmin-widthmax-widthheightmin-heightmax-height

界面属性

  • 外观:appearance
  • 轮廓:outlineoutline-widthoutline-styleoutline-coloroutline-offsetoutline-radiusoutline-radius-<direction>
  • 背景:backgroundbackground-colorbackground-imagebackground-repeatbackground-repeat-xbackground-repeat-ybackground-positionbackground-position-xbackground-position-ybackground-sizebackground-originbackground-clipbackground-attachmentbakground-composite
  • 遮罩:maskmask-modemask-imagemask-repeatmask-repeat-xmask-repeat-ymask-positionmask-position-xmask-position-ymask-sizemask-originmask-clipmask-attachmentmask-compositemask-box-imagemask-box-image-sourcemask-box-image-widthmask-box-image-outsetmask-box-image-repeatmask-box-image-slice
  • 滤镜:box-shadowbox-reflectbackdrop-filtermix-blend-modefilteropacity
  • 裁剪:object-fitclipclip-path
  • 事件:resizezoomcursorpointer-eventstouch-calloutuser-modifyuser-focususer-inputuser-selectuser-drag

文字属性

  • 模式:line-heightline-clampvertical-aligndirectionunicode-bidiwriting-modeime-mode
  • 文本:text-overflowtext-decorationtext-decoration-linetext-decoration-styletext-decoration-colortext-decoration-skiptext-underline-positiontext-aligntext-align-lasttext-justifytext-indenttext-stroketext-stroke-widthtext-stroke-colortext-shadowtext-transformtext-size-adjust
  • 字体:srcfontfont-familyfont-stylefont-stretchfont-weightfont-variantfont-sizefont-size-adjustcolor
  • 内容:tab-sizeoverflow-wrapword-wrapword-breakword-spacingletter-spacingwhite-spacecaret-colorquotescontentcontent-visibilitycounter-resetcounter-incrementpagepage-break-beforepage-break-afterpage-break-inside

交互属性

  • 模式:will-changeperspectiveperspective-originbackface-visibility
  • 变换:transformtransform-origintransform-style
  • 过渡:transitiontransition-propertytransition-durationtransition-timing-functiontransition-delay
  • 动画:animationanimation-nameanimation-durationanimation-timing-functionanimation-delayanimation-iteration-countanimation-directionanimation-play-stateanimation-fill-mode

到此已整合了95%的属性,可满足很多属性排序的需求。其他未列入的属性,可根据自身使用习惯增加与调整。

配置

编码时纯粹靠脑海中根据预设规范排列属性肯定存在难度,也不方便频繁修改代码。记住这些属性排序估计很费脑力,这么多属性,肯定使用工具自动化处理啊!

为了提供通用性,我开源了一个集成StylelintEslintVSCode配置工具@yangzw/bruce-std,配合VSCode插件为用户提供前端文件的代码校验、代码修复和错误提示的功能。

打来CMD工具,执行以下命令安装bruce-std,具体如何使用请查看文档详情。好用的话给我一个Star作为鼓励哈!

npm i -g @yangzw/bruce-std

到了配置插件这一步,其实操作不复杂,直接把过程罗列出来,跟着我一步一步完成。

  • 打开VSCode
  • 选择左边工具栏插件,搜索并安装Stylelint,安装完毕重启VSCode
  • 选择文件 → 首选项 → 设置设置中可选用户工作区
    • 用户:配置生效后会作用于全局项目
    • 工作区:配置生效后只会作用于当前打开项目
  • 点击设置右上角中间图标打开设置(json),打开的对应文件是settings.json
  • 加入以下内容并重启VSCode:为了保障每次改动都能正常格式化代码,必须重启VSCode
{
	// 默认自定义配置
	"css.validate": false,
	"less.validate": false,
	"scss.validate": false,
	"editor.codeActionsOnSave": {
		"source.fixAll.stylelint": true
	},
	// 扩展自定义配置
	"stylelint.configBasedir": "path/@yangzw/bruce-std",
	"stylelint.configFile": "path/@yangzw/bruce-std/stylelint/stylelintrc.js",
	"stylelint.customSyntax": "postcss-scss", // 可变❗
	"stylelint.stylelintPath": "path/@yangzw/bruce-std/node_modules/stylelint",
	"stylelint.validate": ["html", "css", "scss", "less", "vue"]
}

上述配置的path@yangzw/bruce-std模块所在的Npm根目录,可执行npm config get prefix获取Npm根目录并替换path

  • 执行npm config get prefix获取Npm根目录,例如是E:/Node/prefix/node_modules
  • 将上述配置的path替换为E:/Node/prefix/node_modules

校验不同类型代码需实时修改stylelint.customSyntax的值。

  • CSS/SCSSpostcss-scss
  • CSS/LESSpostcss-less
  • HTML/VUEpostcss-html

上述步骤操作完毕就可愉快地敲代码了。每次保存文件可自动格式化代码,该功能不仅将代码根据规范整理排序,甚至尽可能根据修复方案格式化出正确的代码。

Stylelint

留言
Ctrl + Enter
全部评论(35)
独孤若辉的头像
删除
开发经理 @ 广州菲卡
为什么喜欢在一些重点提示语句加“啊”。个人感觉看起来不太舒服。
另外就是一些概念性的东西,感觉解释的太生硬了
点赞
回复
蛇无聊的头像
删除
大前端 @ 萤火
上面的bakground-composite少了c是background-composite
点赞
回复
wzw的头像
删除
前端工程师
浏览器的CSS解析器解析css文件时,对CSS规则是从右到左匹配查找,这段怎么理解呀
1
2
删除
可以想一下,大家族的族谱,想找出自己这一支的脉络,是从初代祖先往下找找遍每一个分支最后找到自己方便(从左往右匹配),还是从自己出发,一直找到自己初代祖先(从右往左匹配)方便呢?
点赞
回复
删除
回复
从自己出发
可以想一下,大家族的族谱,想找出自己这一支的脉络,是从初代祖先往下找找遍每一个分支最后找到自己方便(从左往右匹配),还是从自己出发,一直找到自己初代祖先(从右往左匹配)方便呢?
点赞
回复
陌上南尘的头像
删除
Web前端
太赞了
点赞
回复
hone的头像
删除
Mac 的小伙伴

当你执行 npm config get prefix 显示没有获取到路径的时候。 运行 npm root -g 就可以获取到完整的路径了, @yangzw 在 node_modules 文件夹里[调皮]

cmd + , 就可以打开设置,左上角这个标,点进去就是 settings.json
[呲牙]

感谢作者大大的答复!!![爱心][爱心][爱心]

支持正版从我做起[呲牙]
展开
点赞
回复
玛咔巴咔的头像
删除
前端开发工程师 @ 萌萌
目录点击白屏呦,太烦了哇
点赞
1
删除
(作者)
可能服务器挂了,我下班处理下
点赞
回复
蛇无聊的头像
删除
大前端 @ 萤火
bakground-composite 这个CSS样式存在?
点赞
回复
ldaga的头像
删除
全栈 @ 小项目
“何必不在编码时对CSS代码进行一次完美的编写呢?” —》 “何不在编码时…”
点赞
回复
Frank同志68687的头像
删除
全栈工程师vx:abb366388399
关于reflow和repaint的文章dev.to
点赞
回复
slv的头像
删除
1
动态改变类而不改变样式,这一条,如果切换的class还是改变了几何属性还是会引发回流的
点赞
1
删除
这里应该指集中改变某个元素样式(即写在类中),而非对一个个样式进行逐一修改,如此可以提高渲染效率
1
回复
麓一的头像
删除
前端小罐茶 @ xx
使用transform代替top,top 是不是指 使用了 position的绝对定位?
1
1
删除
(作者)
是的呀
点赞
回复
EEEEEEEEE的头像
删除
前端
配置这个 vscode 插件 会不会和 stylelint 冲突呢
点赞
回复
万物皆可new的头像
删除
web前端
有个问题哈,如果使用 visibility: hidden,来做显示隐藏的话,那留下来的占位怎么处理好勒。
点赞
2
删除
是不是还要看使用场景,随机应变勒
点赞
回复
删除
根据作者的思路 transform:scale(0) 即没有回流 也不占据空间
是不是还要看使用场景,随机应变勒
点赞
回复
浊清的头像
删除
插件安装了,配置也写了,重启后依旧不生效
点赞
1
删除
(作者)
快捷键配置了吗
点赞
回复
JAiKO的头像
删除
FE @ ctripcorp
这个插件format html里写的会显示有点怪异,不知道是不是我其他插件有什么和它冲突了
点赞
回复
赵京斌的头像
删除
web前端
Mac系统选择cmd+, → 用户 → 右上角第二个图标(打开设置),在json文件里插入以下配置。
这里看不懂是怎么操作的,我压根找不到什么用户什么的,我打开了全局的setting.json配置文件把代码加上去,但是颜色是灰色的,我不知道为什么。
点赞
1
删除
(作者)
插件没安装就是灰色的
点赞
回复
此时一位网友路过的头像
删除
摸鱼工程师
第一次见到以回流和重绘两个方面来解释css的性能问题,就凭这个这波买的也不亏!👏👍
5
回复
德育处主任的头像
删除
🛠️打杂
有理有据,令人信服
1
回复
Chiorio的头像
删除
写得太好了 干货满满!
2
回复
桐桐在掘金的头像
删除
受益匪浅
点赞
回复

查看全部 35 条回复