对于移动端的Web单页应用来讲,为了达到媲美原生应用的效果,页面过渡动画是必不可少的。经常使用的页面过渡动画包括:css
(注意:如下讨论和实验均在 Chrome 68 浏览器环境下进行)html
目前大多数设备的屏幕刷新率为60次/秒,算下来每一个帧的预算时间约为16.66毫秒(1/60秒)。考虑到浏览器还有其余工做要执行,实际上预算时间只有10毫秒。跟此预算时间的差值越大,用户就会以为动画过程越卡。那么,在这10毫秒内要完成什么事情呢?当使用JavaScript实现视觉交互效果时,通常要通过如下流程:web
值得注意的是,并不是每一帧都会通过上述每个步骤的处理。若是元素的几何属性(尺寸、位置)没有变化,就不须要进行布局;若是连元素的外观都没有改变,就不须要绘制。因此,实现流畅动画的关键就在于如何减小布局和绘制。浏览器
对于位移动画来讲,最直接的实现方式,就是把元素设成绝对定位,而后去改变它的left样式值。例如:性能优化
<!DOCTYPE html> <html> <head> <style> .page { position: absolute; left: 0; top: 0; width: 100%; min-height: 100%; background: #ddd; transition-duration: 2s; transition-property: left; } .leave { left: -100%; } </style> </head> <body> <div id="page" class="page"></div> <script> var page = document.getElementById('page'); setTimeout(function() { page.classList.add('leave'); }, 2000); </script> </body> </html>
使用Chrome开发者工具中的Performance面板录制动画过程的性能日志,以下图所示:app
可见,元素在移动的过程当中不断触发了布局和绘制。因此,这种实现方式的性能是极低的。网上诸多文献会推荐以transform的变化代替left的变化,而实际状况又是怎么样呢?把样式代码稍做修改:框架
.page { position: absolute; left: 0; top: 0; width: 100%; min-height: 100%; background: #ddd; transition-duration: 2s; transition-property: transform; } .leave { transform: translateX(-100%); }
录制性能日志以下图所示:工具
可见,仅仅是在动画开始和结束两个时间点触发了绘制,而布局则彻底没有触发。这样一来,性能就有了很大的提高。可是,这里还有两个疑问:布局
要回答这两个问题,就得了解合成层。性能
当知足某些条件的时候,元素在渲染时会被分配到一个独立的层中进行渲染,只要该层的内容不发生改变,就不会触发绘制,浏览器会直接经过合成造成一个新的帧。常见的提高为合成层的条件包括:
很明显,上一节的transform位移动画知足了第一个条件。因此整个动画的渲染过程是这样的:
若是让div.page一直在独立的合成层中渲染,则能够省掉上述过程当中绘制的环节。在样式代码添加「will-change: transform」:
.page { position: absolute; left: 0; top: 0; width: 100%; min-height: 100%; background: #ddd; transition-duration: 2s; transition-property: transform; will-change: transform; }
录制性能日志以下:
可见,已经不存在绘制的步骤了。
顺带一提,Chrome开发者工具中有一个Layers面板,能够方便地查看页面上合成层以及成为合成层的缘由。
(注意:因为低版本浏览器不支持will-change,因此实际应用中,若是想把元素提高到独立的合成层中渲染,能够用「transform: translateZ(0)」)
众所周知,不透明度就是经过opacity样式来控制的。那么opacity的变化是否会触发布局和绘制呢?把样式代码修改以下:
.page { position: absolute; left: 0; top: 0; width: 100%; min-height: 100%; background: #ddd; transition-duration: 2s; transition-property: opacity; } .leave { opacity: 0; }
录制性能日志以下图所示:
在常规认知中,opacity的变化并不会致使元素位置和尺寸的变化,理应不会触发布局。但上述过程当中确实触发了一次布局,表现较为诡异。接下来给div.page添加「will-change: opacity」使其一直在独立的合成层中渲染。录制性能日志以下:
可见,仍是会触发一次绘制。而针对这「一次的布局」和「一次的绘制」,我进行了进一步的实验,得出的结论是:opacity从1(包括未设置的状况,下同)变动到小于1,以及从小于1变动到1,都会触发布局和绘制;即便在独立的合成层中渲染,也只能省掉布局,没法省掉绘制。
因为在opacity动画过程当中从1到小于1的变动只会有一次,因此上述的布局和绘制都只触发一次。
同时使用两种动画,修改样式代码以下:
.page { position: absolute; left: 0; top: 0; width: 100%; min-height: 100%; background: #ddd; transition-duration: 2s; transition-property: transform, opacity; } .leave { transform: translateX(-100%); opacity: 0; }
按照前文的描述,动画过程会触发:
假若加上「will-change: transform, opacity」,使div.page一直在独立的合成层中渲染,则只触发一次绘制,由opacity引发。
然而,建立一个新的合成层并非免费的,它会致使额外的内存开销。在单页应用中,应用页面过渡动画的元素是页面的最外层容器,包含了该页面全部内容结构。若是让其长期在独立的合成层中渲染,那内存的消耗是很是大的。
因此,能够仅在动画过程当中让其在独立的合成层中渲染,而在其余状况下则维持常规状态。
若是用transform实现页面过渡动画,想必你们都遇到过一个问题:页面上固定定位的元素,其位置变得不太正常了。
下面经过一段代码模拟页面进入的过程,来演示这个问题:
<!DOCTYPE html> <html> <head> <style> .page { position: absolute; left: 0; top: 0; width: 100%; height: 150%; background: #ddd; transition-duration: 3s; transition-timing-function: cubic-bezier(.55, 0, .1, 1); transition-property: transform, opacity; } .before-enter { transform: translateX(100%); opacity: 0; } .fixed { position: fixed; right: 0; bottom: 0; width: 100%; height: 160px; background: #ffc100; } </style> </head> <body> <div id="page" class="page before-enter"> <div class="fixed"></div> </div> <script> var page = document.getElementById('page'); setTimeout(() => { page.classList.remove('before-enter'); }, 2000); </script> </body> </html>
运行效果以下:
能够看到,固定定位的黄色元素是在动画结束后才忽然出现的。那在这以前它跑到哪去了呢?
若是给一个固定定位元素的任意一个祖先元素设置样式「transform」或者「will-change: transform」,那么该元素就会相对于最近的设置了上述样式的祖先元素定位。
由于div.page的高度设成了150%,因此,在动画过程当中,黄色元素其实是跑到了页面的最底下(超出了浏览器可视范围)去了。而在某些比较旧(如 iOS 9 的Safari)的移动端浏览器中,问题更为严重,固定定位的元素可能会消失掉不再出现。
网上能查到的解决方案有两种:
因此,这里介绍第三种方案——在页面过渡动画结束以后(此时transform样式已被移除,再也不影响fixed),再让固定定位的元素插入到页面容器。而且,为了让它的出现显得不那么忽然,增长缓动动画。代码主要修改点以下:
@keyframes kf-move-in { 0% { transform: translateY(100%); } 100% { transform: translateY(0); } } .move-in { animation-name: kf-move-in; animation-duration: 0.45s; }
<div id="page" class="page before-enter"></div> <script> var page = document.getElementById('page'); setTimeout(function() { // 监听过渡结束 page.addEventListener('transitionend', function() { // 建立、插入固定定位元素 var div = document.createElement('div'); div.className = 'fixed move-in'; page.appendChild(div); }); page.classList.remove('before-enter'); }, 2000); </script>
运行效果以下:
这样一来,整个交互就较为友好了。这同时也说明:技术上的问题,不必定只能经过技术去解决,也能够从交互上去寻求解决方案。
本文同时发布于做者我的博客: https://mrluo.life/article/de...