Community编程与开发github.com

RevanthThota55/FreeFrame-Studio

将网页动画保持在帧预算内——只动画合成属性、页面繁忙时将动画移出主线程、并能定位动画掉帧的根源。涵盖布局/绘制/合成管线、更廉价的属性替换、CSS vs WAAPI vs requestAnimationFrame、Framer Motion 的硬件加速路径、每帧的 React 重渲染、继承 CSS 变量重算、模糊代价以及 `will-change` 真正生效的场景。

FreeFrame-Studio 是什么?

该技能专注于诊断和修复网页动画的性能问题。它帮助开发者确保动画的每一帧都能在预算时间内完成(60fps 下为 16.7ms,120Hz 下为 8ms),从而避免掉帧和卡顿。技能覆盖了浏览器渲染管线(布局→绘制→合成)中的三个成本层级,指导开发者始终优先使用仅触发合成的 `transform` 和 `opacity` 属性,而非昂贵的布局属性。它还深入分析了动画驱动方式的影响:CSS 和 WAAPI 可以脱离主线程运行,而 `requestAnimationFrame` 始终运行在主线程上,会在页面繁忙时导致竞争。对于 React 环境,技能指出了通过状态更新每帧重渲染的常见陷阱,以及继承 CSS 变量(如 Framer Motion 的简写属性)导致样式重算随内容增长而膨胀的根本原因。此外,技能提供了结构化的故障排查流程(从检查动画属性、确认主线程负载、到添加 `will-change`)和清晰的报告格式,帮助开发者准确诊断并修复问题。

兼容平台~Claude Code~Codex CLI~Cursor
npx skills add https://github.com/RevanthThota55/FreeFrame-Studio/tree/HEAD/.claude/skills/animation-performance

Installed? Explore more 编程与开发 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

动画性能

初始回应

当此技能首次被调用且未提供具体问题时,仅回应:

我已准备好找出你的动画掉帧的原因并使其流畅,我的知识源于 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,使其脱离主线程运行
Framer Motion 的 x/y 掉帧动画完整的 transform 字符串
手写动画在 React 中掉帧写入 ref.current.style,而不是状态
随着列表增长拖动变得迟缓停止动画继承的 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 / padding 来缩放scale()
top / left / margin 来移动translate()(百分比是相对于元素自身尺寸)
display / visibility 切换opacity
box-shadowfilter: drop-shadow(…)

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

两点结论:

  • 布局动画并不总会掉帧。 一个绝对定位的元素,或子元素较少的元素,可以动画 padding 而不会有可见成本。但 padding 动画和 scale() 动画看起来一样,而只有其中一个不会在较慢的设备上退化。选择那个没有缺点的方案。
  • 布局属性还会移动错误的东西。 动画侧边栏的 margin-left 每帧都会推动旁边的内容;translateX 则不会。对于在打开的侧边栏旁边的仪表盘,这意味着整个页面每秒重新布局 60 次。

它在哪里运行

JavaScript 始终运行在主线程上。 由 requestAnimationFrame 驱动的动画会与浏览器正在做的其他所有事情竞争。当主线程繁忙时,动画的代码无法运行,帧就会延迟。

驱动线程
JS + requestAnimationFrame (GSAP, motion/react 组件)始终在主线程
网页动画 API (element.animate(), Motion 的 animate())可硬件加速
CSS 过渡和关键帧可硬件加速

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

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

需要警惕的模式是页面执行繁重工作期间运行的运动:页面过渡、导航期间的标签指示器、任何与数据获取或水合同时触发的事情。Vercel 的仪表盘使用 Framer Motion 的共享布局动画来动画活动标签指示器。浏览器正好在那一刻加载下一页,因此掉帧了。将其迁移到 CSS 动画解决了问题。

Framer Motion 可以被加速,但只有当 transform 是一个字符串时:

<motion.div animate={{ x: 100 }} />                          // 主线程,可能卡顿
<motion.div animate={{ transform: "translateX(100px)" }} />  // 硬件加速

x, y, scale, rotate 简写属性是通过 CSS 变量实现的,而动画的 CSS 变量无法被加速。简写属性可读性更好,因此默认使用它们,并在动画需要在主线程繁忙时存活时切换到字符串形式。

加速是渐进增强。 浏览器会在版本之间变化的条件下静默拒绝加速;例如,Chrome 历史上不会加速基于百分比的变换。这不是停止使用 translateY(100%) 的理由。对于未知大小的元素,百分比是正确的选择(这正是 Sonner 和 Vaul 定位通知提示和抽屉的方式),正确性胜过尽力而为的优化。

React:不要通过状态来动画

