[译] JavaScript 是如何工作的:CSS 和 JS 动画背后的原理 + 如何优化性能

4,289 阅读13分钟

这是专门探索 JavaScript 及其构建组件系列的第 13 篇文章。在识别和描述核心元素的过程中,我们还分享了构建 SessionStack 时的一些经验法则,SessionStack 是一个足够强大且高性能的 JavaScript 应用程序,用来帮助用户实时查看和重现其 Web 应用程序的缺陷。

如果你错过了前面的章节,你可以在这里找到它们:

  1. [译] JavaScript 是如何工作的:对引擎、运行时、调用堆栈的概述
  2. [译] JavaScript 是如何工作的:在 V8 引擎里 5 个优化代码的技巧
  3. [译] JavaScript 是如何工作的:内存管理 + 处理常见的4种内存泄漏
  4. [译] JavaScript 是如何工作的: 事件循环和异步编程的崛起 + 5个如何更好的使用 async/await 编码的技巧
  5. [译] JavaScript 是如何工作的:深入剖析 WebSockets 和拥有 SSE 技术 的 HTTP/2,以及如何在二者中做出正确的选择
  6. [译] JavaScript 是如何工作的:与 WebAssembly 一较高下 + 为何 WebAssembly 在某些情况下比 JavaScript 更为适用
  7. [译] JavaScript 是如何工作的:Web Worker 的内部构造以及 5 种你应当使用它的场景
  8. [译] JavaScript 是如何工作的:Web Worker 生命周期及用例
  9. [译] JavaScript 是如何工作的:Web 推送通知的机制
  10. [译] JavaScript 是如何工作的:用 MutationObserver 追踪 DOM 的变化
  11. [译] JavaScript 是如何工作的:渲染引擎和性能优化技巧
  12. [译] JavaScript 是如何工作的:网络层内部 + 如何优化其性能和安全性

概览

你也知道,动画在创造吸引人的 web app 中扮演着重要的角色。随着用户越来越多地将注意力转移到用户体验上,商家也开始意识到完美、愉悦的用户体验的重要性,web app 变得更加重要,并且 UI 更趋于动效。这一切都需要更复杂的动画,以便在用户使用中实现更平滑的状态转换。今天,这甚至不被认为是特别的。用户正在变得越来越挑剔,默认期望高效响应和互动的用户界面。

但是,动效化你的界面可没那么简单。将什么做成动画,什么时候,做成什么样的动画,都是棘手的问题。

JavaScript 和 CSS 动画

创建网页动画的两种主要方式是使用 JavaScript 和 CSS。没有绝对优劣; 一切都取决于你的目标。

CSS 动画

用 CSS 实现动画是让屏幕上的内容移动的最简单方法。

我们将以一个快速示例说明如何在 X 轴和 Y 轴上移动 50 像素的元素。通过设置耗时 1000 ms 的 CSS 过渡来完成的。

.box {
  -webkit-transform: translate(0, 0);
  -webkit-transition: -webkit-transform 1000ms;

  transform: translate(0, 0);
  transition: transform 1000ms;
}

.box.move {
  -webkit-transform: translate(50px, 50px);
  transform: translate(50px, 50px);
}

当类 move 被添加后,transform 的值会改变,过渡开始。

除了过渡的时长,还有其它的用来缓动的选项,这些就是你看到的动画的本质。稍后我们将在本文中更详细地讨论缓动。

如果像上面的代码片段那样创建单独的 CSS 类来管理动画,你就可以使用 JavaScript 来切换每个动画的开启和关闭。

假设你有这样的一个元素:

<div class="box">
  Sample content.
</div>

然后,你可以使用 JavaScript 切换每个动画的开启和关闭:

var boxElements = document.getElementsByClassName('box'),
    boxElementsLength = boxElements.length,
    i;

for (i = 0; i < boxElementsLength; i++) {
  boxElements[i].classList.add('move');
}

上面的代码片段获取了所有具有 box 类的元素,并添加了 move 类以触发动画。

这样做可以为你的 app 提供很好的平衡。你可以专注于使用 JavaScript 管理状态,并简单地在目标元素上设置适当的类,让浏览器处理动画。如果沿着这条路线走下去,你可以监听 transitionend 元素上的事件,但前提是你能够放弃对旧版 Internet Explorer 的支持:

监听 transitioned 过渡结束时触发的事件,如下所示:

var boxElement = document.querySelector('.box'); // 获取有 box 类的第一个元素。
boxElement.addEventListener('transitionend', onTransitionEnd, false);

function onTransitionEnd() {
  // 处理过渡完成。
}

除了 CSS 过渡,你还可以使用 CSS 动画,它使你对动画关键帧、持续时间、重复有更多的控制。

关键帧用于指示浏览器在给定点处 CSS 属性应该有什么值,并填补(关键帧之间的)空白。

我们看个例子:

/**
 * 这是没有加浏览器属性前缀的简化版本
 * 如果加上,会比较冗长!
 */
