页面滚动时jQuery数字计数动画卡顿重复问题排查
问题:数字滚动动画在页面滚动时卡顿,JSFiddle中却正常
我在网站中实现了数字计数动画——当数字元素进入视口可见区域后,会从0向上滚动到目标数值。原本功能正常,但现在出现了奇怪的问题:用户手动滚动页面时,动画会暂停一秒,停止滚动后又重复播放,整体表现十分卡顿。但我把完全相同的代码放到JSFiddle中测试时,却没有出现这种卡顿现象。
复现代码
JavaScript
jQuery(function($) { $(function($, win) { $.fn.inViewport = function(cb) { return this.each(function(i, el) { function visPx() { var H = $(this).height(), r = el.getBoundingClientRect(), t = r.top, b = r.bottom; return cb.call(el, Math.max(0, t > 0 ? H - t : (b < H ? b : H))); } visPx(); $(win).on("resize scroll", visPx); }); }; }(jQuery, window)); $(".fig-number").inViewport(function(px) { // if px>0 (entered V.port) and // if prop initNumAnim flag is not yet set = Animate numbers if (px > 0 && !this.initNumAnim) { this.initNumAnim = true; // Set flag to true to prevent re-running the same animation $(this).prop('Counter', 0).animate({ Counter: $(this).text() }, { duration: 10000, step: function(now) { $(this).text(Math.ceil(now)); } }); } }); });
CSS
html, body { height: 100%; } .spacer { height: 100%; width: 100%; display: block; background: red; color: white; }
HTML
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script> <div class="spacer"> scroll down </div> <div class="number-box"> <h1 class="fig-number">1000</h1> <h1 class="fig-number">1500</h1> </div> <div class="spacer"> scroll down </div> <div class="number-box"> <h1 class="fig-number">2000</h1> <h1 class="fig-number">2500</h1> </div> <div class="spacer"> scroll down </div> <div class="number-box"> <h1 class="fig-number">3000</h1> <h1 class="fig-number">3500</h1> </div>
问题根源分析
咱们先理清楚为什么同一个代码在两个环境表现不一样:
- 滚动事件触发太频繁,没做防抖:原代码里
scroll事件直接绑了visPx函数,滚动时浏览器每秒会触发几十次这个函数,大量的计算直接占满主线程,jQuery的动画只能被迫暂停。JSFiddle环境干净,没有其他脚本抢资源,所以卡顿不明显;但你的网站肯定有其他插件、脚本或者复杂样式,双重负载下问题就暴露了。 - 视口检测计算冗余:每次触发都要计算元素的高度、位置,反复调用
getBoundingClientRect()也是个小性能杀手,积累起来就会拖慢动画。 - 网站环境的额外性能消耗:比如第三方广告、统计脚本、其他滚动监听效果,这些都会占用主线程,让你的数字动画“抢不到资源”,出现卡停重复的情况。
解决方案
1. 给滚动/resize事件加防抖
这是最关键的一步,减少函数触发频率,让浏览器喘口气:
jQuery(function($) { // 防抖函数:指定时间内只执行最后一次调用 function debounce(func, wait = 100) { let timeoutId; return function(...args) { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), wait); }; } $(function($, win) { $.fn.inViewport = function(cb) { return this.each(function(i, el) { function visPx() { const H = $(this).height(); const r = el.getBoundingClientRect(); const t = r.top; const b = r.bottom; return cb.call(el, Math.max(0, t > 0 ? H - t : (b < H ? b : H))); } visPx(); // 用防抖包装事件处理函数 const debouncedVisPx = debounce(visPx); $(win).on("resize scroll", debouncedVisPx); }); }; }(jQuery, window)); $(".fig-number").inViewport(function(px) { if (px > 0 && !this.initNumAnim) { this.initNumAnim = true; $(this).prop('Counter', 0).animate({ Counter: $(this).text() }, { duration: 10000, step: function(now) { $(this).text(Math.ceil(now)); } }); } }); });
2. 简化视口检测逻辑(可选)
如果你不需要精确计算可见像素,只是判断元素是否进入视口,可以简化检测逻辑,减少计算量:
function visPx() { const rect = el.getBoundingClientRect(); // 只要元素有部分进入视口就算可见 const isVisible = rect.top < window.innerHeight && rect.bottom > 0; return cb.call(el, isVisible ? 1 : 0); }
3. 排查网站的其他性能瓶颈
打开浏览器开发者工具的Performance面板,录制一次滚动操作,看看有没有其他脚本占用大量主线程时间:
- 第三方广告、统计插件
- 其他滚动监听事件
- 复杂的CSS动画/过渡
这些都是常见的性能杀手,找到后可以考虑优化或者延迟加载。
4. 用原生JS实现动画(可选)
如果jQuery的animate还是不够流畅,可以试试用requestAnimationFrame实现,它能更好地和浏览器重绘周期同步:
// 替换原有的jQuery animate调用 function animateNumber(element, target) { let current = 0; // 根据总时长计算每帧递增的值(16ms约为一帧的时间) const increment = target / (10000 / 16); function update() { current += increment; if (current >= target) { element.textContent = Math.ceil(target); return; } element.textContent = Math.ceil(current); requestAnimationFrame(update); } requestAnimationFrame(update); } // 在inViewport的回调里调用 $(".fig-number").inViewport(function(px) { if (px > 0 && !this.initNumAnim) { this.initNumAnim = true; animateNumber(this, parseInt($(this).text(), 10)); } });
总结
核心问题就是滚动事件触发过于频繁导致主线程阻塞,加上网站环境的额外性能负载,才出现了JSFiddle中没有的卡顿。先加防抖,再排查其他性能瓶颈,应该就能解决问题了。
内容的提问来源于stack exchange,提问作者Cray
相关产品推荐
相关产品推荐

