[贝聊科技]小动画大学问

对于移动端的Web单页应用来说,为了达到媲美原生应用的效果,页面过渡动画是必不可少的。常用的页面过渡动画包括:

  1. 位移——当前页向左侧或右侧水平移出可视区,下一页由反方向移入可视区。
  2. 不透明度变化——当前页淡出,下一页淡入。
  3. 1和2同时进行。

(注意:以下讨论和实验均在 Chrome 68 浏览器环境下进行)

目前大多数设备的屏幕刷新率为60次/秒,算下来每个帧的预算时间约为16.66毫秒(1/60秒)。考虑到浏览器还有其他工作要执行,实际上预算时间只有10毫秒。跟此预算时间的差值越大,用户就会觉得动画过程越卡。那么,在这10毫秒内要完成什么事情呢?当使用JavaScript实现视觉交互效果时,一般要经过以下流程:

JavaScript视觉交互执行流程

  1. JavaScript的执行。例如修改元素的样式,或者给元素添加/删除样式类。
  2. 样式计算。根据样式规则计算出元素的最终样式。
  3. 布局(layout)。根据上一步的结果,计算元素占据的空间大小及其在屏幕的位置。注意,一个元素布局上的变化有可能会引发其他元素的联动变化。
  4. 绘制(paint)。填充像素的过程,包括元素的每个可视部分。一般来说,绘制是在多个层上进行的。
  5. 合成(composite)。把各层按正确顺序合并成一个层,显示到屏幕上。

值得注意的是,并非每一帧都会经过上述每一个步骤的处理。如果元素的几何属性(尺寸、位置)没有变化,就不需要进行布局;如果连元素的外观都没有改变,就不需要绘制。所以,实现流畅动画的关键就在于如何减少布局和绘制

位移

对于位移动画来说,最直接的实现方式,就是把元素设成绝对定位,然后去改变它的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面板录制动画过程的性能日志,如下图所示:

left动画过程性能日志

可见,元素在移动的过程中不断触发了布局和绘制。所以,这种实现方式的性能是极低的。网上诸多文献会推荐以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动画过程性能日志

可见,仅仅是在动画开始和结束两个时间点触发了绘制,而布局则完全没有触发。这样一来,性能就有了很大的提升。但是,这里还有两个疑问:

  • 为什么transform动画过程没有触发布局和绘制?
  • 为什么动画开始前触发了两次绘制,动画结束之后触发了一次绘制?

要回答这两个问题,就得了解合成层。

合成层

当满足某些条件的时候,元素在渲染时会被分配到一个独立的层中进行渲染,只要该层的内容不发生改变,就不会触发绘制,浏览器会直接通过合成形成一个新的帧。常见的提升为合成层的条件包括:

  • 对opacity或transform应用了animation或transition;
  • 有 3D transform ;
  • will-change设置为opacity或transform。

很明显,上一节的transform位移动画满足了第一个条件。所以整个动画的渲染过程是这样的:

  • 动画开始时,由于div.page被提升为独立的合成层,所以它要重新绘制;而document所在层相当于少了一块内容,也得重新绘制;
  • 动画过程中,div.page没有其他变化,所以不触发布局和绘制;
  • 动画结束后,div.page不再是独立的合成层,回到了document所在层,所以document又重新绘制了一遍。

如果让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;
}

录制性能日志如下:

合成层transform动画过程性能日志

可见,已经不存在绘制的步骤了。

顺带一提,Chrome开发者工具中有一个Layers面板,可以方便地查看页面上合成层以及成为合成层的原因。

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动画过程性能日志

在常规认知中,opacity的变化并不会导致元素位置和尺寸的变化,理应不会触发布局。但上述过程中确实触发了一次布局,表现较为诡异。接下来给div.page添加「will-change: opacity」使其一直在独立的合成层中渲染。录制性能日志如下:

合成层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;
}

按照前文的描述,动画过程会触发:

  • 一次布局,在动画开始时触发,由opacity引起;
  • 两次绘制,在动画开始时触发,因opacity以及提升为独立合成层引起;
  • 由独立合成层回到document所在层时引起。

倘若加上「will-change: transform, opacity」,使div.page一直在独立的合成层中渲染,则只触发一次绘制,由opacity引起。

然而,创建一个新的合成层并不是免费的,它会导致额外的内存开销。在单页应用中,应用页面过渡动画的元素是页面的最外层容器,包含了该页面所有内容结构。如果让其长期在独立的合成层中渲染,那内存的消耗是非常大的。

所以,可以仅在动画过程中让其在独立的合成层中渲染,而在其他情况下则维持常规状态。

transform和fixed的冲突

如果用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与fixed的冲突

可以看到,固定定位的黄色元素是在动画结束后才突然出现的。那在这之前它跑到哪去了呢?

如果给一个固定定位元素的任意一个祖先元素设置样式「transform」或者「will-change: transform」,那么该元素就会相对于最近的设置了上述样式的祖先元素定位。

因为div.page的高度设成了150%,所以,在动画过程中,黄色元素实际上是跑到了页面的最底下(超出了浏览器可视范围)去了。而在某些比较旧(如 iOS 9 的Safari)的移动端浏览器中,问题更为严重,固定定位的元素可能会消失掉再也不出现。

网上能查到的解决方案有两种:

  • 通过绝对定位模拟固定定位。虽然是可行的,但是在移动端浏览器内,交互上会有一些细节问题,而且元素内部的滚动很容易与页面滚动冲突。
  • 把固定定位的元素放到应用transform动画的元素外。但这对使用「Vue.js」这类框架开发的单页应用来说可行性较低,因为在这类框架中,一个页面就是一个组件,单独把页面中的某个元素抽离出来是比较麻烦的。

所以,这里介绍第三种方案——在页面过渡动画结束之后(此时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>

运行效果如下:

解决transform与fixed的冲突

这样一来,整个交互就较为友好了。这同时也说明:技术上的问题,不一定只能通过技术去解决,也可以从交互上去寻求解决方案。

参考文献

本文同时发布于作者个人博客 https://mrluo.life/article/detail/141/page-transition-optimization

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值