Marquee Slider 数次循环滚动后出现卡顿问题排查
Marquee跑马灯组件卡顿问题修复
复现问题汇总
- 组件运行数次滚动循环后出现明显卡顿掉帧
- 大屏设备卡顿程度远高于小屏设备,未实现稳定100%视口宽度铺满
- 现有通过空li控制文本间距的排布效果符合预期,无需调整布局逻辑
- 同类型历史组件未出现同类问题
根因分析
代码存在5个直接导致卡顿的逻辑错误:
- 全局变量泄漏:
_clone函数内循环变量i未声明,直接挂载到window全局,随运行时间持续污染内存,循环次数越多(大屏下clone节点数更多)泄漏越严重 - DOM操作逻辑错误:
_clone函数内选择器写法错误,每次循环都全局查找父容器下所有ul重复清空、重复插入节点,导致DOM节点数量随clone调用指数级增长,大屏下DOM膨胀速度更快 - 动画无休眠逻辑:
requestAnimationFrame递归未加暂停/页面隐藏判断,即使组件暂停、页面切到后台,动画仍在后台持续执行无效计算 - 宽度计算时机错误:单组内容宽度
_marqueeWidth在节点clone完成前就提前计算,导致滚动重置位置偏移,出现肉眼可见的跳帧 - 未开启硬件加速:滚动容器没有单独提升为合成层,大屏下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
相关产品推荐
相关产品推荐

