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

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

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

目前大多数设备的屏幕刷新率为60次/秒,算下来每个帧的预算时间约为16.66毫秒(1/60秒)。考虑到浏览器还有其他工作要执行,实际上预算时间只有10毫秒。跟此预算时间的差值越大,用户就会觉得动画过程越卡。那么,在这10毫秒内要完成什么事情呢?当使用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面板录制动画过程的性能日志,如下图所示:


可见,元素在移动的过程中不断触发了布局和绘制。所以,这种实现方式的性能是极低的。网上诸多文献会推荐以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动画过程没有触发布局和绘制?
  • 为什么动画开始前触发了两次绘制,动画结束之后触发了一次绘制?

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

合成层

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

  • 对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;
}

录制性能日志如下:


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

顺带一提,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;
}

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

  • 一次布局,在动画开始时触发,由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」或者「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>

运行效果如下:


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

参考文献

  • 《渲染性能》
  • 《坚持仅合成器的属性和管理层计数》
  • 《无线性能优化:Composite》

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

[贝聊科技]小动画大学问相关推荐

  1. [贝聊科技]如何实现一个 AttributedLabel

    作者:陈浩 贝聊科技移动开发部 iOS 工程师 Core Text 是苹果提供的富文本排版技术,可以定制开发图文混排功能,DTCoreText.Nimbus.YYLabel 等优秀的开源库底层都是基于 ...

  2. [贝聊科技]贝聊 iPhone X 适配实战

    @NewPan 贝聊科技 iOS 菜鸟工程师 这款为天猫定制的 iPhone,你买了吗?由于没摸过真机,所以严格意义上来说,这篇文章应该有一个更加接地气的名字:"模拟器适配实战". ...

  3. [贝聊科技]如何将 iOS 项目的编译速度提高5倍

    前言 贝聊目前开发的两款App分别是贝聊家长版和贝聊老师版,最近因为在快速迭代开发新功能,项目规模急速增长,单个端业务代码约23万行,私有库约6万行,第三方库代码约15万行,单个客户端的代码行数约60 ...

  4. [贝聊科技]贝聊 IAP 实战之订单绑定

    大家好,我是贝聊科技 的 iOS 工程师 @NewPan. 注意:文章中讨论的 IAP 是指使用苹果内购购买消耗性的项目. 这次为大家带来我司 IAP 的实现过程详解,鉴于支付功能的重要性以及复杂性, ...

  5. [贝聊科技]如何在iOS开发中更好的做假数据?

    当工期比较紧的时候,项目开发中会经常出现移动端等待后端接口数据的情形,不但耽误项目进度,更让人有种无奈的绝望.所以在开发中,我们常常自己做些假数据,以方便开发和UI调试.然而做假数据方法不同,效率和安 ...

  6. [贝聊科技]网页端「应用跳转」技术实现演变

    本文作者:Mr.Luo ,贝聊前端经理.本文同时发布于作者 个人博客 . 由于网页传播的便捷性,从网页向APP导流几乎是所有APP厂商都会采用的推广手段,具体来说就是在网页上提供一些触发点(例如按钮. ...

  7. [贝聊科技]谈谈 iOS 如何动态切换 APP 的主题

    在移动互联网的下半场,越来越多的 APP 更加注重用户体验,以期来打动用户.主题的切换就是可以增强用户体验.结合运营活动的一个点:譬如 QQ 的夜间模式,节日里电商 APP 的皮肤切换等等的这些小细节 ...

  8. [贝聊科技]iOS 代码架构(一)如何创建一个易复用的组件

    前言 贝聊的移动客户端分别有家长端和老师端,一家公司里同时维护多个业务上有关联性的app这种情况其实很常见,例如一些提供 O2O 服务的公司,经常会分用户端和商家端.这些客户端虽然各自负责着一个业务环 ...

  9. [贝聊科技]有关Android应用桌面角标(BadgeNumber)实现的探讨

    作者:小强 贝聊移动开发部 Android工程师 前言:本文主要讲述了以下三方面: 怎么在Android系统下让自家的应用图标像iOS系统那样支持数字角标的显示? 在网上找不到现成的解决方案的情况下, ...

最新文章

  1. 尺度不变特征变换匹配算法详解
  2. jenkins+docker的简单项目部署
  3. unity3d版本控制的设置方法(SVN)
  4. mysql 限定查询_MySQL Limit 限定查询记录数
  5. python动态图-Python图像处理之gif动态图的解析与合成操作详解
  6. python一个类调用另一个类的方法_python 类静态方法实例化另一个类对象的问题?...
  7. MySQL也有潜规则 – Select 语句不加 Order By 如何排序?
  8. windows bat 进入或跳转到其它目录命令
  9. cocos2d-x for wp 之Box2D的应用
  10. vs能运行python吗_vs怎么运行python(vs能运行python吗)
  11. 日语等级考试测试网站
  12. js scrollTop, 滚动条操作
  13. 【Python】简单实现显示图片的高斯和中值滤波效果
  14. 【资产管理】2020年海外头部资管机构经营特点及启示
  15. 向量化计算cell_Matlab向量化编程在二级劝退学科中的一个应用例子
  16. jpg png jpeg 图片无损压缩工具
  17. php 使用alert,PHP实现通用alert函数的方法
  18. mysql implode_PHP implode() 函数
  19. 微信图片去除马赛克_微信怎么把图片加上马赛克_微信如何将照片打码的方法介绍_3DM手游...
  20. TJOI2015 弦论

热门文章

  1. windows更改服务名称_如何在Windows 10的登录屏幕上更改名称
  2. html与CSS的学习笔记
  3. 解决Mac电脑下Sublime Text3快捷键html:5+Tab没有反应
  4. AD导入CAD图纸笔记
  5. java案例大象和鹦鹉_小象调皮装死,象妈妈急忙找来饲养员,被拆穿后小象反应太可爱...
  6. mac 在 terminal 终端快速打开 vscode
  7. 路由器中继间歇性断网怎么处理?
  8. 关于win7下魔兽争霸不能全屏的问题
  9. 20-输出前m大的数
  10. Unity Shader - Metallic mode: Metallic Parameter 金属模式的参数