其实今天的主题应该算是昨天 High Performance CSS 的延伸。
大家应该都有遇过网页的动画有点不顺畅甚至卡顿的经验吧?是不是觉得很烦人呢?就算没有马上跳出网页应该也暗自在心里把这个网站画上了几个叉叉。
今天我们就专注在「如何让网站达到更顺畅的动画体验与效能
」这个主题上。
这点在昨天还有 Day08 时都有提过,因为 transform 不会触发 reflow 与 repaint,所以在效能上会比改变元素的 width, height, position 的 left 或 top 还要好。
阿怎麽同样的东西要讲三次啊!
:因为真的很重要(误)。
别急,当然不会这样骗读者的点阅还有一天的篇幅,今天要介绍的是另一个概念 - Hardware Acceleration 硬体加速。
硬体加速简单来说就是浏览器会把一些比较复杂的页面渲染相关任务交给 GPU 处理,而不是全部都靠 CPU 来完成。而一些页面的 animation 就可以透过 GPU 加速使页面渲染速度更快且更加流畅。
GPU 图形处理器,全名为 Graphics Processing Unit
,是一种专门用在执行绘图运算工作的微处理器。
我们都知道 CPU 在电脑的主机板上,可以说是电脑的「大脑」,而 GPU 则位於电脑的显示卡上.主要用途为图形的渲染。而 GPU 是专门为了执行图形渲染所需要的复杂数学与几何计算而生的,所以把一些复杂的操作交给 GPU 处理,除了性能上的显着提升外,也减轻了 CPU 的工作压力,这效果在 mobile 的设备上尤其明显。
还记得在 Day08 介绍浏览器架构与渲染流程时有提过 Layer 分层吗?其实 GPU 硬体加入的原理就跟分层有关。当页面上的 element 执行了某些操作,例如说 3D 的 transform,该元素会被提升到一个单独的图层,独立於页面上的其他部分,并在後期做之前讲过的栅格化与合成并绘制到萤幕上。
把元素提升到自己的图层的好处是当页面上只有这个元素要做 transform 时,其他的元素不需要重新渲染,这使得效能带来极大的优化。
此外还记得 Day08 时提过的现代浏览器架构吗?
GPU 会运行在独立的 Process 里,不会 block 住 Render Engine 的其他工作,现在是不是更了解浏览器性能提升所带来的影响了啊?
就算 CSS 写得再好,如果浏览器没有开启硬体加速,也是没办法享受它的强大的,这边以常用的 Chrome 为例子:
Chrome 的预设选项是会开启硬体加速的,不过人生总是有些意外,建议还是去看看有没有不小心被关掉了喔!
再来可以在网址列输入 chrome://gpu 看看目前你的 Chrome 浏览器中有哪些功能是开启 GPU 加速的
有一些 CSS 属性被归类於 GPU accelerated properties,也就是被浏览器认定为「有机会」被 GPU 加速的属性:
transform
filter
opacity
所以说只要使用这些属性并且浏览器有启用硬体加速就会自动使用 GPU 加速了吗?
答案是否定的。
使用这些属性并不保证会被 GPU 加速,一般来说这些工作还是由浏览器的渲染引擎来执行的,不过有几个例外,例如如果是 2D 的 transform 就不会开启硬体加速,而 3D 的 transform 如果浏览器有启用的话则会强制使用硬体加速操作。
3D transform ? 原来三维空间的操作可以透过硬体加速呀!可是...我好像没有在写什麽 3D 的动画,都是二维的简单动画,看来...我跟硬体加速还是无缘吗...?
那可不一定,虽然说 3D transform 才能透过硬体加速,但我们可以走一点偷吃步的方式,让二维的变化假扮成是三维的操作,这种偷吃步的方法又被称作 transform hack,而通常会使用的属性有 transformZ 或与 translate3D。
例如你原本预期的动画是这样:
transform: translate(0, 20px);
让元素向下移动 20px,你可以使用以下的方法来开启硬体加速
transform: translate3D(0, 20px, 0);
使用这样的方式会创建一个合成层,这个合成层会被送到 GPU 并由 GPU 来合成。
看到这里我知道你在偷笑了。
那我以後都用 transform hack 的方式来开启 GPU 加速就好啦!效能提升好棒棒!
怎麽到今天你还没有学乖咧,看起来美好的事物通常都是一把双面刃的!
建立一个独立的图层确实可以提高渲染效能,但是它带来的缺点就是会让 memory 的使用量急遽提升,这种状况在 mobile device 上尤其可怕,一不注意可能就会导致手机的浏览器直接 crash。所以在使用这种方式时要格外小心,必须在非常确定这麽做可以提升页面性能时再使用,不然真的会得不偿失,反而让效能出现瓶颈!
後来出现了一个新的 CSS 属性 - will-change,让我们可以不必再依赖 transform hack 的方式来开启硬体加速。
will-change 属性比较像是一个 hint,让你提前告诉浏览器你可能会在未来对元素做什麽类型的操作,让浏览器可以提前做准备,使渲染流程更快速与顺畅。
例如说,我们提过 CSS transform 有机会把元素提升到一个新的图层,进行合成後再渲染到萤幕上,不过提升一个图层其实是耗性能的操作,有可能让 transform 的动画延迟开始,造成萤幕的闪烁。
要解决这个问题的其中一个方法就是提早通知浏览器让它做好准备,等到真的需要时就可以迅速执行。will-change 就可以做到这件事:
will-change: transform;
/* 也可以一次给多个 */
will-change: transform, opacity;
关於 will-change 有哪些属性可以带,可以参考这里。
上面的例子是告诉浏览器这个元素未来会做 transform,让浏览器可以先行准备并「让浏览器选择
」最好的方式来处理这个变动。相较於 hack transform 强制创建可能对效能没有帮助的图层并开启硬体加速,will-change 无疑是更好的选择。
例如
* {
will-change: transform;
}
这样的写法告诉浏览器什麽? 「所有页面上的属性都有可能发生变化,有机会的话帮我优化喔!」这样其实有讲就等於没讲一样,因为完全看不出任何的优先层级,而且这样浏览器还需要花计算资源处理 will-change,过度使用反而有机会让页面效能直接炸裂的!(稍後会有炸裂的 dmeo,慎入,真的是直接炸裂XDD)
浏览器对 will-change 提示进行的优化通常有很高的成本,会占用大量的资源,如果是一个不会一直触发 transform 的元素,在变更生效後建议先透过 JavaScript 动态移除 will-change 属性以释放资源。
例如,监听元素的 animationEnd 事件在动画结束後移除 will-change 的属性。
let el = document.getElementById('puddydat');
// Set will-change when the user hovers over the element
// then remove the hint once the animation has ended
el.addEventListener('mouseenter', addHint);
el.addEventListener('animationEnd', removeHint);
// Function to set the hint
function addHint() {
this.style.willChange = 'transform';
}
// Function to remove the hint
function removeHint() {
this.style.willChange = 'auto';
}
如果元素需要不停的触发动画,例如这个例子 中的红色移动球体,就很适合把 will-change 留在上面。想到的另一个例子是跟随使用者滑鼠鼠标移动的动画,因为可以预期元素会有规律且频繁的变化,所以保持优化状态应该是合理的。
前面讲了那麽多理论,但我知道人总是比较容易相信亲眼见过的事实,那就来个 demo 吧。
我在这个 demo 中渲染出了非常大量的表格元素,并随机给予区分大小写的英文字母,上面会有一个按钮,点击後会把元素作颠倒的排序(reverse),在重新排列时会显示一个渐变的动画。
我分成两种做法,第一种是比较糟糕的做法,排列时直接修改元素的 style.top
document.querySelector('#reorderBtn').addEventListener('click', function () {
elementRows.reverse();
elementRows.forEach(function (eRow, rowIndex) {
eRow.style.top = rowIndex * ROW_HEIGHT + 'px';
});
});
跑起来的结果是这样
第二种方式则是用比较推荐的 transform: translate,并且加上了刚刚提的 will-change
.row {
height: 25px;
box-sizing: border-box;
position: absolute;
width: 100%;
will-change: transofrm;
transition: transform 0.5s;
}
document.querySelector('#reorderBtn').addEventListener('click', function () {
elementRows.reverse();
elementRows.forEach(function (elementRow, rowIndex) {
elementRow.style.transform =
'translate(0, ' + rowIndex * ROW_HEIGHT + 'px)';
});
});
跑起来的结果则是这样
你可能觉得看起来没有什麽巨大差别,的确,做成 gif 图档後看起来没有差很多,所以我决定把 demo 做个简单部署,让各位亲自体会一下两种动画实作方式的效能差异。(强烈建议读者点开来感受一下差异,因为只是简单部署到 AWS S3,就没有另外去设置 https 了,还请各位见谅。)
自己觉得差异非常非常的显着,不过为了方便 demo,我是把 will-change 属性一直留在元素上的,这个 case 应该可以在 reorder 後移除 will-change 才对,不过看起来成效还是不错的,动画相较於原本写法更为顺畅了。你也跟我ㄧ样被吓到了吗?
Demo Source Code: https://github.com/kylemocode/it-ironman-2021/tree/master/css-transform-demo
话说,记得前面提过不要滥用 will-change 吗?给各位看看滥用的下场是什麽。我仅仅是在 CSS 加入一行:
* {
will-change: transform;
}
我们来看看页面变成什麽样子(非常可怕,慎入!)
http://very-bad-animation-demo.s3-website-us-east-1.amazonaws.com/
看起来甚至比第一种方式还更糟了,光初频的渲染就要花上一段时间了呢!
恭喜你们学会用一行 CSS 就能摧毁一个网站的技能(误?)
我自己蛮喜欢今天的主题的,动画在现在的网页几乎是不可或缺的 feature,如何让动画保持顺畅让使用者有良好的体验是前端开发者不能忽视的难题。有趣的是动画的卡顿未必会反应在 Lighthouse 等检测工具的分数上,这就是我在 Day03 说过的「不是只有会反应在分数上的才是做效能优化要关注的点。」
自己也觉得最後三种版本的 demo 还蛮有趣的,仅仅改了一两行 code 居然会让动画效能有那麽大的差异,希望经过昨天与今天的内容,能够让你对於写出效能更好的 CSS 更有自信。
Delivery Optimizations & Render Process Optimizations 这个篇章也告一段落罗,明天将进入 Build Optimizations 篇章,不知道各位还在吗?? 请撑住!
我们明天见!
https://www.sitepoint.com/introduction-to-hardware-acceleration-css-animations/
https://developers.google.com/web/updates/2019/08/get-started-with-gpu-compute-on-the-web
https://www.maxlaumeister.com/articles/css-will-change-property-a-performance-case-study/
https://www.quackit.com/css/css3/properties/css_will-change.cfm
<<: [Day - 13] - Spring 依赖性注入元件管理运作与方法
何谓Git? *说明 : 一套分散式的版本控制系统。 *作用 : 版本控制能够记录档案的内容变化,并...
嗨各位好久不见, 今天要来分享上次的续集 第 2 part , 上篇讲到建构器 今天要来分享关於继...
Hi~今天要介绍什麽是区块链!会分成五个部分,分别是定义、起源、特性、优点、缺点! 定义 当你听到...
-零信任网路安全范式 EAP-TLS、EAP-TTLS 和 PEAP 是 WPA2 中使用的合法身...
为什麽网站地图对於SEO这麽重要呢? 什麽是网站地图XML呢? (英语:Sitemap)描述了一个网...