当前位置: 首页 > news >正文

流畅如丝:利用requestAnimationFrame优化你的Web动画体验

requestAnimationFrame 是前端开发中用于优化动画性能的 API。它允许浏览器在下一次重绘之前执行指定的回调函数,通常用于实现平滑的动画效果。

1.作用

  1. 优化性能requestAnimationFrame 会根据浏览器的刷新率(通常是 60Hz,即每秒 60 帧)来调用回调函数,确保动画流畅且不浪费资源。

  2. 节省资源:当页面不可见或最小化时,浏览器会自动暂停 requestAnimationFrame 的执行,减少 CPU 和 GPU 的消耗。

  3. 避免丢帧:与 setTimeout 或 setInterval 相比,requestAnimationFrame 能更好地与浏览器的渲染周期同步,减少丢帧现象。

2.使用方法

  1. 基本用法

    function animate() {
        // 动画逻辑
        requestAnimationFrame(animate);
    }
    requestAnimationFrame(animate);
  2. 停止动画

    let animationId;
    function animate() {
        // 动画逻辑
        animationId = requestAnimationFrame(animate);
    }
    animate();
    
    // 停止动画
    cancelAnimationFrame(animationId);

优势对比

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>requestAnimationFrame vs setTimeout - 点击开始</title>
    <style>
        .box {
            width: 50px;
            height: 50px;
            position: relative;
            top: 0;
            left: 0;
            margin-bottom: 20px; /* 增加间距 */
        }
        #rafBox {
            background-color: red;
        }
        #timeoutBox {
            background-color: blue;
        }
        .label {
            font-family: Arial, sans-serif;
            margin-bottom: 10px;
        }
        #startButton {
            padding: 10px 20px;
            font-size: 16px;
            margin-bottom: 20px;
            cursor: pointer;
        }
    </style>
</head>
<body>
<button id="startButton">开始动画</button>

<div>
    <div class="label">requestAnimationFrame</div>
    <div id="rafBox" class="box"></div>
</div>
<div>
    <div class="label">setTimeout</div>
    <div id="timeoutBox" class="box"></div>
</div>

<script>
    // 增加动画复杂度:模拟一些计算任务
    function heavyTask() {
        let sum = 0;
        for (let i = 0; i < 1000000; i++) {
            sum += Math.random();
        }
    }

    // requestAnimationFrame 动画
    const rafBox = document.getElementById('rafBox');
    let rafPosition = 0;
    let rafStartTime;
    let rafFrameCount = 0;

    function rafAnimate() {
        heavyTask(); // 模拟复杂计算
        rafPosition += 2; // 每次移动 2px
        rafBox.style.left = rafPosition + 'px';

        rafFrameCount++;
        if (rafPosition < 600) { // 移动到 600px 时停止
            requestAnimationFrame(rafAnimate);
        } else {
            const rafEndTime = performance.now();
            console.log(`requestAnimationFrame 完成!用时:${(rafEndTime - rafStartTime).toFixed(2)}ms,帧数:${rafFrameCount}`);
        }
    }

    // setTimeout 动画
    const timeoutBox = document.getElementById('timeoutBox');
    let timeoutPosition = 0;
    let timeoutStartTime;
    let timeoutFrameCount = 0;

    function timeoutAnimate() {
        heavyTask(); // 模拟复杂计算
        timeoutPosition += 2; // 每次移动 2px
        timeoutBox.style.left = timeoutPosition + 'px';

        timeoutFrameCount++;
        if (timeoutPosition < 600) { // 移动到 600px 时停止
            setTimeout(timeoutAnimate, 16); // 模拟 60Hz 刷新率
        } else {
            const timeoutEndTime = performance.now();
            console.log(`setTimeout 完成!用时:${(timeoutEndTime - timeoutStartTime).toFixed(2)}ms,帧数:${timeoutFrameCount}`);
        }
    }

    // 点击按钮后启动动画
    const startButton = document.getElementById('startButton');
    startButton.addEventListener('click', () => {
        // 重置方块位置
        rafBox.style.left = '0px';
        timeoutBox.style.left = '0px';
        rafPosition = 0;
        timeoutPosition = 0;

        // 记录开始时间
        rafStartTime = performance.now();
        timeoutStartTime = performance.now();

        // 启动动画
        rafAnimate();
        timeoutAnimate();

        // 禁用按钮,防止重复点击
        startButton.disabled = true;
    });
