CSS 动画
CSS VS JS
- 优势: CSS 动画通常比 JavaScript 动画更高效,因为它们可以利用浏览器的优化机制。CSS 动画可以通过 GPU 加速,减少 CPU 的负担,从而提高性能。
- 劣势: CSS 动画的灵活性较低,难以实现复杂的动画效果。JavaScript 动画可以通过编程实现更复杂的逻辑和交互。
- CSS动画可以监听事件吗?
可以监听三个事件:animationstart、animationend、animationiteration(重复播放)。
transition VS animation
transition:用于控制 CSS 属性值的过渡效果,通常用于简单的状态变化,如鼠标悬停时的颜色变化。
- 触发方式:通过改变元素的 CSS 属性触发。
- 执行方式:一次性执行,通常用于简单的状态变化。
animation:用于创建复杂的动画效果,可以定义多个关键帧,支持循环和反向播放。
- 触发方式:通过
@keyframes
定义动画帧,使用animation
属性触发。 - 执行方式:可以循环播放,支持多种动画效果和时间函数。
- 触发方式:通过
transition 属性
transition-property
:指定要应用过渡效果的 CSS 属性。transition-duration
:指定过渡效果的持续时间。transition-timing-function
:指定过渡效果的速度曲线,控制动画的加速和减速。transition-delay
:指定过渡效果开始前的延迟时间。transition
:可以将上述属性合并为一个简写属性。
animation 属性
animation-name
:指定动画的名称,通过@keyframes
定义。animation-duration
:指定动画的持续时间。animation-timing-function
:指定动画的速度曲线,控制动画的加速和减速。animation-delay
:指定动画开始前的延迟时间。animation-iteration-count
:指定动画的重复次数,可以是整数或infinite
(无限次)。animation-direction
:指定动画的播放方向,可以是normal
(正常播放)、reverse
(反向播放)、alternate
(交替播放)等。animation-fill-mode
:指定动画结束后元素的状态,可以是none
(不保持状态)、forwards
(保持最后一帧状态)、backwards
(保持第一帧状态)等。animation-play-state
:控制动画的播放状态,可以是running
(运行)或paused
(暂停)。animation
:可以将上述属性合并为一个简写属性。
animation 的组合规则:
animation: [animation-name] [animation-duration] [animation-timing-function] [animation-delay] [animation-iteration-count] [animation-direction] [animation-fill-mode] [animation-play-state];
简单无限循环的动画示例:
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
.spinner {
animation: spin 2s linear infinite;
}
transform 2D 与 3D
transform
属性用于在二维或三维空间中对元素进行变换。- 二维变换包括平移、缩放、旋转和倾斜。
- 常用函数有:
translate
(平移)、scale
(缩放)、rotate
(旋转)、skew
(倾斜)。 - 核心属性包括
transform-origin
(变换原点)。
- 常用函数有:
- 三维变换包括在三维空间中进行平移、缩放、旋转和透视效果。
- 常用函数有:
translate3d
(三维平移)、scale3d
(三维缩放)、rotateX
、rotateY
、rotateZ
(三维旋转)。 - 核心属性包括
transform-origin
(变换原点)、perspective
(透视效果)、perspective-origin
(透视原点)、backface-visibility
(背面可见性)。
- 常用函数有:
卡片翻转示例:
.card {
perspective: 1000px; /* 设置透视效果 */
}
.card-inner {
transform-style: preserve-3d; /* 保持3D效果 */
transition: transform 0.6s; /* 设置过渡效果 */
}
.card:hover .card-inner {
transform: rotateY(180deg); /* 鼠标悬停时翻转 */
}
.card-front, .card-back {
backface-visibility: hidden; /* 隐藏背面 */
}
动画性能原理
浏览器渲染逻辑
- 浏览器解析 HTML 和 CSS,构建 DOM 树和 CSSOM 树。
- 合并 DOM 和 CSSOM 树,生成渲染树。
- 计算布局,确定每个元素的位置和大小。(回流)
- 绘制每个元素到屏幕上。(重绘)
- 合成:将需要动画的元素提升到独立层,进行 GPU 加速渲染,最终渲染到屏幕上。
性能瓶颈
- 回流(Reflow):当元素的几何属性(如宽度、高度、位置等)发生变化时,浏览器需要重新计算布局,导致性能下降。回流一定会导致重绘。
- 重绘(Repaint):当元素的样式(如颜色、背景等)发生变化时,浏览器需要重新绘制元素,但不需要重新计算布局。
- 合成(Composite):当元素需要进行 GPU 加速渲染时,浏览器需要将元素提升到独立层,增加了额外的开销。(性能最优,只需要CGP处理)
优化技巧
- 使用 GPU 加速:通过
transform
和opacity
属性进行动画,可以利用 GPU 加速,减少 CPU 的负担。 - 减少回流和重绘:尽量避免频繁修改元素的几何属性,使用
class
切换样式而不是直接修改样式。 - 提升到独立层(合成层):使用
will-change
属性或transform: translateZ(0)
提升元素到独立层,减少合成开销。(但是要注意每个合成层都额外消耗内存) - 减少动画/减少计算/批量操作:
- 使用
requestAnimationFrame
批量处理动画效果,避免在每一帧都进行 DOM 操作。 - 使用
IntersectionObserver
监听元素是否在可视区域内,避免对不可见元素进行动画处理。 - 对于复杂的计算任务,可以使用 Web Worker 在后台线程中执行,避免阻塞主线程。
- 使用
大量动画效果优化
不可见元素去掉动画效果
使用intersectionObserver API 监听元素是否在可视区域内,如果不在可视区域内,则去掉动画效果。
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('animate');
} else {
entry.target.classList.remove('animate');
}
});
});
document.querySelectorAll('.animate-on-scroll').forEach(element => {
observer.observe(element);
});
批量操作
使用 requestAnimationFrame
批量处理动画效果,避免在每一帧都进行 DOM 操作。
requestAnimationFrame是浏览器用于定时循环操作的一个接口,它会把每一帧中的所有 DOM 操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率(通常为每秒 60 帧)。这意味着可以将多个 DOM 操作合并在一个requestAnimationFrame的回调函数中执行,而不是在每一帧都单独进行 DOM 操作,从而减少性能开销。
function addElementsBatch() {
const container = document.getElementById('container');
const fragment = document.createDocumentFragment();
for (let i = 0; i < 10; i++) {
const div = document.createElement('div');
div.style.width = '50px';
div.style.height = '50px';
div.style.backgroundColor = `rgb(${Math.random() * 255}, ${Math.random() * 255}, ${Math.random() * 255})`;
fragment.appendChild(div);
}
requestAnimationFrame(() => {
container.appendChild(fragment);
});
}
addElementsBatch();
webWorker
Web Workers 允许在后台线程中运行 JavaScript 代码,可以用于处理复杂的计算任务,而不会阻塞主线程。对于动画效果,可以将一些计算密集型的任务放在 Web Worker 中执行,从而提高性能。
// main.js
const worker = new Worker('animation-worker.js');
worker.onmessage = (e) => {
elements.forEach((el, i) => {
el.style.transform = `translate(${e.data.x[i]}px, ${e.data.y[i]}px)`;
});
};
// animation-worker.js
self.onmessage = () => {
// 复杂计算逻辑
const positions = calculatePositions();
self.postMessage(positions);
};
性能调试
Chrome DevTools
- Performance 面板:可以录制页面的性能数据,查看回流、重绘、合成等信息,帮助分析动画性能瓶颈。
- 看FPS 图表:是否有明显掉帧(绿色区域断裂);
- 看Main 线程:是否有长任务(红色块)与动画帧重叠;
- 看Layers 面板:是否有图层过多(Layer 爆炸)。
- **Lighthouse **:运行 Lighthouse 的 “Performance” 审计,勾选 “动画” 相关选项,会生成 “动画性能评分” 及优化建议(如 “避免使用 layout 触发类动画”)
线上监控手段
线上主要关注动画执行时候的帧率、长任务、渲染阶段的耗时等指标,判断动画是否卡顿。
1. FPS
通过requestAnimationFrame(动画帧回调)计算相邻帧的时间间隔,推导 FPS:
为什么用requestAnimationFrame而不是setInterval? requestAnimationFrame由浏览器渲染线程触发,与屏幕刷新同步,能更准确反映实际帧率;setInterval受主线程阻塞影响,计时不准
let lastTime = 0;
let frameCount = 0;
let fps = 0;
let isAnimating = false; // 标记动画是否在执行
// 动画开始时启动监控
function startAnimationMonitor() {
isAnimating = true;
lastTime = performance.now();
frameCount = 0;
// 用requestAnimationFrame监听每帧
function checkFPS(timestamp) {
if (!isAnimating) return;
frameCount++;
// 每1000ms计算一次FPS(避免高频计算消耗性能)
if (timestamp - lastTime >= 1000) {
fps = Math.round((frameCount * 1000) / (timestamp - lastTime));
// 上报FPS(如低于30则标记为卡顿)
reportPerformance({
type: 'animation_fps',
value: fps,
isStutter: fps < 30,
animationName: 'slideIn' // 动画名称(便于定位)
});
// 重置计数
frameCount = 0;
lastTime = timestamp;
}
requestAnimationFrame(checkFPS);
}
requestAnimationFrame(checkFPS);
}
// 动画结束时停止监控
function stopAnimationMonitor() {
isAnimating = false;
}
2. 监听长任务
使用PerformanceObserver
监听长任务(超过50ms的任务):若动画执行时频繁出现 > 100ms 的长任务,基本可判定动画卡顿是由此导致(如动画同时执行了复杂的 JS 计算)。
动画执行时,主线程若被超过 50ms 的任务(如复杂 JS 计算、频繁 DOM 操作)阻塞,会直接导致掉帧(因为requestAnimationFrame/CSS 动画依赖主线程调度)。
// 监听长任务(>50ms的任务)
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
// 若长任务发生在动画执行期间,则记录
if (isAnimating) {
reportPerformance({
type: 'animation_long_task',
duration: entry.duration, // 任务耗时(ms)
startTime: entry.startTime, // 任务开始时间
animationName: 'slideIn'
});
}
});
});
// 监听所有长任务
observer.observe({ entryTypes: ['longtask'] });
3. 监听渲染阶段的时长
使用 PerformanceObserver
监听渲染阶段的时长,判断是否存在过长的渲染时间(如超过16.67ms),从而影响动画流畅度。
- 若layout耗时高:说明动画使用了width/top等触发重排(回流)的属性(应改用transform);
- 若paint耗时高:说明动画涉及大面积重绘(如box-shadow动画,应减少绘制区域)。
- 若composite耗时高:说明合成层过多或合成层合成耗时过高(应减少合成层数量)。
// 监听渲染相关的性能条目
const perfObserver = new PerformanceObserver((list) => {
list.getEntries().forEach(entry => {
if (isAnimating) {
// 布局阶段(Layout)
if (entry.entryType === 'layout') {
reportPerformance({
type: 'animation_layout',
duration: entry.duration, // 布局耗时
animationName: 'slideIn'
});
}
// 绘制阶段(Paint)
if (entry.entryType === 'paint') {
// 如“首次绘制”“首次内容绘制”等,若在动画中触发,可能耗时过高
reportPerformance({
type: 'animation_paint',
duration: entry.duration,
animationName: 'slideIn'
});
}
}
});
});
// 监听布局、绘制、合成等条目
perfObserver.observe({
entryTypes: ['layout', 'paint', 'composite']
});
监控GPU性能
浏览器没有直接获取 GPU 使用率的 API,但可通过 “图层数量” 和 “合成耗时” 间接判断。即:若使用PerformanceObserver
监听 composite耗时若高:说明合成层过多或合成层合成耗时过高(应减少合成层数量)。
监控要点
轻量化上报策略
- 采样上报:线上用户量大时,无需采集 100% 数据,可按 10% 比例采样(减少服务器压力);
- 阈值触发:仅上报异常数据(如 FPS<30、长任务> 200ms),正常数据不上报;
- 批量上报:将多个指标合并为一个请求,减少网络开销。
关联性能数据和动画数据(补充维度)
- 需在动画的 “开始、执行中、结束” 三个阶段埋点,将性能数据与动画行为绑定:用于精准定位 “哪个动画”“在什么场景下” 出现性能问题(如用户反馈 “弹窗动画卡顿”,可通过该动画的埋点数据验证)。
- 上报时候要带上环境信息,如设备类型,网络类型。
可视化分析/告警
- 通过数据可视化工具 展示动画性能指标,便于团队分析和优化。
- 设置告警规则(如 FPS < 30 或 长任务 > 200ms),及时发现和处理性能问题。
线上问题复现与线下验证 略
监控 FPS 有哪些方案
使用 requestAnimationFrame:通过
requestAnimationFrame
计算每帧的时间间隔,推导 FPS。- 优点:简单易用,直接获取每帧渲染时间。
- 缺点:只能在动画执行时监控,且无法区分:动画渲染 和 主线程 哪种情况导致的FPS下降。
- 代码:略(查看上面)
使用 Performance API:通过
PerformanceObserver
监听帧率frame
相关的性能条目。- 优点:可以获取更详细的性能数据,如布局、绘制等。
- 缺点:兼容性差,不可见不记录;
- 代码
js// 检查浏览器兼容性 if (PerformanceObserver.supportedEntryTypes.includes('frame')) { const observer = new PerformanceObserver((list) => { const entries = list.getEntries(); for (const entry of entries) { // entry.duration 是帧持续时间(毫秒) const currentFPS = Math.round(1000 / entry.duration); console.log(`Current FPS: ${currentFPS}`); // 可在此上报 FPS 数据 reportFPS(currentFPS); } }); // 开始监听渲染帧 observer.observe({ entryTypes: ['frame'] }); } else { console.log('Your browser does not support frame performance observer'); // 降级方案:使用 requestAnimationFrame }
Web Worker 方式:定时发生事件,监控主线程阻塞导致的FPS减少
- 可区分 “渲染引擎问题” 和 “主线程阻塞问题”; 若下面计算有差异,可以区分是否是主线程卡顿:
- 在web Worker中使用
requestPostAnimationFrame
(实验性api,渲染完成后推送到主线程和webWorker,不同于requestAnimationFrame
是渲染前推送到主线程) 计算FPS,不依赖主线程,但是依赖渲染进程。 - 在web Worker中使用
setInterval
定时 postMessage 给主线程,在主线程中计算 FPS。
- 在web Worker中使用
- 可区分 “渲染引擎问题” 和 “主线程阻塞问题”; 若下面计算有差异,可以区分是否是主线程卡顿:
Performance.now() + 定时器
- 缺点:若主线程卡顿,会导致计算数据异常(无法区分渲染卡顿)
使用第三方库:如
stats.js
、fpsmeter
等库,可以快速集成 FPS 监控。