getComputedStyle 方法与性能问题
前言
最近老是在项目中发现一些由getComputedStyle
方法引起的性能问题,无非就是一不小心引起了大量的样式计算(Recalculate Style
)和重排(Layout
),从而引起了线程阻塞,造成页面卡顿;
getComputedStyle 方法的原理
getComputedStyle
方法(以下简称为gCS
)的语法就不多说了,可以移步:Window.getComputedStyle() - Web API 接口参考 | MDN;
其实疑惑最大的地方就在于为何gCS
方法获取的计算样式一定会触发新的样式计算甚至是布局计算呢?熟悉浏览器渲染管线的人应该都清楚,样式计算环节处于构建DOM tree
和构建layout tree
之间,按理说绘制当前帧时每个DOM
结点的计算样式(Computed Style
)应该是已知的,为何不能从已经计算好的计算样式里面取值呢?
以上疑惑的答案就在规范里:
Return a live CSS declaration block with the following properties[1]
关键就在于这个live
,即实时;由于其设计目的就是要返回一个实时的CSS
声明块(实际上是一个只读的CSSStyleDeclaration
对象),而执行gCS
的当前并不能保证之前的计算样式一定和当前最新的计算样式是一致的,所以在获取相应属性的最新计算值时不得不重新计算样式(Recalculate Style
),而某些CSS
属性受到布局的影响也就不得不重新计算布局;
Recalculate Style 的细节
-
单纯地调用
gCS
方法并不会引起任何的Recalculate Style
和重排;1
2
3
4
5
6
7<template id="test" rule="v-">
<div id="app">
<ul v-for="ul in list">
<li v-for="item in list">{{ item }}</li>
</ul>
</div>
</template>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16const num = 100
const list = new Array(num).fill(0).map((val, idx) => idx + 1)
const res = test.render({
list
})
document.body.appendChild(res.content) // 渲染100个相对复杂的DOM
const app = document.getElementById('app')
const doms = app.querySelectorAll('ul')
function relayoutTest (el) {
const style = getComputedStyle(el) // 单纯地触发gCS
}
console.time('getComputedStyle')
for(let i = 0; i < num; i++) {
relayoutTest(doms[i])
}
console.timeEnd('getComputedStyle') // 计算同步执行时间通过上述测试可以看到,单纯地调用
gCS
方法并没有产生样式计算和重排,仅仅是一个普通的函数调用开销; -
只有通过
gCS
方法返回的CSSStyleDeclaration
对象访问某个具体的属性值时才会同步地引起进一步Recalculate Style
或重排;1
2
3
4function relayoutTest (el) {
const style = getComputedStyle(el)
return style.height // 仅仅是访问height属性
}可以看到,同样的测试条件,仅仅是进一步访问了
CSSStyleDeclaration
对象的height
属性,就会立即触发重排并引发了一定时间的线程阻塞; -
至于说访问属性会引起重排还是普通的重新计算样式,这里推荐一篇博文的总结[2]:
一个案例
最近发现element-plus
里面的dialog
组件在某些场景下(子结点数量多时)存在着严重的卡顿,仔细一看源码才发现是一处很典型的引发重排和样式计算的使用:
好家伙,一行代码出现了两个问题点,gCS
方法的问题就不用多说了,然而访问这个offsetParent
属性也会引起重排;要命的是,这行代码对于dialog
组件内的几乎所有子结点会调用该方法,所以子结点一多就不可避免地卡顿,然而源码上也注释了这行代码存在潜在的风险,需要一些优化建议;
所以这就引申出一个问题:对于gCS
方法有没有更高效的使用和优化方法?
优化思考
计算样式DOM化
所谓地计算样式DOM
化,就是将每次DOM
结点的计算样式结果存放到DOM
接口中,而不再需要通过类似gCS
方法来强制计算或重排,相当于是将最新的计算样式缓存到DOM
上,只有真正的重排、重绘或组合才会引起计算样式地改变;
可以思考一下我们使用gCS
方法的一些场景,很多时候我们之所以用gCS
方法来获取样式属性值,是因为DOM
的style
属性只会返回当前节点显式声明的样式属性(即通过样式文件,样式声明或内联样式所匹配到的样式),所以不得不用gCS
方法来获取那些默认样式的值;很明显,这种情况下并不符合规范中所预想的gCS
方法使用背景(即由于某些操作导致样式发生变化,且想要立即同步地获取到最新的计算样式),我们明明只是想要获取一些压根就没有变化的计算样式,为何还要强制地去重排或样式计算;
目前还不清楚各大浏览器引擎内部有没有缓存最新的计算样式,不过确实没有看到W3C
有相关的规范从已有的计算样式中获取值的方法;考虑到缓存所有DOM
的计算样式确实需要一定的内存开销,毕竟现在计算样式的属性数量已经高达300+
了,所以不知道这种内存开销是否在可接受范围内;
fastdom
这是一个专门用来批量读/写DOM
属性的库,用于解决读/写DOM
属性所带来布局抖动或其它类似的开销;
其原理很简单,代码也很少,推荐直接阅读源码;fastdom
通过将读和写任务分别存到不同的任务队列中,并通过rAF
(requestAnimationFrame
)函数在下一帧执行相应的任务,且先批量执行读任务再批量执行写任务(这个顺序很重要);
很明显,这种操作会把DOM
读写操作改成异步任务(其实也就是下一帧),而不会阻塞当前帧的操作;不过如果批量的读写次数过多时,在下一帧执行时仍然会造成线程阻塞……
减少同时操作的 DOM 数量
在目前的条件下,如果同时使用gCS
方法影响DOM
的数量过多(100+
,其实还跟DOM
所影响的范围有关),会不可避免地阻塞到线程;哪怕是通过fastdom
那种通过将操作异步化,其阻塞无非只是被延后,在这种情况下解决不了本质的问题;
结合 style 和默认属性值
上面也提到过:
很多时候我们之所以用
gCS
方法来获取样式属性值,是因为DOM
的style
属性只会返回当前节点显式声明的样式属性(即通过样式文件,样式声明或内联样式所匹配到的样式),所以不得不用gCS
方法来获取那些默认样式的值;
这不就是提供了一种思路吗?既然那些默认样式style
属性上并不会有,但是默认样式的属性值规范中是有规定的(即已知的),不过考虑到由于某些样式可以继承,所以对于那些不可继承的CSS
属性,如果style
属性得到的相应属性值为空,那不就是代表其计算属性就是默认值!
以上面的element-plus
提到的问题为例,其通过gCS
方法来访问position
属性,由规范[3]可知,这个属性正好是不可继承的属性!
所以之前的代码完全可以优化为:
1 | export const isVisible = (element: HTMLElement) => { |
可以通过测试对比一下两种方式获取position
属性值的时间消耗:
-
gCS
:1
2
3
4function relayoutTest (el) {
const style = getComputedStyle(el)
return style.position
} -
style
+ 默认值:1
2
3function relayoutTest (el) {
return el.style.position || 'static'
}
显然,如果遇到其他的不可继承属性,也可以尝试用这种方式来优化性能;
相关文档
- 为什么getComputedStyle挂在了window上而不是Element.prototype? - 贺师俊的回答 - 知乎
- 【CSS进阶】原生JS getComputedStyle等方法解析 - 程序猿的生活的文章 - 知乎
- What forces layout/reflow. The comprehensive list.:引起重排的行为,包括了getComputedStyle方法引起重排的几种情况。
- Window.getComputedStyle() - Web API 接口参考 | MDN
- https://blog.lisong.hn.cn/前端优化/2018/06/05/前端性能优化之-force-reflow/:里面有关于getComputedStyle耗时的测试
- 获取元素CSS值之getComputedStyle方法熟悉 « 张鑫旭-鑫空间-鑫生活
- Avoid Large, Complex Layouts and Layout Thrashing | Web Fundamentals:一篇关于布局抖动优化的文章
- 使用 fastdom 解决布局抖动 | Nx-website-component