.box {
  /* 指定动画 */
  animation-name: movingBox;

  /* 动画时长 */
  animation-duration: 2300ms;

  /* 动画重复次数 */
  animation-iteration-count: infinite;

  /* 动画正反交替进行 */
  animation-direction: alternate;
}

@keyframes movingBox {
  0% {
    transform: translate(0, 0);
    opacity: 0.4;
  }

  25% {
    opacity: 0.9;
  }

  50% {
    transform: translate(150px, 200px);
    opacity: 0.2;
  }

  100% {
    transform: translate(40px, 30px);
    opacity: 0.8;
  }
}

它的效果是这样的(快速演示) — sessionstack.github.io/blog/demos/…

使用 CSS 动画,你可以独立于目标元素来定义动画本身,并使用 animation-name 属性选择所需的动画。

CSS 动画有时仍然是需要浏览器属性前缀的,-webkit- 用于 Safari,Safari Mobile 和 Android。Chrome,Opera,Internet Explorer 和 Firefox 都会在没有前缀的情况下起作用。许多工具可以帮助你创建所需 CSS 的浏览器属性前缀,从而允许你在源文件中编写无前缀的版本。

JavaScript 动画

与使用 CSS 过渡或动画相比,使用 JavaScript 创建动画更复杂,但它通常为开发人员提供了更强大的功能。

JavaScript 动画是作为代码的一部分内联编写的。你也可以将它们封装在其他对象中。下面是你需要用 JavaScript 来编写的重新创建前面描述的 CSS 过渡:

var boxElement = document.querySelector('.box');
var animation = boxElement.animate([
  {transform: 'translate(0)'},
  {transform: 'translate(150px, 200px)'}
], 500);
animation.addEventListener('finish', function() {
  boxElement.style.transform = 'translate(150px, 200px)';
});

默认情况下,Web 动画仅修改元素的显示。如果你想让你的对象留在它被移动到的位置,那么当动画完成时你应该修改它的底层样式。这就是为什么我们要监听 finish 事件,并将 box.style.transform 属性设置为 translate(150px, 200px),这与我们动画的第二个变换相同。

使用 JavaScript 动画,你可以在每一步完全控制元素的样式。这意味着你可以放慢动画,暂停动画,停止动画,反转动画,并根据需要操作元素。如果你构建复杂的面向对象的 app,这一点尤其有用,因为你可以适当地封装你的行为。

什么是缓动?

自然动作让你的用户对你的 web app 感到更加舒适,从而带来更好的用户体验。

自然情况下,没有什么东西是从一个点到另一个点做线性移动的。事实上,随着它们在我们周围的物质世界中移动,事物往往会加速或减速,因为我们并非处于真空状态,并且存在影响这个因素的不同因素。人类的大脑受制于此会期望这种运动,所以当你为 app 制作动画时,你应该利用这些知识为你带来好处。

有一些术语需要了解一下:

  • 「ease-in」 - 开始慢,然后加速。
  • 「ease out」 - 开始快,然后减速。

两个可以合并,比如「ease in out」。

缓动可以让动画感觉起来更自然。

缓动关键词

CSS 过渡和动画允许你选择想要使用的缓动类型。有不同的会影响动画缓动的关键词。当然你完全可以使用自定义的缓动。

以下是你可以在 CSS 中用来控制缓动的一些关键词:

  • linear
  • ease-in
  • ease-out
  • ease-in-out

我们逐一研究,看看究竟是什么意思。

线性(linear)动画

没有任何缓动的动画称为线性动画。

以下是线性过渡的图示:

随着时间的推移,值会等量增加。使用线性运动时,总会感觉不自然。一般来说,你应该避免线性运动。

这是一个简单的实现线性动画的方式:

transition: transform 500ms linear;

缓出(ease-out)动画

前面已经说过,缓出动画与线性动画相比更快地开始,而后变慢。这就是它的图示:

一般来讲,缓出是用户界面工作的最好选择,因为快速开始给你一种快速响应的感觉,而因为不一致运动在结束时慢下来感觉比较自然。

有很多实现缓出效果的方法,但最简单的就是使用 CSS 关键词:

transition: transform 500ms ease-out;

缓入(ease-in)动画

它与缓出相反 —— 开始慢,结束快。图示如下:

与缓出相比,缓入感觉不太自然,因为它开始慢给人一种无响应的感觉。快速结束也很奇怪,因为整个动画是在加速,而在现实世界中,物体在忽然停止时往往会减速。

要使用缓入动画,类似于缓出或者线性动画,使用关键词:

transition: transform 500ms ease-in;

缓入缓出(ease-in-out)动画

它是缓入和缓出的结合,图示如下:

不要用于持续时间过长的动画,这会让人觉得你的用户界面无响应。

使用 CSS 关键词 ease-in-out 实现缓入缓出动画:

transition: transform 500ms ease-in-out;

自定义缓动

