文字图片制作网站,北京市违法建设投诉网站,自己能做app软件吗,h5制作的软件各位同仁#xff0c;各位技术探索者们#xff0c;大家好。今天#xff0c;我们将深入探讨一个在前端性能优化领域极具挑战性的话题#xff1a;如何实现大规模列表的“极致”虚拟滚动。我们都知道#xff0c;在现代Web应用中#xff0c;展示成千上万条数据是家常便饭。然而…各位同仁各位技术探索者们大家好。今天我们将深入探讨一个在前端性能优化领域极具挑战性的话题如何实现大规模列表的“极致”虚拟滚动。我们都知道在现代Web应用中展示成千上万条数据是家常便饭。然而浏览器处理如此庞大的DOM元素往往会导致页面卡顿、响应迟缓用户体验直线下降。传统的虚拟滚动技术已经为我们解决了大部分问题但今天我们将结合CSS的content-visibility属性与React的虚拟滚动机制探索一种更深层次的优化实现“只渲染视口内Fiber”的错觉从而大幅提升性能。一、大规模列表的性能瓶颈与传统虚拟滚动的局限在深入探讨新技术之前我们首先回顾一下大规模列表带来的核心性能问题。当我们在浏览器中渲染一个包含数千甚至数万个列表项时会遇到以下几个主要瓶颈DOM 元素过多浏览器需要为每个DOM元素分配内存并维护其在DOM树中的结构。过多的DOM元素会消耗大量内存。布局Layout和绘制Paint时间长当滚动、改变尺寸或更新样式时浏览器可能需要重新计算所有可见元素的几何信息布局然后将它们绘制到屏幕上。元素越多这个过程越耗时。JavaScript 执行负担如果每个列表项都有复杂的React组件逻辑、事件监听器或状态管理那么即使是React的Fiber协调过程也可能因为组件数量庞大而变得缓慢。内存泄漏不恰当的事件监听器或数据引用可能导致旧的、不可见的列表项无法被垃圾回收。为了解决这些问题虚拟滚动Virtual Scrolling技术应运而生。其核心思想是只渲染当前视口内和少量缓冲区域内的列表项而将视口外的列表项替换为占位符或者干脆不渲染。传统虚拟滚动的工作原理一个典型的虚拟滚动器会计算总高度根据所有列表项的数量和平均/预估高度计算出整个可滚动区域的总高度并将其应用到一个内部容器上。监听滚动事件监测容器的滚动位置。确定可见范围根据滚动位置和视口高度计算出当前应该渲染哪些列表项startIndex到endIndex。通常还会额外渲染一个小的缓冲区域以避免快速滚动时出现空白。动态渲染仅渲染startIndex到endIndex之间的列表项。视口外的列表项组件会被卸载unmount其DOM元素会被移除。传统虚拟滚动的优势显著减少DOM元素这是最主要的优势直接解决了DOM元素过多的问题。减少布局和绘制浏览器只需处理少量可见元素的布局和绘制。传统虚拟滚动的局限性尽管传统虚拟滚动效果显著但它并非没有缺点特别是在某些场景下组件卸载与挂载开销当用户快速滚动时列表项组件会频繁地被卸载和重新挂载。如果组件的挂载mount和卸载unmount生命周期包含复杂的逻辑如数据请求、订阅/取消订阅、复杂的DOM操作这会导致额外的性能开销。状态丢失组件在被卸载后其内部状态会丢失。如果用户滚动回来组件需要重新初始化状态。虽然可以通过外部管理状态来缓解但会增加代码复杂度。SEO 和可访问性视口外的DOM元素被移除这意味着搜索引擎爬虫和屏幕阅读器可能无法访问到所有内容这在某些情况下是一个问题。“闪烁”效应在快速滚动或计算不准确的情况下可能会出现短暂的空白区域影响用户体验。并非所有渲染开销都消除即使是占位符例如一个div只设置高度它仍然是一个DOM元素仍然需要浏览器进行布局和绘制。正是这些局限性促使我们寻找更极致的优化方案。二、CSScontent-visibility浏览器级的渲染优化利器在Web标准领域为了应对大型页面性能问题CSS引入了一个强大的新属性content-visibility。这个属性的出现为我们提供了一个在浏览器渲染层面进行优化的新视角。什么是content-visibilitycontent-visibility属性允许用户代理浏览器在元素不相关时跳过其布局和绘制工作从而显著提高页面加载性能。简单来说当一个设置了content-visibility的元素不在用户的视口内时浏览器会对其内容进行高度优化甚至完全跳过其内容的渲染过程就像它根本不存在一样但又不移除DOM元素。content-visibility的核心值content-visibility主要有以下几个值visible(默认值):元素的内容总是可见的并正常进行布局和绘制。hidden:元素的内容被隐藏并且浏览器会跳过其布局和绘制。与display: none类似但它不影响元素的盒子模型只是不显示内容。auto:这是最强大也是最常用的值。当元素在视口内时它的内容正常渲染。当元素在视口外时浏览器会跳过其内容的布局和绘制。浏览器会尝试保留其在DOM树中的位置但不会渲染其内部的任何内容。这使得浏览器可以大幅节省渲染成本。配合contain-intrinsic-size防止滚动跳动content-visibility: auto虽然强大但它有一个潜在问题当浏览器跳过元素的布局和绘制时它并不知道该元素实际占据的高度。如果一个元素在视口外时被跳过然后滚动到视口内时才进行布局并确定高度可能会导致滚动条突然跳动因为浏览器需要重新计算整个页面的滚动高度。为了解决这个问题contain-intrinsic-size属性应运而生。它允许我们为具有content-visibility: auto的元素指定一个预估的固有尺寸。contain-intrinsic-size: width height:例如contain-intrinsic-size: 100px 200px表示预估宽度为100px高度为200px。contain-intrinsic-size: auto length:例如contain-intrinsic-size: auto 200px表示宽度由内容决定高度预估为200px。contain-intrinsic-size: 100px:简写表示宽度和高度都预估为100px。当元素在视口外时浏览器会使用contain-intrinsic-size指定的尺寸作为占位符从而保持滚动条的稳定性。一旦元素进入视口它就会被正常布局并使用其真实尺寸。content-visibility的浏览器支持截至2023年末content-visibility属性在主流现代浏览器Chrome, Edge, Firefox, Opera中已经得到了良好的支持。Safari 也在积极开发中。这意味着我们可以在生产环境中使用它但仍需注意目标用户的浏览器分布。content-visibility的优势总结使用content-visibility可以带来以下显著优势跳过布局和绘制对于视口外的元素浏览器完全跳过其内部内容的布局和绘制过程带来巨大的性能提升。不移除DOM元素与传统虚拟滚动不同元素依然存在于DOM树中只是不被渲染。这意味着元素的React Fiber节点、组件状态、事件监听器等都被保留无需重新创建。保持状态组件状态不会因为滚动而丢失用户体验更流畅。提升SEO和可访问性所有内容都存在于DOM中对搜索引擎和辅助技术更友好。更快的首次渲染可能对于某些场景由于浏览器可以更快地跳过大量非必要的渲染工作初始页面加载可能会更快。三、content-visibility配合 React 虚拟滚动极致优化之道现在让我们把content-visibility这个强大的CSS属性与React的虚拟滚动理念结合起来。这并非简单地在现有虚拟滚动器上添加一个CSS属性而是对其核心思路的一种“范式转变”。核心思想的转变传统虚拟滚动“只渲染视口内的DOM元素。” 视口外的元素从DOM中移除组件被卸载。content-visibility React“渲染所有列表项的React组件和Fiber节点但让浏览器只布局和绘制视口内的DOM元素。” 视口外的元素仍然存在于DOM中组件保持挂载状态但浏览器对其渲染工作进行极致优化。这种转变意味着React Fiber树可能会包含所有列表项的Fiber节点。这意味着React在协调阶段会处理更多的Fiber节点。DOM树也将包含所有列表项的DOM元素。浏览器渲染引擎这是关键它会利用content-visibility: auto的特性智能地跳过视口外元素的布局、绘制甚至部分合成阶段。这种组合的适用场景与优势复杂列表项组件如果你的列表项组件包含复杂的UI逻辑、内部状态、耗时的副作用如数据订阅、大量计算那么避免其频繁挂载/卸载的成本将是巨大的。需要保持状态当列表项组件的内部状态例如一个输入框的值一个复选框的选中状态需要在用户滚动时保持不变时此方法非常理想。快速滚动的场景由于组件不需要重新挂载滚动体验将更加流畅没有“闪烁”或重新加载的感知。SEO和可访问性要求高所有内容都在DOM中对搜索引擎和辅助技术更友好。浏览器布局/绘制是瓶颈如果你的列表项DOM结构复杂导致浏览器布局和绘制成本高昂content-visibility能直接解决这个问题。潜在的权衡当然没有银弹。这种方法也有其权衡之处React Fiber树和内存所有的列表项组件都会被挂载并存在于React的Fiber树中。对于极大规模的列表例如数百万条这可能会导致Fiber树过大占用较多内存甚至影响React自身的协调性能。JavaScript 执行开销尽管浏览器跳过了渲染但如果所有组件在挂载时都有耗时的JavaScript逻辑执行例如大量数据转换、复杂的初始化逻辑这些开销仍然存在。首次加载首次渲染时React仍需处理所有组件的初始渲染。如果组件数量巨大且渲染逻辑复杂首次加载时间可能会比传统虚拟滚动稍长但后续滚动性能会更好。因此理解这些权衡非常重要。对于绝大多数“大”列表几百到几万条这种方法带来的浏览器渲染性能提升是压倒性的。四、实现一个具备content-visibility的React虚拟滚动器现在让我们通过代码示例来具体实现这一优化策略。我们将从一个基本的虚拟滚动器开始逐步引入content-visibility和contain-intrinsic-size并处理固定高度和可变高度的场景。4.1 基础结构一个简单的列表组件首先我们定义一个普通的列表项组件用于展示数据。// src/components/ListItem.jsx import React from react; const ListItem React.memo(({ index, data, style }) { // 模拟复杂渲染或计算 const complexCalculation () { let result 0; for (let i 0; i 10000; i) { result Math.sin(i) * Math.cos(i); } return result; }; // console.log(Rendering Item ${index}); // 用于观察挂载/渲染情况 return ( div style{{ ...style, borderBottom: 1px solid #eee, padding: 10px 15px }} h3Item {index}/h3 p{data.text}/p pValue: {data.value}/p {/* 模拟一些复杂的DOM结构 */} div style{{ display: flex, justifyContent: space-between, fontSize: 0.8em, color: #666 }} spanID: {data.id}/span spanDate: {data.date}/span /div {/* pComplex Calc Result: {complexCalculation()}/p // 如果需要测试JS计算开销可以取消注释 */} /div ); }); export default ListItem;4.2 数据生成器为了模拟大量数据我们创建一个数据生成函数。// src/utils/dataGenerator.js const generateData (count) { const data []; for (let i 0; i count; i) { data.push({ id: i, text: This is a long description for item number ${i}. It contains some placeholder text to make it realistic., value: Math.floor(Math.random() * 1000), date: new Date().toLocaleDateString(), // 可以添加一个随机高度来模拟可变高度 randomHeight: Math.max(50, Math.floor(Math.random() * 150) 50) // 50px 到 200px }); } return data; }; export default generateData;4.3 实现ContentVisibilityVirtualScroller(固定高度版)我们将创建一个React组件它负责处理滚动逻辑并应用content-visibility。// src/components/ContentVisibilityVirtualScroller.jsx import React, { useRef, useState, useEffect, useCallback } from react; import ListItem from ./ListItem; // 引入列表项组件 const ContentVisibilityVirtualScroller ({ items, itemHeight 50, // 默认固定高度 containerHeight 500, overscan 5 // 视口外额外渲染的缓冲项数量 }) { const scrollContainerRef useRef(null); const [scrollTop, setScrollTop] useState(0); const totalHeight items.length * itemHeight; // 监听滚动事件 const handleScroll useCallback(() { if (scrollContainerRef.current) { setScrollTop(scrollContainerRef.current.scrollTop); } }, []); useEffect(() { const container scrollContainerRef.current; if (container) { container.addEventListener(scroll, handleScroll); return () { container.removeEventListener(scroll, handleScroll); }; } }, [handleScroll]); return ( div ref{scrollContainerRef} style{{ height: containerHeight, overflowY: auto, border: 1px solid #ccc, position: relative, // 确保内部绝对定位的元素能正确参考 }} div style{{ height: totalHeight, // 整个列表的总高度 position: relative, }} {items.map((item, index) ( div key{item.id} style{{ // 每个列表项的定位和尺寸 position: absolute, top: index * itemHeight, width: 100%, height: itemHeight, // 核心优化content-visibility contentVisibility: auto, containIntrinsicSize: ${itemHeight}px, // 预估高度这里就是实际高度 }} ListItem index{index} data{item} / /div ))} /div /div ); }; export default ContentVisibilityVirtualScroller;关键点解释totalHeight我们仍然需要计算所有列表项的总高度并将其赋给一个内部的占位div以确保滚动条的正确长度。items.map与传统虚拟滚动不同这里我们遍历items数组中的所有项为每一项创建一个DOM元素。*position: absolute和 top: indexitemHeight** 每个列表项都通过绝对定位来放置在正确的位置上。这模拟了传统虚拟滚动中计算translateY 的效果。contentVisibility: auto这是核心。它告诉浏览器当这个div不在视口内时可以跳过其内容的渲染。containIntrinsicSize:${itemHeight}px“这为浏览器提供了视口外元素的高度信息。因为我们这里是固定高度所以直接使用itemHeight。这能确保滚动条的稳定性和准确性。与传统虚拟滚动的对比特性传统虚拟滚动content-visibility虚拟滚动DOM 元素数量仅视口内 缓冲区的DOM元素所有列表项的DOM元素React 组件仅视口内 缓冲区的组件被挂载和渲染所有列表项的组件都被挂载和渲染 (Fiber 节点存在)浏览器渲染仅处理少量可见DOM元素的布局和绘制浏览器跳过视口外DOM元素的布局和绘制组件状态滚动时可能丢失需外部管理组件状态保持不变挂载/卸载成本频繁仅一次内存占用DOM/Fiber 树小DOM/Fiber 树大SEO/A11y仅可见内容可访问所有内容都可访问开发复杂性需要精确计算可见范围和缓冲区需要处理contain-intrinsic-size和动态高度测量4.4 改进支持可变高度的ContentVisibilityVirtualScroller可变高度是content-visibility虚拟滚动更具挑战性的场景因为contain-intrinsic-size需要准确的预估值。我们的策略是初始预估为所有项设置一个合理的默认contain-intrinsic-size。测量真实高度当列表项第一次进入视口并被浏览器实际渲染时测量其真实高度。存储高度将测量到的高度存储起来以便下次渲染时使用。更新contain-intrinsic-size使用存储的真实高度来更新该项的contain-intrinsic-size。这需要ResizeObserver或useLayoutEffect来进行DOM测量。// src/components/ContentVisibilityVirtualScrollerVariableHeight.jsx import React, { useRef, useState, useEffect, useCallback } from react; import ListItem from ./ListItem; // 引入列表项组件 const INITIAL_ITEM_HEIGHT_GUESS 100; // 初始预估高度很重要 const ContentVisibilityVirtualScrollerVariableHeight ({ items, containerHeight 500, }) { const scrollContainerRef useRef(null); const itemRefs useRef(new Map()); // 用于存储每个列表项的DOM引用 const [itemHeights, setItemHeights] useState(new Map()); // 存储每个列表项的真实高度 const [totalHeight, setTotalHeight] useState(0); // 整个列表的总高度 // 计算总高度 useEffect(() { let currentTotalHeight 0; for (let i 0; i items.length; i) { currentTotalHeight itemHeights.get(items[i].id) || INITIAL_ITEM_HEIGHT_GUESS; } setTotalHeight(currentTotalHeight); }, [items, itemHeights]); // 使用 ResizeObserver 测量并更新项的高度 useEffect(() { const observer new ResizeObserver(entries { const newHeights new Map(itemHeights); let changed false; entries.forEach(entry { const itemId entry.target.dataset.itemId; if (itemId) { const newHeight entry.contentRect.height; if (newHeight 0 newHeights.get(itemId) ! newHeight) { newHeights.set(itemId, newHeight); changed true; } } }); if (changed) { setItemHeights(newHeights); } }); // 观察所有当前挂载的列表项 itemRefs.current.forEach(ref { if (ref) observer.observe(ref); }); return () observer.disconnect(); }, [itemHeights]); // 当 itemHeights 变化时可能需要重新观察但通常只在初次挂载时设置观察者 // 计算每个列表项的 top 定位 const getItemTop useCallback((index) { let top 0; for (let i 0; i index; i) { top itemHeights.get(items[i].id) || INITIAL_ITEM_HEIGHT_GUESS; } return top; }, [items, itemHeights]); return ( div ref{scrollContainerRef} style{{ height: containerHeight, overflowY: auto, border: 1px solid #ccc, position: relative, }} div style{{ height: totalHeight, // 整个列表的总高度现在是动态计算的 position: relative, }} {items.map((item, index) { const measuredHeight itemHeights.get(item.id); const currentItemHeight measuredHeight || INITIAL_ITEM_HEIGHT_GUESS; const top getItemTop(index); return ( div key{item.id} ref{el { if (el) itemRefs.current.set(item.id, el); else itemRefs.current.delete(item.id); // 清理旧的引用 }} >// src/App.js import React, { useState } from react; import generateData from ./utils/dataGenerator; import ContentVisibilityVirtualScroller from ./components/ContentVisibilityVirtualScroller; import ContentVisibilityVirtualScrollerVariableHeight from ./components/ContentVisibilityVirtualScrollerVariableHeight; const NUM_ITEMS 10000; // 1万条数据 const itemsData generateData(NUM_ITEMS); function App() { const [useVariableHeight, setUseVariableHeight] useState(false); return ( div style{{ padding: 20px, maxWidth: 800px, margin: 0 auto }} h1content-visibility Virtual Scrolling Demo/h1 pTotal items: {NUM_ITEMS}/p label input typecheckbox checked{useVariableHeight} onChange{(e) setUseVariableHeight(e.target.checked)} / Use Variable Height Items /label hr / {useVariableHeight ? ( ContentVisibilityVirtualScrollerVariableHeight items{itemsData} containerHeight{600} / ) : ( ContentVisibilityVirtualScroller items{itemsData} itemHeight{60} // 固定高度示例 containerHeight{600} / )} /div ); } export default App;通过这个示例你可以清楚地看到content-visibility的工作方式。当你滚动时浏览器会跳过视口外元素的渲染但你不会看到元素被卸载和重新挂载的“闪烁”感。对于可变高度虽然初始滚动可能有些许跳动但一旦元素被测量过滚动体验就会变得非常流畅。五、高级考量与最佳实践5.1 内存管理与Fiber树大小如前所述content-visibility方法会使所有列表项的React组件保持挂载因此React的Fiber树将包含所有这些节点。对于数百万级别的列表项这可能会导致JavaScript 堆内存占用增加每个Fiber节点、组件实例、其内部状态和闭包都会占用内存。React 协调性能即使是快速路径React也需要遍历更大的Fiber树来检查更新。建议谨慎评估列表规模对于百万级别以上的列表可能需要重新考虑是否所有的组件状态都必须保持。优化组件确保ListItem组件足够轻量避免在render或useEffect中执行过多昂贵的计算。使用React.memo避免不必要的渲染。按需加载如果列表真的非常庞大考虑分页或无限滚动与后端数据结合而不是一次性加载所有数据。5.2 初始渲染性能在首次加载时React仍然需要为所有列表项创建Fiber节点并执行其首次渲染逻辑。如果列表项组件的渲染逻辑非常复杂这可能会导致初始加载时间比传统的、只渲染少量元素的虚拟滚动器更长。优化策略延迟渲染对于不重要的部分可以使用requestIdleCallback或setTimeout延迟其渲染。骨架屏Skeleton Screen在列表加载期间显示骨架屏提升用户感知性能。SSR/SSG对于SEO和首次加载性能要求高的应用可以考虑服务器端渲染或静态站点生成。5.3 动态高度变化如果列表项的高度在首次测量后又发生了变化例如图片加载完成、文本展开/收起ResizeObserver会再次捕获到这些变化并更新itemHeights。这使得contain-intrinsic-size能够适应动态内容。注意事项确保ResizeObserver能够正确地被所有需要观察的元素引用并在组件卸载时正确清理。频繁的高度变化会导致setItemHeights频繁更新从而触发totalHeight和所有列表项top值的重新计算这本身也可能带来性能开销。适度的节流throttle或防抖debounce可能有助于优化。5.4 辅助功能Accessibility与 SEOcontent-visibility的一个显著优点是所有内容都存在于DOM中。这意味着屏幕阅读器可以访问所有列表项的内容而不仅仅是当前可见的。搜索引擎爬虫可以索引所有内容对SEO更有利。这与传统虚拟滚动相比是一个巨大的优势因为传统虚拟滚动通常会移除视口外的内容对辅助功能和SEO不友好。5.5 浏览器兼容性在使用content-visibility之前务必检查你的目标用户群的浏览器兼容性。对于不支持的浏览器你需要提供一个回退方案。回退方案CSSsupports可以使用supports规则来检测浏览器是否支持content-visibility然后提供不同的样式。.list-item { /* 默认样式 */ height: 100px; } supports (content-visibility: auto) { .list-item { content-visibility: auto; contain-intrinsic-size: 100px; } }JavaScript 检测在JavaScript中检测CSS.supports(content-visibility, auto)然后动态地添加/移除样式类或选择不同的虚拟滚动实现。六、总结与展望通过将CSS的content-visibility属性与React的虚拟滚动机制相结合我们能够实现一种全新的、极致的列表渲染优化。这种方法的核心在于我们不再完全移除视口外的DOM元素和React组件而是让它们保持挂载并通过浏览器原生的content-visibility属性来跳过其布局和绘制过程。这种模式的优势在于它能够保留组件状态、减少组件频繁挂载/卸载的开销、提升SEO和可访问性并显著降低浏览器在处理大规模列表时的渲染负担。尤其适用于列表项组件本身较为复杂、需要维持状态或DOM结构较为复杂的场景。然而它并非没有权衡。更大的React Fiber树和潜在的JavaScript内存占用是我们需要考虑的因素。对于极大规模的列表我们仍需仔细评估其适用性。content-visibility是Web平台为解决大规模内容性能问题而提供的一个强大工具。它代表了浏览器引擎优化渲染的新趋势。作为前端开发者我们应该积极拥抱并探索这些新特性将它们巧妙地融入到我们的React应用中为用户提供更流畅、更高效的体验。未来随着更多浏览器对这些属性的支持日趋完善以及我们对它们理解的加深这种极致的虚拟滚动优化方案将会在更多场景下大放异彩。