美文网首页
性能飙升50%,react-virtualized-list如何

性能飙升50%,react-virtualized-list如何

作者: SailingBytes | 来源:发表于2024-06-16 10:03 被阅读0次

    引言:在处理大规模数据集渲染时,前端性能常常面临巨大的挑战。本文将探讨 react-virtualized-list 库如何通过虚拟化技术和 Intersection Observer,实现前端渲染性能飙升 50% 的突破,页面渲染速度提升 95% !🔥🚀

    image.png

    背景

    最近,公司监控系统出现了加载卡顿和白屏问题,需要一个能够处理大规模数据渲染的方案。由于核心需求是列表项数据需要动态更新和自动刷新,所以用到了 react-virtualized-list 库。这个过程相当曲折,具体业务需求细节后面我会详细写一篇文章,这里先介绍一下react-virtualized-list库的特性、适用场景、API和实现原理。

    希望对你有所帮助、有所借鉴。大家有什么疑问或者建议,欢迎在评论区一起讨论!

    什么是虚拟化?

    虚拟化技术,顾名思义,是一种通过仅渲染当前用户可见的数据项,而不是整个数据集,来优化性能的技术。这种技术在处理大量数据时尤为重要,因为它显著减少了 DOM 节点的数量,从而提高了性能。通过虚拟化,可以在用户滚动列表时动态加载和卸载元素,保持界面流畅。

    下面是react-virtualized-list在虚拟化方面做的处理:

    image.png

    我们来看看真实的 DOM 情况!

    image.png

    react-virtualized-list 简介

    react-virtualized-list 是一个专门用于显示大型数据集的高性能 React 组件库。它同时适用于 PC 端移动端,通过虚拟化技术实现了延迟加载和无限滚动功能,尤其是非常适合需要高效渲染和加载大量数据的应用场景,如聊天记录、商品列表等。

    此外,react-virtualized-list 库还提供了场景适用的效果展示和示例代码

    核心特性 🔥🔥

    1. 高性能:仅渲染当前视口内的元素,显著减少 DOM 节点数量。
    2. 延迟加载:动态加载数据,避免一次性加载大量数据带来的性能问题。
    3. 无限滚动:支持无限滚动,用户可以持续滚动查看更多内容。
    4. 自定义渲染:提供灵活的 API,允许开发者自定义列表项的渲染方式。
    5. 视口内刷新:支持自动刷新视口内的内容,确保数据的实时性。
    6. 支持 TS 和 JS:适用于 TypeScript 和 JavaScript 项目。

    安装

    可以通过 npm 或 yarn 轻松安装 react-virtualized-list

    npm install react-virtualized-list
    # 或者
    yarn add react-virtualized-list
    

    基本用法

    下面是一个简单的示例,展示了如何使用 react-virtualized-list 创建一个无限滚动的虚拟化列表:

    import React, { useState, useEffect } from 'react';
    import VirtualizedList from 'react-virtualized-list';
    import './style/common.css';
    
    const InfiniteScrollList = () => {
      const [items, setItems] = useState([]);
      const [hasMore, setHasMore] = useState(true);
    
      const loadMoreItems = () => {
        // 模拟 API 调用,延迟 1 秒加载新数据
        setTimeout(() => {
          const newItems = Array.from({ length: 20 }, (_, index) => ({
            id: items.length + index,
            text: `Item ${items.length + index}`
          }));
          setItems(prevItems => [...prevItems, ...newItems]);
          setHasMore(newItems.length > 0);
        }, 1000);
      };
    
      useEffect(() => {
        loadMoreItems();
      }, []);
    
      const renderItem = (item) => <div>{item.text}</div>;
    
      return (
          <div className='content'>
            <VirtualizedList
              listData={items}
              renderItem={renderItem}
              containerHeight='450px'
              itemClassName='item-class'
              onLoadMore={loadMoreItems}
              hasMore={hasMore}
              loader={<div>Loading...</div>}
              endMessage={<div>No more items</div>}
            />
          </div>
      );
    };
    
    export default InfiniteScrollList;
    
    /* ./style/common.css  */
    .content {
        width: 350px;
        padding: 16px;
        border: 1px solid red;
        margin-top: 10vh;
    }
    .item-class {
        height: 50px;
        border: 1px solid blue;
        margin: 0px 0 10px;
        padding: 10px;
        background-color: #f0f0f0;
    }
    

    通过 onLoadMorehasMore 属性实现无限滚动,在用户滚动到列表底部时自动加载更多数据。这种功能常见于滚动加载下页数据。

    image.png

    进阶用法

    动态加载数据

    为了进一步提高性能,可以使用动态加载技术,只在需要时加载数据。以下是一个示例,展示了如何结合 react-virtualized-list 和动态数据加载:

    import React, { useState, useEffect } from 'react';
    import VirtualizedList from 'react-virtualized-list';
    import './style/common.css';
    
    const fetchProductData = async (product) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve({ description: `Description for ${product.name}`, imageUrl: `https://via.placeholder.com/150?text=Product+${product.id}` });
        }, 500);
      });
    };
    
    const fetchProducts = async (page) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          const products = Array.from({ length: 10 }, (_, i) => ({
            id: page * 10 + i,
            name: `Product ${page * 10 + i}`
          }));
          resolve(products);
        }, 500);
      });
    };
    
    const DynamicInfiniteList = () => {
      const [products, setProducts] = useState([]);
      const [hasMore, setHasMore] = useState(true);
      const [page, setPage] = useState(0);
    
      const loadMoreProducts = async () => {
        const newProducts = await fetchProducts(page);
        setProducts(prevProducts => [...prevProducts, ...newProducts]);
        setPage(prevPage => prevPage + 1);
        if (newProducts.length < 10) setHasMore(false);
      };
    
      useEffect(() => {
        loadMoreProducts();
      }, []);
       
      return (
          <div className='content'>
            <VirtualizedList
              listData={products}
              renderItem={(product, data) => (
                <div>
                  <h2>{product.name}</h2>
                  <p>{data ? data.description : 'Loading...'}</p>
                  {data && <img src={data.imageUrl} alt={product.name} />}
                </div>
              )}
              itemClassName='item-class-dynamic'
              fetchItemData={fetchProductData}
              onLoadMore={loadMoreProducts}
              hasMore={hasMore}
              containerHeight='500px'
              loader='Loading more products...'
              endMessage='No more products'
            />
          </div>
      );
    };
    
    export default DynamicInfiniteList;
    
    /* ./style/common.css  */
    .content {
        width: 350px;
        padding: 16px;
        border: 1px solid red;
        margin-top: 10vh;
    }
    .item-class-dynamic {
        height: 300px;
        padding: 20px;
        border-bottom: 1px solid #eee;
    }
    

    注意:在上面代码中,我们使用 onLoadMore 模拟商品列表的滚动加载,并在 VirtualizedList 组件的 fetchItemData 实现了商品详情的动态加载。这对于大数据集下,后端无法一次性返回数据非常有利

    自定义渲染

    react-virtualized-list 还提供了自定义渲染功能,开发者可以根据具体需求定制列表项的渲染方式。以下是一个示例,展示了如何自定义列表项的样式和内容:

    import React from 'react';
    import VirtualizedList from 'react-virtualized-list';
    
    const data = Array.from({ length: 1000 }).map((_, index) => ({
      title: `Item ${index}`,
      index: index,
      description: `This is the description for item ${index}.`
    }));
    
    const ListItem = ({ item, style }) => (
      <div style={{ ...style, padding: '10px', borderBottom: '1px solid #ccc' }}>
        <h3>{item.title}</h3>
        <p>{item.description}</p>
      </div>
    );
    
    const itemStyle = {
        height: '100px',
        border: '1px solid blue',
        margin: '0px 0 10px',
        padding: '10px',
        backgroundColor: '#f0f0f0'
    };
    
    const MyVirtualizedList = () => (
      <div style={{width: '350px', padding: '16px', border: '1px solid red'}}>
        <VirtualizedList
            listData={data}
            itemStyle={itemStyle}
            renderItem={({ index, style }) => <ListItem item={data[index]} style={style} />}
            containerHeight='80vh'
        />
      </div>
    );
    
    export default MyVirtualizedList;
    

    此外,react-virtualized-list 还提供了其他的用法场景和相关 API,详情请见使用文档

    image.png

    实现原理(🔥核心重点,一定要了解)

    在构建大型 Web 应用时,经常会遇到需要展示大量数据的情况,比如电子商务平台的产品列表等。传统的渲染方式可能会面临性能问题,因为它们需要在页面上同时呈现大量 DOM 元素,导致页面加载缓慢、滚动卡顿等问题。

    为了解决这个问题,我们可以使用虚拟化列表来优化渲染过程。而 react-virtualized-list 库的核心在于通过虚拟化技术优化渲染过程。其主要原理包括以下几点:

    image.png

    1. 可视区域监测:利用Intersection Observer API

    在虚拟化列表的实现中,一个关键步骤是监测可视区域内的元素。传统的方法是通过监听滚动事件并计算每个元素的位置来实现,然而这种方式效率较低。

    // 获取需要监测可视性的元素
    const elements = document.querySelectorAll('.target-element');
    
    // 监听滚动事件
    window.addEventListener('scroll', () => {
        // 计算每个元素的位置
        elements.forEach(element => {
            const rect = element.getBoundingClientRect();
            if (
                rect.top >= 0 &&
                rect.left >= 0 &&
                rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
                rect.right <= (window.innerWidth || document.documentElement.clientWidth)
            ) {
                // 元素在可视区域内
                // 执行相应操作
                console.log(`${element} is visible.`);
            }
        });
    });
    

    相比之下,我们可以利用现代浏览器提供的 Intersection Observer API 来更高效地监测元素的可见性变化。

    // 定义一个 Intersection Observer
    const observer = new IntersectionObserver(entries => {
        entries.forEach(entry => {
            // 如果元素可见
            if (entry.isIntersecting) {
                // 执行相应操作
                console.log(`${entry.target} is visible.`);
            }
        });
    });
    
    // 获取需要监测可视性的元素
    const elements = document.querySelectorAll('.target-element');
    
    // 监测每个元素
    elements.forEach(element => {
        observer.observe(element);
    });
    

    这里我封装了一个 React Hooks useIntersectionObserver,提供了Intersection Observer API 的能力。

    image.png

    2. 仅渲染可见区域:优化性能

    虚拟化列表的另一个关键优化是仅渲染可见区域内的元素,而不是渲染整个列表。这样做可以大大减少渲染所需的时间和资源,提高页面的性能表现。

    import useIntersectionObserver from './useIntersectionObserver';
    
    const [visibleItems, setVisibleItems] = useState<Set<number>>(new Set());
    
    const handleVisibilityChange = useCallback((isVisible: boolean, entry: IntersectionObserverEntry) => {
        const index = parseInt(entry.target.getAttribute('data-index')!, 10);
        setVisibleItems(prev => {
          const newVisibleItems = new Set(prev);
          if (isVisible) {
            newVisibleItems.add(index);
          } else {
            newVisibleItems.delete(index);
          }
          return newVisibleItems;
        });
      }, []);
      
    const { observe, unobserve } = useIntersectionObserver(containerRef.current, handleVisibilityChange, null, observerOptions);
    

    3. 动态加载和卸载:保持内存使用最小化

    最后,虚拟化列表还可以通过动态加载和卸载元素来保持内存使用最小化。当用户滚动到可视区域时,新的元素被动态加载,而离开可视区域的元素则被卸载,从而减少页面的内存占用。

    image.png
    const visibleRange = useMemo(() => {
        const sortedVisibleItems = [...visibleItems].sort((a, b) => a - b);
        const firstVisible = sortedVisibleItems[0] || 0;
        const lastVisible = sortedVisibleItems[sortedVisibleItems.length - 1] || 0;
        // 设置缓存区
        return [Math.max(0, firstVisible - BUFFER_SIZE), Math.min(listData.length - 1, lastVisible + BUFFER_SIZE)];
      }, [visibleItems, listData.length]);
      
    const renderItems = () => {
        return listData.length ? listData.map((item, index) => {
          if (index >= visibleRange[0] && index <= visibleRange[1]) {
            return (
              <div
                className={itemClassName || undefined}
                style={itemContainerStyle}
                ref={node => handleRef(node, index)}
                key={index}
                data-index={index}
              >
                <VirtualizedListItem
                  item={listData[index]}
                  isVisible={visibleItems.has(index)}
                  refreshOnVisible={refreshOnVisible}
                  fetchItemData={fetchItemData}
                  itemLoader={itemLoader}
                >
                  {renderItem}
                </VirtualizedListItem>
              </div>
            );
          }
          return null;
        }) : (
          emptyListMessage ? emptyListMessage : null
        );
      };
    

    当元素进入视口时,我们加载它;当元素离开视口时,我们卸载它。这样就可以保持页面上始终只有视口内的内容被渲染,从而提高页面的性能和响应速度。

    除此之外,通过使用 useMemo 计算当前可见的列表项范围 (visibleRange),以及设置一个缓冲区 (BUFFER_SIZE);使用useMemouseCallback 用于性能优化的 Hook。它们帮助避免不必要的计算和重新渲染。

    image.png

    性能对比(🔥性能飙升 50%)

    下面我们就来看下,传统滚动 Scroll 监听和 Intersection Observer API 的性能对比数据(假设在相同环境和数据集下测试):

    方法 初始渲染时间 滚动性能 内存使用
    传统滚动监听 300ms
    Intersection Observer API 150ms
    • 初始渲染时间:使用 Intersection Observer API 的初始渲染时间较短,因为只渲染可见区域。
    • 滚动性能:传统滚动监听由于频繁的滚动事件触发和位置计算,滚动性能较低;Intersection Observer API 的滚动性能较高,因为它利用了浏览器的优化机制。
    • 内存使用:Intersection Observer API 由于仅加载和渲染可见元素,内存使用更低。

    性能测试代码分析

    以下是一个示例,展示了如何使用 console.time 和 console.timeEnd 来测量性能:

    // 测量传统滚动监听的性能
    console.time('Scroll');
    window.addEventListener('scroll', () => {
        // 模拟计算每个元素的位置
        const elements = document.querySelectorAll('.target-element');
        elements.forEach(element => {
            const rect = element.getBoundingClientRect();
            if (rect.top >= 0 && rect.bottom <= window.innerHeight) {
                // 模拟渲染逻辑
            }
        });
    });
    console.timeEnd('Scroll');
    
    // 测量 Intersection Observer API 的性能
    console.time('IntersectionObserver');
    const observer = new IntersectionObserver(entries => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                // 模拟渲染逻辑
            }
        });
    });
    const elements = document.querySelectorAll('.target-element');
    elements.forEach(element => observer.observe(element));
    console.timeEnd('IntersectionObserver');
    

    注意:传统滚动监听方法还会涉及大量计算,这里仅简单测量了监听性能的统计部分。

    传统的滚动监听方式通过监听 scroll 事件,在每次滚动时计算每个目标元素的位置,并判断其是否在视窗内。这部分代码的执行会阻塞主线程,尤其在滚动频繁的情况下可能导致性能问题,因为需要不断重新计算元素位置。

    相比之下,Intersection Observer API 更高效。它可以检测元素是否可见,并在元素进入或退出视窗时触发回调函数,从而实现需要的功能。

    0.gif

    性能总结

    在性能方面,传统实现方法通常需要通过监听滚动(scroll)事件来计算元素位置。这种方法存在以下问题:

    • 性能消耗大:频繁监听滚动事件会导致性能消耗增加,尤其是在大型数据集的情况下。
    • 计算复杂度高:需要手动计算每个列表项与视口的交叉情况,逻辑复杂且容易出错。需要花费大量时间和精力来优化和调试这些计算逻辑。

    相比之下,Intersection Observer API 的性能更优,具有以下优点:

    1. 性能开销低Intersection Observer API 利用浏览器的内部优化机制,减少了不必要的计算和事件触发,从而提高了性能。相比之下,传统的 scroll 事件监听方式由于密集触发,可能会导致较大的性能问题。
    2. 多元素监测Intersection Observer API 允许同时监测多个元素的交叉状态,而不需要为每个元素都绑定事件监听器。这使得在处理复杂布局和交互时更加高效。
    3. 异步执行:当元素进入或离开交叉状态时,Intersection Observer 会异步执行回调函数,不会阻塞主线程。这有助于保持页面的响应性和流畅性。
    4. 应用场景广泛Intersection Observer API 可以应用于多种场景,如懒加载、无限滚动、广告展示与统计、页面元素动画等。这些应用场景通常需要高效地处理元素与视口之间的交互。

    综上所述,Intersection Observer API 在处理大型数据集和复杂交互时,相比传统的 scroll 事件监听方式,提供了更高的性能和更灵活的解决方案。

    项目成果展示(🔥渲染速度提升95%)

    下面我们看下优化后的性能,展示实际改进的用户体验和加载时间。

    首先从视觉感官上看,几乎是一瞬间图表就加载了出来。我们接着再来看看接口Network与数据对比!

    image.png

    为了清楚地展示优化前后页面加载速度的提升,我们可以将相关数据整理成一个表格形式,如下所示:

    优化指标 优化前 优化后 加载速度提升
    总耗时 15000 毫秒(15秒) 750 毫秒 提速了95%

    这个表格展示了优化措施的显著效果,从中可以看出,经过优化后,整体加载时间也从15000毫秒大幅减少至750毫秒,加载速度提高了95%。

    总结

    通过使用 react-virtualized-list 库,监控系统项目前端渲染性能得到了显著提升。统计结果显示:页面加载速度提高了 95%,用户体验得到了明显改善。如果你也在处理大数据集的渲染问题,不妨试试这个库。

    希望本文能对你有所帮助,有所借鉴!大家有什么疑问或者建议,欢迎在评论区一起讨论。

    参考资料

    1. Intersection Observer API
    2. react-virtualized-list
    3. 详解 Intersection Observer API ( 交叉观察器 )

    相关文章

      网友评论

          本文标题:性能飙升50%,react-virtualized-list如何

          本文链接:https://www.haomeiwen.com/subject/mhftcjtx.html