40

页面过渡动画优化探索

 5 years ago
source link: http://www.heeroluo.net/article/detail/141/page-transition-optimization?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

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

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

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

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

6raYZbe.jpg!web

  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面板录制动画过程的性能日志,如下图所示:

Af6jq2E.png!web

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

录制性能日志如下图所示:

36RRvyj.png!web

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

  • 为什么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;
}

录制性能日志如下:

BV3aQjE.png!web

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

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

iQbIbeb.png!web

(注意:由于低版本浏览器不支持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;
}

录制性能日志如下图所示:

yqYVFrf.png!web

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

3Aj2e2N.png!web

可见,还是会触发一次绘制。而针对这「一次的布局」和「一次的绘制」,我进行了进一步的实验,得出的结论是: 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>

运行效果如下:

VNBvuiJ.gif

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

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

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

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

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

所以,这里介绍第三种方案—— 在页面过渡动画结束之后 (此时transform样式已被移除), 再让固定定位的元素添加到页面容器 。并且,为了让它的出现显得不那么突然,增加缓动动画。代码主要修改点如下:

@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>

运行效果如下:

ZJVZjij.gif

这样一来,效果就好多了。这同时也说明:技术上的问题,也可以从交互上去寻求解决方案。

参考文献


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK