Skip to content

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];

简单无限循环的动画示例:

css
@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(三维缩放)、rotateXrotateYrotateZ(三维旋转)。
    • 核心属性包括 transform-origin(变换原点)、 perspective(透视效果)、 perspective-origin(透视原点)、 backface-visibility(背面可见性)。

卡片翻转示例:

css
.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; /* 隐藏背面 */
}

动画性能原理

浏览器渲染逻辑

  1. 浏览器解析 HTML 和 CSS,构建 DOM 树和 CSSOM 树。
  2. 合并 DOM 和 CSSOM 树,生成渲染树。
  3. 计算布局,确定每个元素的位置和大小。(回流)
  4. 绘制每个元素到屏幕上。(重绘)
  5. 合成:将需要动画的元素提升到独立层,进行 GPU 加速渲染,最终渲染到屏幕上。

性能瓶颈

  1. 回流(Reflow):当元素的几何属性(如宽度、高度、位置等)发生变化时,浏览器需要重新计算布局,导致性能下降。回流一定会导致重绘。
  2. 重绘(Repaint):当元素的样式(如颜色、背景等)发生变化时,浏览器需要重新绘制元素,但不需要重新计算布局。
  3. 合成(Composite):当元素需要进行 GPU 加速渲染时,浏览器需要将元素提升到独立层,增加了额外的开销。(性能最优,只需要CGP处理)

优化技巧

  1. 使用 GPU 加速:通过 transformopacity 属性进行动画,可以利用 GPU 加速,减少 CPU 的负担。
  2. 减少回流和重绘:尽量避免频繁修改元素的几何属性,使用 class 切换样式而不是直接修改样式。
  3. 提升到独立层(合成层):使用 will-change 属性或 transform: translateZ(0) 提升元素到独立层,减少合成开销。(但是要注意每个合成层都额外消耗内存)
  4. 减少动画/减少计算/批量操作
    • 使用 requestAnimationFrame 批量处理动画效果,避免在每一帧都进行 DOM 操作。
    • 使用 IntersectionObserver 监听元素是否在可视区域内,避免对不可见元素进行动画处理。
    • 对于复杂的计算任务,可以使用 Web Worker 在后台线程中执行,避免阻塞主线程。

大量动画效果优化

不可见元素去掉动画效果

使用intersectionObserver API 监听元素是否在可视区域内,如果不在可视区域内,则去掉动画效果。

javascript
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 操作,从而减少性能开销。

javascript
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 中执行,从而提高性能。

javascript
// 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受主线程阻塞影响,计时不准

js
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 动画依赖主线程调度)。

js
// 监听长任务(>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耗时高:说明合成层过多或合成层合成耗时过高(应减少合成层数量)。
js
// 监听渲染相关的性能条目
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耗时若高:说明合成层过多或合成层合成耗时过高(应减少合成层数量)。

监控要点

  1. 轻量化上报策略

    • 采样上报:线上用户量大时,无需采集 100% 数据,可按 10% 比例采样(减少服务器压力);
    • 阈值触发:仅上报异常数据(如 FPS<30、长任务> 200ms),正常数据不上报;
    • 批量上报:将多个指标合并为一个请求,减少网络开销。
  2. 关联性能数据和动画数据(补充维度)

    • 需在动画的 “开始、执行中、结束” 三个阶段埋点,将性能数据与动画行为绑定:用于精准定位 “哪个动画”“在什么场景下” 出现性能问题(如用户反馈 “弹窗动画卡顿”,可通过该动画的埋点数据验证)。
    • 上报时候要带上环境信息,如设备类型,网络类型。
  3. 可视化分析/告警

    • 通过数据可视化工具 展示动画性能指标,便于团队分析和优化。
    • 设置告警规则(如 FPS < 30 或 长任务 > 200ms),及时发现和处理性能问题。
  4. 线上问题复现与线下验证 略

监控 FPS 有哪些方案

  1. 使用 requestAnimationFrame:通过 requestAnimationFrame 计算每帧的时间间隔,推导 FPS。

    • 优点:简单易用,直接获取每帧渲染时间。
    • 缺点:只能在动画执行时监控,且无法区分:动画渲染 和 主线程 哪种情况导致的FPS下降。
    • 代码:略(查看上面)
  2. 使用 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
    }
  3. Web Worker 方式:定时发生事件,监控主线程阻塞导致的FPS减少

    • 可区分 “渲染引擎问题” 和 “主线程阻塞问题”; 若下面计算有差异,可以区分是否是主线程卡顿:
      • 在web Worker中使用requestPostAnimationFrame(实验性api,渲染完成后推送到主线程和webWorker,不同于requestAnimationFrame是渲染前推送到主线程) 计算FPS,不依赖主线程,但是依赖渲染进程。
      • 在web Worker中使用setInterval定时 postMessage 给主线程,在主线程中计算 FPS。
  4. Performance.now() + 定时器

    • 缺点:若主线程卡顿,会导致计算数据异常(无法区分渲染卡顿)
  5. 使用第三方库:如 stats.jsfpsmeter 等库,可以快速集成 FPS 监控。