</script>
</body>
</html>
  • 你会看到一个“开始动画”按钮,点击按钮后:

    • 红色方块:使用 requestAnimationFrame,动画更加流畅。

    • 蓝色方块:使用 setTimeout,动画可能会出现卡顿。

打开浏览器的控制台(按 F12),可以看到两者的完成时间和帧数对比

requestAnimationFrame 的兼容封装 

requestAnimationFrame 在不同浏览器中的兼容性确实存在差异,尤其是在一些旧版本的浏览器中(如 IE9 及以下)。为了确保代码的兼容性,我们可以封装一个通用的 requestAnimationFrame 方法,如果浏览器不支持 requestAnimationFrame,则自动降级为 setTimeout

// 兼容性封装
const requestAnimFrame = (function () {
    return (
        window.requestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        window.oRequestAnimationFrame ||
        window.msRequestAnimationFrame ||
        function (callback) {
            // 如果不支持 requestAnimationFrame,则使用 setTimeout 模拟
            return window.setTimeout(callback, 1000 / 60); // 模拟 60Hz 刷新率
        }
    );
})();

// 兼容性封装 cancelAnimationFrame
const cancelAnimFrame = (function () {
    return (
        window.cancelAnimationFrame ||
        window.webkitCancelAnimationFrame ||
        window.mozCancelAnimationFrame ||
        window.oCancelAnimationFrame ||
        window.msCancelAnimationFrame ||
        function (id) {
            // 如果不支持 cancelAnimationFrame,则使用 clearTimeout
            window.clearTimeout(id);
        }
    );
})();

3.使用方法

启动动画

let animationId;

function animate() {
    // 动画逻辑
    animationId = requestAnimFrame(animate);
}

animate(); // 启动动画

停止动画

cancelAnimFrame(animationId); // 停止动画

4.完整示例

以下是一个完整的示例,展示如何使用封装的 requestAnimFrame 方法,并且保留啦优势对比的蓝色方块,同时也增加了两个按钮,可以重复观看,以感受他们的差异!

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>requestAnimationFrame vs setTimeout 对比</title>
    <style>
        .box {
            width: 50px;
            height: 50px;
            position: relative;
            top: 0;
            left: 0;
            margin-bottom: 20px; /* 增加间距 */
        }
        #rafBox {
            background-color: red;
        }
        #timeoutBox {
            background-color: blue;
        }
        .label {
            font-family: Arial, sans-serif;
            margin-bottom: 10px;
        }
        button {
            padding: 10px 20px;
            font-size: 16px;
            margin-right: 10px;
            cursor: pointer;
        }
    </style>
</head>
<body>
<button id="startButton">开始动画</button>
<button id="resetButton">重新执行动画</button>

<div>
    <div class="label">requestAnimationFrame</div>
    <div id="rafBox" class="box"></div>
</div>
<div>
    <div class="label">setTimeout</div>
    <div id="timeoutBox" class="box"></div>
</div>

<script>
    // 兼容性封装
    const requestAnimFrame = (function () {
        return (
            window.requestAnimationFrame ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame ||
            window.oRequestAnimationFrame ||
            window.msRequestAnimationFrame ||
            function (callback) {
                // 如果不支持 requestAnimationFrame,则使用 setTimeout 模拟
                return window.setTimeout(callback, 1000 / 60); // 模拟 60Hz 刷新率
            }
        );
    })();

    const cancelAnimFrame = (function () {
        return (
            window.cancelAnimationFrame ||
            window.webkitCancelAnimationFrame ||
            window.mozCancelAnimationFrame ||
            window.oCancelAnimationFrame ||
            window.msCancelAnimationFrame ||
            function (id) {
                // 如果不支持 cancelAnimationFrame,则使用 clearTimeout
                window.clearTimeout(id);
            }
        );
    })();

    // requestAnimationFrame 动画
    const rafBox = document.getElementById('rafBox');
    let rafPosition = 0;
    let rafAnimationId;

    function rafAnimate() {
        rafPosition += 2; // 每次移动 2px
        rafBox.style.left = rafPosition + 'px';

        if (rafPosition < 600) { // 移动到 600px 时停止
            rafAnimationId = requestAnimFrame(rafAnimate);
        }
    }

    // setTimeout 动画
    const timeoutBox = document.getElementById('timeoutBox');
    let timeoutPosition = 0;
    let timeoutAnimationId;

    function timeoutAnimate() {
        timeoutPosition += 2; // 每次移动 2px
        timeoutBox.style.left = timeoutPosition + 'px';

        if (timeoutPosition < 600) { // 移动到 600px 时停止
            timeoutAnimationId = setTimeout(timeoutAnimate, 16); // 模拟 60Hz 刷新率
        }
    }

    // 点击按钮启动动画
    document.getElementById('startButton').addEventListener('click', () => {
        // 启动 requestAnimationFrame 动画
        rafAnimate();
        // 启动 setTimeout 动画
        timeoutAnimate();
    });

    // 点击按钮重新执行动画
    document.getElementById('resetButton').addEventListener('click', () => {
        // 停止当前动画
        cancelAnimFrame(rafAnimationId);
        clearTimeout(timeoutAnimationId);

        // 重置方块位置
        rafBox.style.left = '0px';
        timeoutBox.style.left = '0px';
        rafPosition = 0;
        timeoutPosition = 0;

        // 重新启动动画
        rafAnimate();
        timeoutAnimate();
    });
