懒渲染技术在前端性能优化中的实战应用与踩坑总结

令狐红爱 优化 阅读 1,592
赞 17 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

最近做的那个电商项目,商品列表页面展示2000+商品,页面卡得一批,滚动条拖动都明显卡顿。一开始就是简单的React组件渲染,结果用户一滚动页面就卡死几秒,产品经理天天来催我优化。

懒渲染技术在前端性能优化中的实战应用与踩坑总结

项目组讨论了几个方案:虚拟滚动、分页加载、懒渲染。考虑到用户习惯是一次性看到所有商品(虽然数据量大),最终选择了懒渲染。主要是想让用户体验好一点,同时避免频繁的网络请求。

说实话开始没想到这个方案有多复杂,以为就是简单的intersection observer监听可视区域,后面发现事情没那么简单。

基本实现思路

懒渲染的核心就是只渲染当前可视区域内的元素,加上上下预加载一点点缓冲区域。原理简单,实现起来各种边界情况需要考虑。

import React, { useState, useRef, useCallback } from 'react';

const LazyRenderList = ({ data, itemHeight = 80, buffer = 5 }) => {
  const containerRef = useRef(null);
  const [visibleRange, setVisibleRange] = useState({ start: 0, end: 20 });

  const calculateVisibleRange = useCallback(() => {
    if (!containerRef.current) return;
    
    const container = containerRef.current;
    const scrollTop = container.scrollTop;
    const containerHeight = container.clientHeight;
    
    const startIndex = Math.floor(scrollTop / itemHeight) - buffer;
    const endIndex = Math.ceil((scrollTop + containerHeight) / itemHeight) + buffer;
    
    setVisibleRange({
      start: Math.max(0, startIndex),
      end: Math.min(data.length, endIndex)
    });
  }, [data.length, itemHeight, buffer]);

  // 监听滚动事件
  const handleScroll = useCallback(() => {
    calculateVisibleRange();
  }, [calculateVisibleRange]);

  return (
    <div 
      ref={containerRef}
      className="lazy-container"
      onScroll={handleScroll}
      style={{ height: '500px', overflowY: 'auto', position: 'relative' }}
    >
      <div 
        style={{ 
          height: ${data.length * itemHeight}px,
          position: 'relative'
        }}
      >
        {data.slice(visibleRange.start, visibleRange.end).map((item, index) => (
          <div
            key={item.id}
            className="list-item"
            style={{
              position: 'absolute',
              top: ${(visibleRange.start + index) * itemHeight}px,
              height: ${itemHeight}px,
              width: '100%'
            }}
          >
            {/* 渲染具体商品 */}
          </div>
        ))}
      </div>
    </div>
  );
};

最大的坑:滚动性能问题

第一版实现后发现问题很大,滚动过程中还是卡顿。主要是因为scroll事件触发太频繁,每次滚动都在计算可见区域范围,然后重新render。浏览器根本扛不住。

折腾了半天发现需要节流,但是普通的节流函数会导致滚动不流畅。后来用了requestAnimationFrame来优化:

const throttleScrollHandler = useCallback(() => {
  let ticking = false;
  
  return () => {
    if (!ticking) {
      requestAnimationFrame(() => {
        calculateVisibleRange();
        ticking = false;
      });
      ticking = true;
    }
  };
}, [calculateVisibleRange]);

// 使用
const throttledScroll = throttleScrollHandler();

return (
  <div 
    ref={containerRef}
    onScroll={throttledScroll}
  >
    {/* 其他内容 */}
  </div>
);

这样确实好了不少,但是还有个小问题:当快速滚动到列表底部时,会出现短暂的空白区域。原因是可视区域变化太快,计算跟不上。这个问题后来通过增加buffer值缓解了,从5个增加到10个。

内存泄漏问题差点翻车

测试环境发现内存一直在涨,滚动时间长了页面直接卡死。查了半天发现是event listener没有及时清理。之前用的是原生的addEventListener,组件卸载时忘了removeEventListener。

改成useEffect的返回函数来清理:

useEffect(() => {
  const container = containerRef.current;
  if (!container) return;

  const handleContainerScroll = throttledScroll;
  
  container.addEventListener('scroll', handleContainerScroll, { passive: true });
  
  return () => {
    container.removeEventListener('scroll', handleContainerScroll);
  };
}, [throttledScroll]);

这里passive: true也很重要,可以让浏览器提前知道这个事件处理器不会调用preventDefault,提升滚动性能。这个细节我第一次做移动端优化时踩过坑,这次想起来加了。

不同设备兼容性的处理

开发阶段是在Chrome调试的,到了Safari和移动端Android上发现行为不太一致。特别是Safari上,某些情况下IntersectionObserver的表现跟预期不一样。

后来统一使用onscroll事件监听,放弃了一部分IntersectionObserver的优化。虽然性能稍微差一点点,但兼容性更好,代码也更可控。

移动端的touchmove事件也需要单独处理,因为移动端的滚动机制跟PC端不一样。这里需要注意preventDefault的问题,不然可能影响页面的正常手势操作。

数据更新时的边界处理

项目中商品数据是动态更新的,比如搜索筛选会改变列表数据。这时候如果当前可视区域内的商品被删除了,会出现位置错乱的问题。

解决方案是在数据变更时重新计算整个列表的高度,并重置可视区域范围:

useEffect(() => {
  // 数据发生变化时重置可视区域
  setVisibleRange(prev => ({
    start: 0,
    end: Math.min(data.length, prev.end) // 保持end不超过新数据长度
  }));
}, [data.length]);

这个逻辑看起来简单,但是实际处理时需要考虑各种情况:新增数据、删除数据、排序变化等等。每个场景的处理方式都不太一样。

最终效果和剩余问题

优化完成后,页面滚动确实流畅了很多。2000+商品列表滚动时FPS基本能保持在50以上,用户体验提升了至少3倍。

不过还是有个小瑕疵:快速滚动到底部时偶尔会有轻微的闪烁,这是因为数据渲染和位置计算的时间差造成的。理论上可以通过预渲染更多元素来解决,但是会消耗更多内存。权衡之后觉得这个小问题可以接受。

另外就是首屏渲染时间比之前略长,因为需要额外计算可视区域范围。不过这个影响不大,用户感知不明显。

整体来说这个懒渲染方案达到了预期目标:页面响应快了,内存占用降了,滚动体验也好了。虽然实现过程有点曲折,但结果还算满意。

回过头来看

这次懒渲染的实现让我对性能优化有了更深的理解。有时候看似简单的技术方案,实际落地时会遇到各种意想不到的问题。

特别是一些看似微不足道的细节,比如节流函数的实现方式、事件监听器的清理、不同浏览器的兼容性处理,这些往往决定了最终的用户体验。

现在回头看,如果项目一开始就规划好性能优化策略,可能能避免一些后期的重构工作。不过这也算是积累经验了吧,下次遇到类似需求就有现成的轮子可以用。

以上是我这次懒渲染实现的完整踩坑过程,包括核心代码实现和遇到的各种问题。虽然方案不是完美的,但在实际项目中效果不错。有更好实现方式的朋友欢迎交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论