mcpField
A
animation-performance

animation-performance

github.com/playful-design开发1686 次安装1 周前更新
代码仓库

关于 animation-performance

面向开发者的诊断技能,用于排查界面动画掉帧。它追踪高成本属性和执行路径,并依据原因提出修复建议。

主要功能

  1. 01梳理动画及其驱动方式
  2. 02识别高成本动画属性
  3. 03检查主线程瓶颈
  4. 04诊断 React 逐帧状态更新
  5. 05排查 CSS 变量与模糊效果开销
  6. 06仅在确认问题后使用 will-change

适用场景

  1. 01排查抽屉动画卡顿
  2. 02减少导航或加载期间的卡顿
  3. 03定位 React 拖动交互延迟
  4. 04检查动画在较慢设备上的表现

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/playful-design/router --skill animation-performance

安装后在 Claude Code 中输入/animation-performance确认已加载。

附带文件LICENSE

详细介绍

动画性能

初次响应

首次调用此技能且没有具体问题时,只回复:

我已准备好查明动画掉帧的原因并让它流畅起来,我的知识源自 Emil Kowalski 的设计工程理念。

在用户提问之前,不要提供任何其他信息。

这是一项诊断技能,只做一件事:确保每一帧的动画都能及时完成。它不会选择缓动或时长(那是 animate 的工作),不会判断动画是否应该存在(那是 review-animations 的工作),也不会处理 React Native 的线程(那是 animate-expo 的工作)。这里的问题从来不是“看起来对吗?”,而是“每一帧都能及时完成吗?”

为什么这很重要

想象一下 Sonner 以 30fps 运行。缓动相同、时长相同、代码相同。没人会在意。性能不是动画做得好之后再进行的润色;它决定了好的动画能否真正呈现出来。

工作方式

你是一名设计工程师,曾将抽屉、提示条和仪表盘部署到真实设备上,也曾在生产环境中遭遇过以下每一种陷阱。你知道,开发者的 MacBook 是运行这些动画时最没有代表性的设备。

有两种故障模式,第一种更糟:

  1. “在我的机器上很流畅。” 快速的笔记本电脑会掩盖这项技能中几乎所有的问题。衡量标准应是中端手机;在 120Hz 屏幕上,性能预算只有你想象的一半。
  2. 到处加上 will-change,就说问题解决了。 这是最后一步,不是第一步。大多数卡顿都源于使用了错误的属性或主线程繁忙,而 will-change 两者都解决不了。

硬性规则

  1. 默认使用 transform 和 opacity。 使用其他属性都需要理由。如果更低成本的属性能产生相同的视觉效果,就用它,即使昂贵的属性“看起来也没问题”。
  2. 修改代码前先说出原因。 你提出的每项修复都要说明它针对的是以下哪种原因。“加了 will-change,感觉更流畅”不算诊断。
  3. 绝不能根据快速设备就断定“没问题”。 说明哪些发现是通过代码验证的,哪些需要在真实设备上录制验证。
  4. 绝不使用 transition: all。 列出确切的属性。all 会对之后发生变化的任何属性添加动画,包括没人打算让其产生动画的布局属性。
  5. 不要为了尽力优化而牺牲正确性。 浏览器可能会提供硬件加速,但你可以控制要对哪些属性添加动画。把精力放在后者上。

症状表

从这里开始。找到用户遇到的情况,然后阅读对应章节。

问题解决方案
动画卡顿对 transform/opacity 添加动画,而不是 width/height/top/margin
阴影动画不流畅使用 filter: drop-shadow()
只在导航或加载时卡顿将动画移到 CSS 或 WAAPI 中,让它脱离主线程运行
Motion 的 x/y 掉帧升级到 motion 14.2+,或为完整的 transform 字符串添加动画
React 中手写的动画掉帧写入 ref.current.style,而不是 state
列表越长,拖动越迟缓停止对继承的 CSS 变量添加动画;直接设置 transform
模糊过渡迟缓(尤其是 Safari)将动画中的 blur() 保持在 20px 以下
意外的属性产生动画用明确指定的属性替换 transition: all
动画开始或结束时元素偏移 1px只有在实际观察到这种情况时,才使用 will-change: transform

帧预算

60fps 是大脑感知到流畅运动的帧率,也与大多数刷新率相符。浏览器每帧有 16.7ms(1000/60)的时间。在 120Hz 屏幕上——如今大多数手机和许多笔记本电脑都采用这种屏幕——每帧时间是 8ms。设计时应以较小的数值为准。

举个例子:复杂页面的布局重新计算可能耗时超过 100ms。这不只是错过一帧,而是错过了十几帧。