动画库在 React 的渲染周期之外运行,所以它们没问题。陷阱是手写动画每帧都调用 setState:组件每 16.7ms 重新渲染一次,仅渲染一项就超出了预算。

// 掉帧:每帧一次重渲染
setY(nextY);

// 流畅:无重渲染
ref.current.style.transform = `translateY(${nextY}px)`;
// 或者使用 motion value: y.set(nextY)

注意 requestAnimationFrame、pointermove、scroll 或拖动处理函数中的状态更新。

两个看起来正确的陷阱

两者都使用了 transform 但仍然掉帧。

继承的 CSS 变量。 当抽屉中容纳了大约 20 个以上项目时,Vaul 的拖动变得迟钝,且没有涉及重新渲染。拖动距离被写入到一个 CSS 变量中,translateY() 读取它:

// 每帧为每个后代重新计算样式
const style = { "--swipe-amount": `${draggedDistance}px` };

// 只影响此元素
const style = { transform: `translateY(${draggedDistance}px)` };

CSS 变量会继承,因此在父元素上更改它会触发每个子元素的样式重新计算。内容越多,每帧越慢。当 transform 动画随着内容增长而变差时,请怀疑此原因。这也是 Framer Motion 简写属性无法被加速的相同原因。

模糊。 filter: blur() 的成本会迅速升高,尤其是在 Safari 中。将动画模糊保持在 20px 以下;2–5px 足以柔化交叉淡入淡出。

will-change

浏览器可以在动画进行中将动画从 CPU 移交给 GPU,这个移交可能会在开始或结束时使元素偏移 1 个像素。will-change 提前提升元素,因此不会发生移交:

.drawer {
  will-change: transform;
}

仅在你看到偏移或测量到掉帧时才添加它。 浏览器已经会为动画 transform 或 opacity 的元素提升图层,因此在常见路径上它什么也不做,而且每个提升的图层都会消耗 GPU 内存。将它放在动画的元素上,永远不要放在 * 或大型容器上。

它能发挥价值的地方是昂贵的绘制,例如大面积的模糊或滤镜元素。提升它并保持图层较小;小图层的合成成本比全屏图层低。

应该避免 JS 动画吗?

不。主线程动画只会在页面在其期间繁忙时受影响,这很少见,而且现在你知道如何发现了。

Framer Motion 能给你 CSS 无法做到的东西:真正的弹簧、共享布局动画、卸载组件的退出动画、动量手势。问题是你是否需要这些,而不是是否允许 JS 动画。

工作分工: CSS 用于简单运动以及任何在页面繁忙时播放的动画;Framer Motion 用于复杂、可中断、由手势驱动的运动。

工作流程

阶段 1:找出动画的内容

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

元素驱动器属性播放时机
标签指示器motion/react layoutIdlayout路由变化
抽屉CSS transitiontransform用户拖动
卡片悬停CSS transitionall悬停

将 transition: all 和 Motion 简写属性展开为它们实际动画的内容。

阶段 2:诊断

按此列表逐一排查。它按每个原因出现的频率排序:

  1. 哪些属性被动画? 任何不是 transform/opacity 的属性都是首要怀疑对象:首先是布局属性,然后是 box-shadow 和 border-radius,它们看起来不像布局并容易被忽略。
  2. 卡顿是否与页面繁忙同时发生? 导航、数据获取、水合、长任务。将动画迁移到 CSS 或 WAAPI。
  3. React 是否在每帧重新渲染? rAF、指针或滚动处理函数中的状态更新。
  4. 是否随着内容增长而变得更糟? 继承的 CSS 变量,或者同时动画的节点太多。
  5. CSS 变量是否被动画了? 直接地,或通过 Motion 的 x/y/scale。
  6. 有任何动画的 blur() 超过 20px 吗?
  7. 仍然偏移或卡顿? 现在添加 will-change: transform 到动画元素上。

通过录制确认,而不是凭感觉:使用 DevTools 性能面板查看掉帧和长任务,使用动画面板放慢运动。通过远程调试在真实的中端手机上进行分析。桌面上的 CPU 限制模拟的是弱 CPU,而不是弱 GPU 或小的内存预算。

阶段 3:报告

对于每个发现:

**[元素]**:`file.tsx:42`
原因:[上方列表中的哪个原因]
之前:`transition: all 200ms`(动画 `padding`,触发布局)
之后:`transition: transform 200ms` 配合 `scale(0.97)`
验证方式:来自代码 / 需要设备录制

按成本对发现进行排序:布局优先于绘制,繁忙主线程优先于空闲。然后停止。在被要求时才应用修复。

Individual skills in this repo

This repo contains 3 individual skills — each has its own dedicated page.

相关技能