用requestAnimationFrame实现的滚动动画为什么偶尔会卡顿?

玉佩 Dev 阅读 39

在做页面导航平滑滚动时,我用requestAnimationFrame写了个滚动函数,但偶尔会出现动画卡顿的情况,特别是在低端设备上。代码逻辑是这样的:


function smoothScroll(target) {
  const step = () => {
    const diff = target - window.scrollY;
    if (Math.abs(diff) < 0.5) return;
    window.scrollBy(0, diff * 0.1);
    requestAnimationFrame(step);
  };
  requestAnimationFrame(step);
}

我尝试过把diff * 0.1改成更小的系数,也调整过raf的调用时机,但问题依旧。特别是当快速连续点击导航栏时,滚动会突然变慢甚至停顿几帧。这是不是跟浏览器渲染队列有关?有没有更好的优化方法?

我来解答 赞 11 收藏
二维码
手机扫码查看
2 条解答
雅涵
雅涵 Lv1
这个问题在WP里面也经常遇到,特别是在处理主题的平滑滚动时。你的实现方式有个常见问题:每次requestAnimationFrame调用都会创建新的回调,当快速连续触发时,多个动画会互相竞争。

试试这个改进方案,关键点是要用cancelAnimationFrame来清理之前的动画:

let scrollAnimationId = null;

function smoothScroll(target) {
if (scrollAnimationId) {
cancelAnimationFrame(scrollAnimationId);
}

const startPosition = window.scrollY;
const distance = target - startPosition;
const duration = 500; // 毫秒
let startTime = null;

const step = (timestamp) => {
if (!startTime) startTime = timestamp;
const progress = timestamp - startTime;
const percentage = Math.min(progress / duration, 1);
window.scrollTo(0, startPosition + distance * easeInOutCubic(percentage));

if (progress < duration) {
scrollAnimationId = requestAnimationFrame(step);
} else {
scrollAnimationId = null;
}
};

scrollAnimationId = requestAnimationFrame(step);
}

// 缓动函数能让滚动更顺滑
function easeInOutCubic(t) {
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
}


主要改进:
1. 增加了动画取消机制,避免多个动画叠加
2. 使用时间基准而不是帧数来控制动画
3. 加入缓动函数让滚动更自然
4. 固定动画时长而不是依赖滚动距离

这个方案我在20多个主题上用过,低端设备表现也不错。如果还有卡顿,可以试试降低duration值或者改用更简单的缓动函数。
点赞 1
2026-03-07 18:01
爱学习的之芳
你这个问题主要是因为快速连续触发滚动时,多个动画帧叠加导致的性能问题。每次点击导航栏都会启动一个新的动画循环,而之前的动画可能还没结束,这就造成了冲突。

我们可以通过加一个状态锁来解决这个问题,确保同一时间只有一个滚动动画在运行。另外,你的算法里用的是相对滚动 window.scrollBy,这确实容易出现累积误差,建议改成绝对定位的方式。

给你一个改进版本:

let isScrolling = false;

function smoothScroll(target) {
if (isScrolling) return;
isScrolling = true;

const start = window.scrollY;
const distance = target - start;
const duration = 500; // 500ms
const startTime = performance.now();

const step = (currentTime) => {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / duration, 1);
const ease = progress * (2 - progress); // 简单的缓动公式

window.scrollTo(0, start + distance * ease);

if (progress < 1) {
requestAnimationFrame(step);
} else {
isScrolling = false;
}
};

requestAnimationFrame(step);
}


这里做了几个关键改动:首先是加了 isScrolling 锁,防止重复触发;其次是用了绝对定位 window.scrollTo,避免累积误差;最后加入了简单的缓动效果,让动画更平滑。

要注意的是,在实际项目中,你还得考虑一些边界情况,比如用户手动滚动时应该中断动画,或者目标元素被删除的情况要做异常处理。这些都是潜在的安全隐患点,防止因为异常导致页面崩溃。

对了,如果你发现低端设备上还是不够流畅,可以考虑把 duration 时间缩短到300ms左右,这样虽然动画快了点,但能显著提升响应速度。性能和体验有时候就是个权衡的过程。
点赞 6
2026-02-16 06:06