性能优化之Recalculate Style耗时过长

最近公司项目longtask卡顿治理中,大部分卡点都在Recalculate Style耗时过长,经常各种500ms以上的longtask,甚至各种1s以上的longtask。虽然连猜都蒙的最后都治理了,但是理解的还是不够深入,还需要再进一步通过阅读chrome源码blink渲染引擎render部分,来找到最终的解释。

先介绍一下公司的项目特点以及卡顿时候的performance表现。

项目特点:

1.dom节点数比较多,差不多大几千甚至上万。

2.style标签多,差不多400~600之间。主要原因还是,内部各种业务npm包或者模块联邦,然后呢UI库的共享做的不太好,导致各种style标签。

3.style标签多了,css规则就多,差不多3w~5w之间。

卡顿时候的表现:Recalculate Style 耗时特别长,Elements affected 数目特别多。

卡顿时候的几个操作场景
1.rnd的拖拽组件,在拖动开始时跟结束时。拖动开始时候在body元素上添加了dragging的class

2.Spin组件,加载中跟加载完成时。加载时候给Spin对应的dom加了一个.semi-spin-hidden的class

3.浮层类组件popver,在body元素中append一个portial浮层div。

简单分析下:可以看到这几个卡顿的场景,要么是改了class,要么是改了dom,导致需要重新计算样式的节点数特别多,或者是说需要Recalculate Style的Elements affected特别多。而我们项目的css-rules数又是几万,这么多的节点需要重新跟css-rule逐个重新匹配下,那么耗时过长也是难免的了。而我这次要研究的就是Elements affected是怎么算出来的,为何这么多呢?我在一个div上新增或者删除了一个class,为什么会回流这么多节点呢?

chrome是通过失效集来标识需要回流哪些节点的,可以看一下chrome的源码中关于失效集的介绍 :

下面举几个简单的例子

示例1:

.a span{ color: red}

对应的失效集

.a { span}

解释:假如.a元素删除了class a的话,那么这么元素下的所有后代span元素都需要回流。

示例2:

.a .b span{color: red}

对应的失效集

.a { span}

.b { span}

解释:
.a元素删除了class a的话,那么这么元素下的所有后代span元素都需要回流。

  .b元素删除了class a的话,那么.b元素下的所有后代span元素也都需要回流。

注意.a删除class后,需要回流的元素并不是 .a 后代然后.b后代的span元素,而是跟示例1一样.a的所有后代span元素。

可以看出,选择器的最右侧的选择器span 是失效集中要失效的元素,选择器前面的路径中的.a .b 作为失效集中的匹配部分。 

示例3:

.a ~ .b {}

对应的失效集合

.a { *}

PS:如果出现相邻选择器,则直接将 wholeSubtreeInvalid 置为 true,即所有后代元素需要重新计算样式。不过是当前元素父元素的所有后代,或者说是当前元素的所有兄弟元素

示例4:

.a :not(.b) { }

对应的失效集合

.a  { * }

PS::not选择器目前在失效集构建算法中被跳过。直接所有。

实验代码:上面几个小例子我用下面一份简单的小代码试验验证过了。大家可以自己改一下验证一些自己的想法。

<!DOCTYPE html>
<html>
	<head></head>
	<style>
		div {
			width: 100px;
			height: 200px;
			border: 1px solid green;
		}
		.a ~ .b{
			width: 100px;
			height: 200px;
			border: 2px solid black;
		}
	</style>
	<body>
		<div>
			<div id="test" class="a">
				<div class="b"></div>
			</div>
			<div class="b"></div>
			<div class="b"></div>
			<div class="b"></div>
			<div class="b"></div>
		</div>
		<script>
			
			function test() {
				longtask();
				document.getElementById('test').classList.remove('a');
			}
			
			setTimeout(() => {
				test();
			}, 500)
			
			function longtask(){
				const tmp = [];
				for (let i = 0; i < 10000 * 1000; i++) {
					tmp.push(i)
				}
			}
		</script>
	</body>
</html>

回头再来看,我们这一个卡顿的场景:rnd拖拽组件卡顿

rnd在拖拽时,会给body添加一个react-draggable-transparent-selection,而结束时候则去掉了这个class。此class是为了控制body所有子元素在拖动过程中不能进行文字拖选的。

此段css规则为

.react-draggable-transparent-selection *::selection {all: inherbet}

根据前面说的失效集规则为

.react-draggable-transparent-selection { * }

所以在拖动开始跟结束时候,body下面的所有子元素都需要Recalculate Style。所以需要计算的元素特别多,而我们css规则也一样多的夸张,一段比较长的longtask就不可避免了。

总结:

1.我们的项目在回流时候,并不像各种八股文里提到的layout耗时长,而是Recalculate Style耗时长,感觉主要原因还是css规则太多了,几万条css规则,一旦需要回流的元素比较多,那么重新匹配就是M*N的关系,回流元素数*css规则数,所以还是要尽量减少css规则数,尤其这种微应用+各种内部npm包+模块联邦的项目。

2.css规则尽量要简单,匹配路径要尽量短,尽量减少使用兄弟选择器、not()、nth-child()这类的复杂选择器,这种选择如果blink引擎在构建失效集时做的优化并不彻底或者未做优化,会导致大量节点的回流。

项目中有这样一段css规则,有了这段css规则后,在某些场景下,任何浮层类操作(向body元素append一个div),都会导致回流很多节点。到现在也没搞明白为何会这样,最终只是去掉这段css规则,改用简单的class选择器来解决的。

div:nth-child(2)>div {  margin: 2px 0;  color: var(--color-text-0)}

3.chrome源码中的很多readme文件,比市面上的各种八股文要有深度的多,虽然源码读起来比较费劲,但是readme文件读一读是可以的。

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MYILWdJK' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片