</script>
</body>
</html>

5.代码说明

  1. 兼容性封装

    • requestAnimFrame 和 cancelAnimFrame 封装了 requestAnimationFrame 和 cancelAnimationFrame 的兼容性逻辑。

  2. 动画逻辑

    • 红色方块:使用 requestAnimFrame,动画更加流畅。

    • 蓝色方块:使用 setTimeout,动画可能会出现卡顿。

  3. 按钮功能

    • 开始动画:点击后同时启动 requestAnimFrame 和 setTimeout 的动画。

    • 重新执行动画:点击后停止当前动画,重置方块位置,并重新启动动画。

6.效果展示

7.案例总结

通过这个示例,你可以直观地看到 requestAnimFrame 和 setTimeout 的差异:

  • requestAnimFrame:动画流畅,性能更优。

  • setTimeout:动画可能会出现卡顿。

  • 备注:网上随便下载的gif录屏软件,貌似这种差异不是很明显,建议自己复制代码查看,我实际看到的还是很明显的

 8.性能优化建议

  1. setTimeout 的性能问题

    • 丢帧setTimeout 的时间间隔是固定的(如 16ms 模拟 60Hz),但无法保证与浏览器的渲染周期同步,可能导致丢帧或卡顿。

    • 资源浪费:即使页面不可见或最小化,setTimeout 仍会继续运行,浪费 CPU 和 GPU 资源。

    • 精度问题setTimeout 的时间精度受系统负载影响,可能导致动画不流畅。

  2. requestAnimationFrame 的性能优势

    • 与浏览器渲染同步requestAnimationFrame 会在每次浏览器重绘前调用回调函数,确保动画流畅。

    • 节省资源:当页面不可见或最小化时,requestAnimationFrame 会自动暂停,减少资源消耗。

    • 高精度requestAnimationFrame 的时间戳精度更高,适合高性能动画。


9.注意事项

  1. 避免频繁操作 DOM

    • 在动画回调函数中,尽量减少对 DOM 的频繁操作(如修改样式或布局),因为 DOM 操作会触发重排(reflow)和重绘(repaint),影响性能。

    • 可以通过以下方式优化:

      • 使用 transform 和 opacity 代替 topleft 等属性,因为前者不会触发重排。

      • 使用 will-change 属性提示浏览器优化渲染。

  2. 避免阻塞主线程

    • 如果动画回调函数中有复杂的计算任务(如大量循环或递归),可能会导致主线程阻塞,影响动画流畅性。

    • 可以通过以下方式优化:

      • 将复杂计算任务放到 Web Worker 中执行。

      • 使用 requestIdleCallback 处理低优先级的任务。

  3. 处理动画停止

    • 使用 cancelAnimationFrame 或 clearTimeout 及时停止动画,避免不必要的资源消耗。

    • 在页面不可见时(如切换标签页),可以通过 Page Visibility API 检测页面状态,暂停动画。

  4. 兼容性问题

    • 虽然 requestAnimationFrame 在现代浏览器中支持良好,但在旧版本浏览器(如 IE9 及以下)中需要降级为 setTimeout

    • 使用兼容性封装(如前面的代码)可以解决这个问题。

  5. 动画帧率控制

    • requestAnimationFrame 的帧率通常是 60Hz,但如果动画逻辑过于复杂,可能会导致帧率下降。

    • 可以通过时间戳(performance.now())计算帧间隔,动态调整动画逻辑,确保帧率稳定。

  6. 内存泄漏

    • 如果动画回调函数中引用了外部变量或 DOM 元素,可能会导致内存泄漏。

    • 确保在动画停止时,清理不必要的引用。


