预防“布局抖动”
布局抖动是因 JavaScript 的 DOM 元素被多被次暴力写,然后读,导致文档重排而出现的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
// 读 var h1 = element1.clientHeight; // 写(无效布局) element1.style.height = (h1 * 2) + 'px'; // 读(触发布局) var h2 = element2.clientHeight; // 写(无效布局) element2.style.height = (h2 * 2) + 'px'; // 读(触发布局) var h3 = element3.clientHeight; // 写(无效布局) element3.style.height = (h3 * 2) + 'px'; |
当DOM元素被写入值,布局就“无效”,而多次这样就会导致文档重排。浏览器很懒,它总想等到当前操作(或帧)的最后一步才重排。
然而,如果在当前操作(帧)完成前,从DOM元素中获取值,这会迫使浏览器提早执行布局操作,这称为“强制同步布局”,这可是性能杀手!
布局抖动的副作用在现代桌面浏览器上并不明显;但对于低配置的移动设备来说,其后果就不堪设想了。
能快速修复?
在理想情况下,我们可能通过简单地重复执行,以至于将DOM元素的读写操作放在一起执行。这意味着文档只需重排一次即可。
1 2 3 4 5 6 7 8 9 10 11 |
// 读 var h1 = element1.clientHeight; var h2 = element2.clientHeight; var h3 = element3.clientHeight; // 写(无效布局) element1.style.height = (h1 * 2) + 'px'; element2.style.height = (h2 * 2) + 'px'; element3.style.height = (h3 * 2) + 'px'; // 文档在最后一帧将进行重排 |
现实情况会怎么样?
现实情况并非如此简单。大型应用程序的代码会分散到各个地方,因此这些地方都有危险的DOM操作。所以不能简单地(绝对不应该)聚集它们,而需要解耦代码,只是需要控制好执行顺序。那如何让读写操作捆绑在一起,从而获得最佳性能呢?
进入requestAnimationFrame
window.requestAnimationFrame
是一个将操作安排在下一帧一起执行的函数,类似于setTimeout(fn, 0)
。这是非常有用的,因为能使用它来安排所有DOM的写操作在下一帧一起执行,保留所有DOM的读操作在当前同步状态。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// 读 var h1 = element1.clientHeight; // 写 requestAnimationFrame(function() { element1.style.height = (h1 * 2) + 'px'; }); // 读 var h2 = element2.clientHeight; // 写 requestAnimationFrame(function() { element2.style.height = (h2 * 2) + 'px'; }); |
这意味着我们能很好地封装代码了。经过小小调整后的代码,就将高耗能的DOM操作捆绑在一起!实在太棒了!
工作实例
我创建了一个工作案例来证明这个观点。从第一个截屏的chrome时间轴可看出,有多个布局抖动穿插其中。
Before | After |
---|---|
![]() |
![]() |
在改用requestAnimationFrame
后,仅仅只触发一次布局事件,其结果是操作快了约96%。
它具有伸缩性吗?
在一个简单案例里,使用requestAnimationFrame
来延迟DOM写操作,从而大大提高性能,但这项技术没有伸缩性可言。
在我们的应用中,可能需要在DOM元素上执行先写后读操作,然后再次掉入布局抖动的坑,只是在不同帧。
1 2 3 4 5 6 7 8 9 10 |
// 读 var h1 = element1.clientHeight; // 写 requestAnimationFrame(function() { element1.style.height = (h1 * 2) + 'px'; // 我们可能想在设置高度后再读取新高度值。 var height = element1.clientHeight; }); |
我们可以将读操作放到另外一个requestAnimationFrame
,但我们不能保证应用程序的另一部分,没有把写操作放在同一帧上。
介绍 ‘FastDom’
FastDom是一个轻量的库,它提供一个公共接口,能让DOM的读/写操作捆绑在一起。其实,它就是利用上述同样的 requestAnimationFrame
技术来大大提高DOM操作速度。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
fastdom.read(function() { var h1 = element1.clientHeight; fastdom.write(function() { element1.style.height = (h1 * 2) + 'px'; }); }); fastdom.read(function() { var h2 = element2.clientHeight; fastdom.write(function() { element2.style.height = (h1 * 2) + 'px'; }); }); |
FastDom通过接收读写操作,并在下一帧捆绑它们(先读后写),从而消除DOM的相互影响。这意味着我们能独立编写应用程序组件,而不用担心它们在应用程序中互相影响。
使用FastDom的启示
通过使用FastDom,会让所有DOM任务变成异步,这意味着你不能总是假设DOM将会以什么状态进行操作。操作从之前的同步,变成现在的异步方式。因此,可能没执行完异步处理函数就会执行下一步操作了。
要解决这一点,我打算用事件系统来明确操作何时完成,和明确依赖于完成后所做出的响应操作。
虽然所做工作是一样的,但能通过增加代码量来显著提高性能。我个人认为这个代价小。
FastDom案例
完善FastDom
web应用缺少一个明确的方式,来解决布局抖动问题。正如一个应用程序很难协调所有不同的部分,来确保产品最终是高效的。如果FastDom能为开发者们提供一个简单接口来解决这个问题,那只能意味着它是个好东西。
瞧一瞧 FastDom 项目,欢迎随时通过 pull requests 或 filing issues 来完善它。