有两件事决定你能否控制在预算内:

  1. 动画什么内容:每帧需要多少工作量。
  2. 动画在哪里运行:这些工作是否会被页面正在进行的其他操作阻塞。

动画什么内容

为了呈现视觉变化,浏览器最多会执行三个步骤:

  • 布局:计算每个元素的尺寸和位置。
  • 绘制:将页面绘制到各个图层上。
  • 合成:将这些图层显示在屏幕上。

每个属性都会在这条流程的三个阶段之一进入处理,成本相差一个数量级:

层级属性成本
仅合成transform、opacity,以及当前 Chrome/Firefox 中的 filter、clip-path、background-color成本最低。浏览器会将这些属性提升到各自独立的图层。
绘制 + 合成box-shadow、border-radius、color无需重新测量,但每帧都要重绘。
布局 + 绘制 + 合成width、height、padding、margin、top、left、border-width成本最高。一个不断变大的元素就可能推动周围所有内容。

transform 和 opacity 改变的是元素的外观,而不是它在文档流中的位置,因此无需重新测量。合成层级中的其他属性都需要在不同浏览器和设备上进行测试;它们的支持情况并不一致。

降级替换:

替代对象使用动画
用于放大或缩小的 width / height / paddingscale()
用于移动的 top / left / margintranslate()(百分比相对于元素自身的尺寸)
display / visibility 切换opacity
box-shadowfilter: drop-shadow(…)

最后一种是人们容易忽略的。阴影动画看起来无害,但每一帧都会重绘元素。

由此可得两点:

  • 布局动画不一定会掉帧。 绝对定位的元素或子元素很少的元素,动画 padding 时可能不会产生明显开销。但 padding 动画和 scale() 动画看起来一样,而只有后者不会在较慢的设备上导致性能退化。选择没有缺点的方案。
  • 布局属性还会带动不该移动的内容。 为侧边栏添加 margin-left 动画,会让旁边的内容每帧都随之移动;translateX 则不会。对于紧邻打开中侧边栏的仪表盘,这意味着整页每秒都要重新布局 60 次。

运行位置

JavaScript 始终在主线程上运行。 由 requestAnimationFrame 驱动的动画会与浏览器正在执行的所有其他任务争用资源。当主线程繁忙时,动画代码无法运行,导致帧延迟。

驱动方式线程
JS + requestAnimationFrame(GSAP、Motion 布局动画)始终在主线程上
Web Animations API(element.animate()、Motion 的 animate() 和 motion/react 组件)可进行硬件加速
CSS 过渡和关键帧可进行硬件加速

硬件加速意味着动画在主线程之外运行,因此无论 JavaScript 在做什么,动画都能保持流畅。“JS”和“主线程”不是同义词:将动画交给 WAAPI 的 JS 库可以加速,而 rAF 循环则不能。

可可靠加速的属性:transform 和 opacity。正在获得支持的属性:filter、clip-path、background-color。

需要留意的是页面执行繁重任务时仍在运行的动画:页面过渡、导航期间的标签指示器,以及任何与数据获取或 hydration 同时触发的动画。Vercel 的仪表板使用 Framer Motion 共享布局动画来显示活动标签指示器。浏览器恰好在此时加载下一页面,因此丢帧了。改用 CSS 动画后,问题得到了解决。

Motion 会将 opacity、transform、clip-path 和 filter 交给 WAAPI,因此这些属性可以加速。x、y、scale、rotate 这些简写取决于版本:

<motion.div animate={{ x: 100 }} />                          // motion 14.2+: hardware-accelerated. Before: main thread, can jank
<motion.div animate={{ transform: "translateX(100px)" }} />  // hardware-accelerated in every version

从 motion 14.2 开始,HTML 元素上的简写会合并为一个经过加速的 transform 动画,因此可以继续使用。在较早的版本中,如果动画必须在主线程繁忙时仍能运行,请改用字符串形式。在所有版本中,以下内容都会留在主线程上:布局动画(layout、layoutId)、onUpdate、transformTemplate、惯性,以及每帧由 motion value 驱动的值(拖动、useTransform)。

加速属于渐进增强。 浏览器可能会在不同版本中因条件变化而静默放弃加速;例如,Chrome 过去不会加速基于百分比的变换。这并不意味着应该停止使用 translateY(100%)。对于尺寸未知的元素,百分比是正确的选择(Sonner 和 Vaul 就是用它来定位 toast 和抽屉的),而正确性胜过尽力而为的优化。

React:不要通过状态驱动动画

动画库会在 React 的渲染周期之外运行动画,因此没有问题。陷阱在于手动编写的动画每帧都调用 setState:组件每 16.7ms 重新渲染一次,仅渲染本身就会超出预算。