10.优化建议

  1. 使用硬件加速

    • 通过 transform: translate3d(0, 0, 0) 或 will-change: transform 启用 GPU 加速,提升动画性能。

  2. 减少重排和重绘

    • 使用 transform 和 opacity 实现动画,避免修改 widthheighttopleft 等属性。

  3. 批量操作 DOM

    • 如果需要修改多个 DOM 元素的样式,可以使用 DocumentFragment 或 requestAnimationFrame 批量处理,减少重排次数。

  4. 使用 Web Worker

    • 将复杂的计算任务放到 Web Worker 中执行,避免阻塞主线程。

  5. 性能监控

    • 使用 performance.now() 或浏览器开发者工具(如 Chrome 的 Performance 面板)监控动画性能,找出性能瓶颈。


11.优化后的动画

以下是一个优化后的动画示例,结合了上述建议:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>三个方块的性能对比</title>
    <style>
        body {
            font-family: Arial, sans-serif;
            margin: 20px;
        }
        .animation-container {
            margin-bottom: 40px; /* 每个动画区块之间的间距 */
        }
        .label {
            font-size: 16px;
            margin-bottom: 10px; /* 文字与方块的间距 */
        }
        .box {
            width: 50px;
            height: 50px;
            position: relative;
            left: 0;
        }
        #optimizedBox {
            background-color: red;
            will-change: transform; /* 启用硬件加速 */
        }
        #unoptimizedRAFBox {
            background-color: green;
        }
        #unoptimizedTimeoutBox {
            background-color: blue;
        }
        button {
            padding: 10px 20px;
            font-size: 16px;
            margin-right: 10px;
            cursor: pointer;
        }
    </style>
</head>
<body>
    <button id="startButton">开始动画</button>
    <button id="resetButton">重新开始动画</button>

    <!-- 优化后的动画 -->
    <div class="animation-container">
        <div class="label">优化后的动画(requestAnimationFrame + transform)</div>
        <div id="optimizedBox" class="box"></div>
    </div>

    <!-- 未优化的 requestAnimationFrame 动画 -->
    <div class="animation-container">
        <div class="label">未优化的 requestAnimationFrame 动画(使用 left)</div>
        <div id="unoptimizedRAFBox" class="box"></div>
    </div>

    <!-- 未优化的 setTimeout 动画 -->
    <div class="animation-container">
        <div class="label">未优化的 setTimeout 动画(使用 left)</div>
        <div id="unoptimizedTimeoutBox" class="box"></div>
    </div>

    <script>
        // 兼容性封装
        const requestAnimFrame = (function () {
            return (
                window.requestAnimationFrame ||
                window.webkitRequestAnimationFrame ||
                window.mozRequestAnimationFrame ||
                window.oRequestAnimationFrame ||
                window.msRequestAnimationFrame ||
                function (callback) {
                    return window.setTimeout(callback, 1000 / 60);
                }
            );
        })();

        const cancelAnimFrame = (function () {
            return (
                window.cancelAnimationFrame ||
                window.webkitCancelAnimationFrame ||
                window.mozCancelAnimationFrame ||
                window.oCancelAnimationFrame ||
                window.msCancelAnimationFrame ||
                function (id) {
                    window.clearTimeout(id);
                }
            );
        })();

        // 优化后的动画(requestAnimationFrame + transform)
        const optimizedBox = document.getElementById('optimizedBox');
        let optimizedPosition = 0;
        let optimizedAnimationId;

        function optimizedAnimate() {
            optimizedPosition += 2; // 每次移动 2px
            optimizedBox.style.transform = `translateX(${optimizedPosition}px)`; // 使用 transform

            if (optimizedPosition < 600) { // 移动到 600px 时停止
                optimizedAnimationId = requestAnimFrame(optimizedAnimate);
            }
        }

        // 未优化的 requestAnimationFrame 动画(使用 left)
        const unoptimizedRAFBox = document.getElementById('unoptimizedRAFBox');
        let unoptimizedRAFPosition = 0;
        let unoptimizedRAFAnimationId;

        function unoptimizedRAFAnimate() {
            unoptimizedRAFPosition += 2; // 每次移动 2px
            unoptimizedRAFBox.style.left = unoptimizedRAFPosition + 'px'; // 使用 left

            if (unoptimizedRAFPosition < 600) { // 移动到 600px 时停止
                unoptimizedRAFAnimationId = requestAnimFrame(unoptimizedRAFAnimate);
            }
        }

        // 未优化的 setTimeout 动画(使用 left)
        const unoptimizedTimeoutBox = document.getElementById('unoptimizedTimeoutBox');
        let unoptimizedTimeoutPosition = 0;
        let unoptimizedTimeoutAnimationId;

        function unoptimizedTimeoutAnimate() {
            unoptimizedTimeoutPosition += 2; // 每次移动 2px
            unoptimizedTimeoutBox.style.left = unoptimizedTimeoutPosition + 'px'; // 使用 left

            if (unoptimizedTimeoutPosition < 600) { // 移动到 600px 时停止
                unoptimizedTimeoutAnimationId = setTimeout(unoptimizedTimeoutAnimate, 16); // 模拟 60Hz 刷新率
            }
        }

        // 点击按钮启动动画
        document.getElementById('startButton').addEventListener('click', () => {
            // 启动优化后的动画
            optimizedAnimate();
            // 启动未优化的 requestAnimationFrame 动画
            unoptimizedRAFAnimate();
            // 启动未优化的 setTimeout 动画
            unoptimizedTimeoutAnimate();
        });

        // 点击按钮重新开始动画
        document.getElementById('resetButton').addEventListener('click', () => {
            // 停止当前动画
            cancelAnimFrame(optimizedAnimationId);
            cancelAnimFrame(unoptimizedRAFAnimationId);
            clearTimeout(unoptimizedTimeoutAnimationId);

            // 重置方块位置
            optimizedBox.style.transform = 'translateX(0px)';
            unoptimizedRAFBox.style.left = '0px';
            unoptimizedTimeoutBox.style.left = '0px';
            optimizedPosition = 0;
            unoptimizedRAFPosition = 0;
            unoptimizedTimeoutPosition = 0;

            // 重新启动动画
            optimizedAnimate();
            unoptimizedRAFAnimate();
            unoptimizedTimeoutAnimate();
        });
    </script>