你可以定义自己的缓动曲线,从而更好地控制项目动画的形成的感受。

实际上,ease-inease-outlinearease 关键词可以对应到预定义的贝塞尔曲线里,这在CSS 过渡规范网络动画规范里有详细说明。

贝塞尔曲线

让我们看一下贝塞尔曲线的工作原理。贝塞尔曲线有四个值,或者更确切地说,它需要两对数字。每对描述三次贝塞尔曲线控制点的 X 和 Y 坐标。贝塞尔曲线的起点坐标是 (0, 0),终点坐标是 (1, 1)。你可以设置这两组数。两个控制点的 X 值必须在 [0, 1] 范围内,并且每个控制点的 Y 值可以超过 [0, 1] 限制,尽管规范没有明确说超过多少。

即使每个控制点的 X 和 Y 值发生轻微变化,都会给你一个完全不同的曲线。我们来看看两个贝塞尔曲线图,点的坐标相近但不同。

如你所见,两个图区别比较大。两条曲线的第一个控制点的矢量差为 (0.045, 0.183),第二个控制点差 (-0.427, -0.054)。

第二条曲线的 CSS 写法如下:

transition: transform 500ms cubic-bezier(0.465, 0.183, 0.153, 0.946);

前两个数字是第一个控制点的 X 和 Y 坐标,后两个数字是第二个控制点的 X 和 Y 坐标。

性能优化

无论何时动画,你都应该保持 60 fps,否则会对用户的体验产生负面影响。

与世界上其他所有的东西一样,动画也是有代价的。动画一些属性比其他属性更便宜。例如,动画修改一个元素的 widthheight 会改变它的形状,而且可能引起页面上其它元素的移动和形状改变。这个过程称为布局。我们在之前的一篇文章中已经详细讨论过布局和渲染。

一般来说,你应该避免使用触发布局或绘制的属性动画。对于大多数现代浏览器,这意味着将动画(修改的属性)限制为 opacitytransform.

will-change

你可以使用 [will-change](https://dev.w3.org/csswg/css-will-change/) 通知浏览器你打算更改元素的属性。浏览器会在你进行更改之前做最合适的优化。但不要过度使用 will-change,因为这样做会浪费浏览器资源,从而导致更多的性能问题。

可以这样为变换和不透明度添加 will-change

.box {  will-change: transform, opacity;}

Chrome,Firefox 和 Opera 的浏览器支持非常好。

选 JavaScript 还是 CSS?

你可能知道了 —— 这个问题没有正确或错误的答案。你只需要记住以下几点:

  • 基于 CSS 的动画和原生支持的 Web 动画通常在称为「合成器线程」的线程上处理。它与浏览器的「主线程」不同,在该主线程中执行样式,布局,绘制和 JavaScript。这意味着如果浏览器在主线程上运行一些耗时的任务,这些动画可以继续运行而不会中断。
  • 在许多情况下,transformsopacity 都可以在合成器线程中处理。
  • 如果任何动画出发了绘制,布局,或者两者,那么「主线程」会来完成该工作。这个对基于 CSS 还是 JavaScript 实现的动画都一样,布局或者绘制的开销巨大,让与之关联的 CSS 或 JavaScript 执行工作、渲染都变得毫无意义。

选择合适的对象来做动画

优秀的动画能让用户对你的项目的享受和参与感更添一层。无论你是喜欢宽度,高度,位置,颜色还是背景,你可以制作任何你喜欢的任何动画,但你需要了解潜在的性能瓶颈。选择不当的动画会对用户体验产生负面影响,因此动画需要兼具性能和适当性。动画越少越好。动画只是为了让你的用户体验感觉自然,但不要过度使用动画。

用动画来增强交互

不要只是因为你能就做动画。相反,使用策略性放置的动画来增强用户交互。避免不必要的中断或阻碍用户活动的动画。

避免高代价动画属性

唯一比放置得不好的动画还糟糕的是那些导致页面卡顿的动画。这种类型的动画让用户感到沮丧和不快乐。

SessionStack 中使用动画非常简单。总的来说,我们遵循上述做法,但由于 UI 的复杂性,我们还有更多利用动画的场景。SessionStack 必须像视频一样重新创建用户在浏览 web app 时遇到问题时发生的所有内容。为此,SessionStack 仅利用会话期间我们的库收集的数据:用户事件,DOM 更改,网络请求,异常,调试消息等。我们的播放器经过高度优化,可以正确呈现和使用所有收集的内容数据,以便从视觉和技术角度出发,为终端用户的浏览器及其中发生的所有事情提供像素级的模拟。

为了确保复制得自然,尤其是在长时间和繁重的用户会话中,我们使用动画正确指示加载/缓冲,并遵循关于如何实现它们的最佳实践,以便我们不占用太多 CPU 时间并让事件轮询自由地渲染会话。

如果你想试试 SessionStack,有免费方案哦。

资源


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOS前端后端区块链产品设计人工智能等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