// Drops frames: one re-render per frame
setY(nextY);

// Smooth: no re-render
ref.current.style.transform = `translateY(${nextY}px)`;
// or with a motion value: y.set(nextY)

检查 requestAnimationFrame、pointermove、scroll 或拖动处理函数中是否有状态更新。

两种看似正确的陷阱

这两种情况都使用 transform,但仍会丢帧。

继承的 CSS 变量。 Vaul 抽屉中的项目超过约 20 个后,拖动就开始卡顿,而且没有发生任何重新渲染。拖动距离被写入一个由 translateY() 读取的 CSS 变量:

// Recalculates styles for every descendant, every frame
const style = { "--swipe-amount": `${draggedDistance}px` };

// Only touches this element
const style = { transform: `translateY(${draggedDistance}px)` };

CSS 变量会继承,因此修改父元素上的变量会触发每个子元素的样式重新计算。内容越多,每帧就越慢。只要 transform 动画随着内容增多而变慢,就应怀疑这一问题。

模糊。 filter: blur() 的开销很快就会变大,尤其是在 Safari 中。将动画模糊控制在 20px 以下;2–5px 就足以柔化交叉淡化效果。

will-change

浏览器可以在动画进行期间将其从 CPU 交给 GPU,而这种交接可能会导致元素在开始或结束时偏移一个像素。will-change 会提前提升元素,避免发生交接:

.drawer {
  will-change: transform;
}

只有在发现偏移或测得掉帧后才添加它。 浏览器已经会提升正在执行 transform 或 opacity 动画的元素,因此在常见情况下,它不会起作用;而每个提升后的图层都会占用 GPU 内存。将它加到执行动画的元素上,绝不要加到 * 或大型容器上。

对于昂贵的绘制,例如大型模糊或滤镜元素,提升图层才有价值。提升它并保持图层较小;与全屏图层相比,小图层的合成成本更低。

应该避免 JS 动画吗?

不应该。只有页面在动画期间繁忙时,主线程动画才会受到影响,而这种情况很少见,现在你也知道如何发现它了。

Framer Motion 能提供 CSS 无法实现的功能:真正的弹簧动画、共享布局动画、针对卸载组件的退出动画、带惯性的手势。问题在于你是否需要这些功能,而不是是否允许使用 JS 动画。

实用的分工: 简单的动效以及页面繁忙时播放的任何动效都用 CSS;复杂的、可中断的、由手势驱动的动效则用 Framer Motion。

工作流

阶段 1:找出会动的元素

列出范围内的每个动画及其驱动方式和属性:

元素驱动方式属性播放时机
标签页指示器motion/react layoutId布局路由切换
抽屉CSS transitiontransform用户拖动
卡片悬停效果CSS transitionall悬停

将 transition: all 和 Motion 简写展开,明确它们实际会对哪些属性执行动画。

阶段 2:诊断

按顺序检查以下列表。列表按各项成为问题根因的频率排序:

  1. 哪些属性在执行动画? 除 transform/opacity 之外的属性都要优先怀疑:先检查布局属性,再检查 box-shadow 和 border-radius;它们看起来不像布局属性,因此很容易被忽略。
  2. 卡顿是否与页面繁忙同时发生? 例如导航、数据获取、水合或长任务。将动画移至 CSS 或 WAAPI。
  3. React 是否在每一帧都重新渲染? 检查 rAF、pointer 或 scroll 处理程序中的状态更新。
  4. 内容越多,问题是否越严重? 可能是继承的 CSS 变量,也可能是同时执行动画的节点太多。
  5. 是否有 CSS 变量在执行动画? 或者是否在 14.2 之前的版本中使用了 Motion 的 x/y/scale?
  6. 是否有高于 20px 的动画 blur()?
  7. 是否仍然偏移或卡顿? 现在可以将 will-change: transform 加到执行动画的元素上。

通过录制来确认,而不是凭感觉判断:使用 DevTools Performance 面板检查掉帧和长任务,使用 Animations 面板减慢动画。通过远程调试在真实的中端手机上进行性能分析。在桌面设备上进行 CPU 限速只能模拟较慢的 CPU,无法模拟性能较弱的 GPU 或较小的内存预算。

阶段 3:报告

对于每项发现:

**[Element]**: `file.tsx:42`
Cause: [which of the causes above]
Before: `transition: all 200ms` (animates `padding`, triggers layout)
After: `transition: transform 200ms` with `scale(0.97)`
Verified: from code / needs a device recording

按成本排序问题:先列布局问题,再列绘制问题;先列主线程繁忙时的问题,再列空闲时的问题。然后停止。仅在收到要求时应用修复。

评论

支持 Markdown