</body>
</html>

12.效果展示

csdn上gif大小不能超过5M,我这动图超过5M啦,在线压缩居然要收费,算了!

大家自己复制源码感受一下吧!不贴gif图啦,jpg的对付着看吧!!!

你可以清晰地看到三种实现方式的性能差异:

  1. 优化后的动画:使用 requestAnimationFrame 和 transform,性能最佳。

  2. 未优化的 requestAnimationFrame 动画:使用 left,性能稍差。

  3. 未优化的 setTimeout 动画:使用 left,性能最差。

13.总结

通过优化 DOM 操作、启用硬件加速、减少重排和重绘,可以显著提升动画性能。同时,注意兼容性和资源管理,确保动画在不同设备和浏览器中都能流畅运行。

 

相关文章:

  • 基于Web大学生创新服务平台(源码+lw+部署文档+讲解),源码可白嫖!
  • 摄影工作室预约管理系统基于Spring BootSSM
  • Sympy入门之微积分基本运算
  • 【中间件】Rabbit离线部署操作
  • windows单节点验证victoriametrics结合AlertManger实现告警推送webhook
  • 对接马来西亚、印度、韩国、越南等全球金融数据示例
  • 个人作品集模板!除了Figma还可以选择什么软件?
  • neo4j-如何让外部设备访问wsl中的neo4j
  • Python 类与对象概念全解析:从零到实战
  • Ubuntu上安装Docker
  • 统计哲学的频率学派和贝叶斯学派
  • Redis的大Key问题如何解决?
  • 基于单片机的农作物自动灌溉系统
  • sougou AI close
  • Milvus WeightedRanker 对比 RRF 重排机制
  • Linux信号的诞生与归宿:内核如何管理信号的生成、阻塞和递达?
  • Mysql-经典实战案例(10):如何用PT-Archiver完成大表的自动归档
  • 英语:基础知识
  • teaming技术
  • vulnhub靶场【billu系列】之billu_b0x2靶机
  • 第十一届世界雷达展开幕,尖端装备、“大国重器”集中亮相
  • 孟夏韵评《无序的学科》丨误读与重构的文化漂流
  • 试点首发进口消费品检验便利化措施,上海海关与上海商务委发文
  • 广西:坚决拥护党中央对蓝天立进行审查调查的决定
  • 俄媒:俄乌代表团抵达谈判会场
  • 政企共同发力:多地密集部署外贸企业抢抓90天政策窗口期