You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Marquee Slider 数次循环滚动后出现卡顿问题排查

Marquee跑马灯组件卡顿问题修复

复现问题汇总

  • 组件运行数次滚动循环后出现明显卡顿掉帧
  • 大屏设备卡顿程度远高于小屏设备,未实现稳定100%视口宽度铺满
  • 现有通过空li控制文本间距的排布效果符合预期,无需调整布局逻辑
  • 同类型历史组件未出现同类问题

根因分析

代码存在5个直接导致卡顿的逻辑错误:

  1. 全局变量泄漏:_clone函数内循环变量i未声明,直接挂载到window全局,随运行时间持续污染内存,循环次数越多(大屏下clone节点数更多)泄漏越严重
  2. DOM操作逻辑错误:_clone函数内选择器写法错误,每次循环都全局查找父容器下所有ul重复清空、重复插入节点,导致DOM节点数量随clone调用指数级增长,大屏下DOM膨胀速度更快
  3. 动画无休眠逻辑:requestAnimationFrame递归未加暂停/页面隐藏判断,即使组件暂停、页面切到后台,动画仍在后台持续执行无效计算
  4. 宽度计算时机错误:单组内容宽度_marqueeWidth在节点clone完成前就提前计算,导致滚动重置位置偏移,出现肉眼可见的跳帧
  5. 未开启硬件加速:滚动容器没有单独提升为合成层,大屏下transform重绘区域更大,容易触发主线程阻塞导致掉帧

修复后代码

修复后的JS逻辑

(function marquee() {
    var marqueeWrapper = $('.js-marquee');
    var SLIDESPEED = 1000; // 数值越小速度越快

    marqueeWrapper.each(function (index, element) {
        var _marqueeWrapper = $(element);
        var _marqueeTracks = _marqueeWrapper.find('>ul');
        // 缓存初始slide节点,后续clone基于原始节点操作
        var _marqueeSlides = _marqueeTracks.find('>li').detach();
        var _marqueeWidth = 0;
        var shifted = _marqueeWrapper.attr('data-marquee-shift') || false;
        var SPEED = (_marqueeWrapper.attr('data-marquee-speed') * _marqueeSlides.length) || (SLIDESPEED * _marqueeSlides.length); 
        var posX = 0;
        var tempSteps = 0;
        var steps = 0;
        var rafId = null;

        function _clone() {
            var times = Math.ceil(_marqueeWrapper.outerWidth(true) / _marqueeWidth) + 1;
            _marqueeTracks.each(function () {
                var track = $(this);
                track.empty();
                // 修复:声明循环变量i,避免全局泄漏
                for (var i = 1; i <= times; i++) {
                    _marqueeSlides.clone().appendTo(track);
                }
            })
            // clone完成后再计算单组内容准确宽度
            _marqueeWidth = parseFloat(_marqueeSlides.last().position().left + _marqueeSlides.last().outerWidth(true));
            // 基于宽度和速度计算每帧位移,适配不同刷新率设备
            steps = _marqueeWidth / (SPEED * 60 / 1000);
        }

        function _animated() {
            posX += -steps;
            _marqueeTracks.css({
                transform: 'translate3d(' + posX + 'px, 0, 0)'
            });
            if (Math.abs(posX) >= _marqueeWidth) {
                posX = 0;
            }
            // 暂停状态下停止递归,避免无效计算
            if (steps > 0 && !document.hidden) {
                rafId = window.requestAnimationFrame(_animated);
            }
        }

        function _pause() {
            tempSteps = steps;
            steps = 0;
            if (rafId) {
                window.cancelAnimationFrame(rafId);
            }
        }

        function _resume() {
            steps = tempSteps;
            if (!rafId) {
                _animated();
            }
        }

        function _shiftPosition() {
            if(shifted) posX = -(_marqueeSlides.first().outerWidth(true)) / 2 ;
        }
        
        function _registerEvents() {
            _marqueeTracks.on('mouseenter', _pause);
            _marqueeTracks.on('mouseleave', _resume);
            $(window).on('resize', debounce(_clone, 300))
            // 页面切回时自动恢复动画
            document.addEventListener('visibilitychange', function() {
                if (!document.hidden && steps > 0) {
                    _animated();
                }
            })
        }

        function init() {
            _shiftPosition()
            // 先插入一组原始节点用于计算宽度
            _marqueeTracks.append(_marqueeSlides.clone());
            _marqueeWidth = parseFloat(_marqueeSlides.last().position().left + _marqueeSlides.last().outerWidth(true));
            _clone();
            _animated();
            _registerEvents();
        }
            
        function debounce(func, wait, immediate) {
            var timeout;
            return function () {
                var context = this,
                    args = arguments;
                var later = function () {
                    timeout = null;
                    if (!immediate) func.apply(context, args);
                };
                var callNow = immediate && !timeout;
                clearTimeout(timeout);
                timeout = setTimeout(later, wait);
                if (callNow) func.apply(context, args);
            };
        }

        init();
    })
})();

补充CSS样式(解决100%视口铺满+硬件加速问题)

.marquee {
  background: black;
  width: 100%;
  overflow: hidden;
}
.marquee__track {
  display: flex;
  padding: 0;
  margin: 0;
  list-style: none;
  /* 新增:强制内容不压缩,开启硬件加速 */
  width: max-content;
  will-change: transform;
}
.marquee__item {
  display: flex;
  justify-content: center;
  align-items: center;
  color: white;
  height: 80px;
  margin-right: 10px;
  padding-bottom: 10px; 
  /* 新增:禁止flex项收缩,避免宽度计算错误 */
  flex-shrink: 0;
}
.marquee__item_vegan {
 flex: 0 0 120px; 
 font-size: 50px;
  font-family: Gothic821;
}
.marquee__item_gluten {
  flex: 0 0 160px;
  font-size: 50px;
  font-family: Gothic821;
}
.marquee__item_natural {
  flex: 0 0 130px;
  font-size: 50px;
  font-family: Gothic821;
}
.marquee__item_empty {
  flex: 0 0 180px;
}
.marquee__item_small {
  flex: 0 0 80px;
}
.marquee__item_icon {
  width: 50px; 
}

HTML结构无需调整,原有空li间距逻辑完全保留。


内容的提问来源于stack exchange,提问作者Maxswift

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.31 00:51:24