@tanstack/react-virtual 动态尺寸虚拟列表示例深度解析

发布时间:2026/10/6 7:42:38
@tanstack/react-virtual 动态尺寸虚拟列表示例深度解析 前端UI组件【免费下载链接】virtual Headless UI for Virtualizing Large Element Lists in JS/TS, React, Solid, Vue and Svelte项目地址https://gitcode.com/gh_mirrors/vi/virtual点击查看免费下载动态尺寸Dynamic Size是虚拟滚动中最棘手的场景之一元素在渲染前尺寸未知只有挂载后才能测量。本文以当前仓库 examples/react/dynamic 示例为主线完整讲解基于tanstack/react-virtual实现动态行、动态列、窗口级动态网格与实验性手动 DOM 更新的四种实战写法并深入virtual-core源码揭示measureElement动态测量、scrollToIndex定位与directDomUpdates直接 DOM 更新的底层原理。读完你不仅能跑通示例还能在真实项目中复刻这套「先估算、后实测、滚动中实时校正」的动态虚拟化方案。一、示例概览与运行方式examples/react/dynamic的 README 给出了最简运行说明npm install npm run start结合该目录下的 package.json 可以看更完整的脚本体系脚本命令作用devvite启动开发服务器热更新buildtsc vite build先做 TypeScript 类型检查再产出生产构建servevite preview预览生产构建产物依赖方面示例直接使用 npm 包tanstack/react-virtual当前仓库中对应源码位于 packages/react-virtual核心引擎在 packages/virtual-core并用faker-js/faker生成测试数据react/react-dom为 19.x工程配置由 vite.config.js仅挂载vitejs/plugin-react与 tsconfig.jsonstrict严格模式、jsx: react-jsx、moduleResolution: Bundler提供。注意本仓库是 pnpm workspace若在仓库根目录运行建议使用pnpm install安装再进入examples/react/dynamic目录按上方 npm 脚本执行。二、示例结构四个演示路由与「动态尺寸」定义示例入口 main.tsx 根据location.pathname在四个演示间切换路由组件演示内容/RowVirtualizerDynamic容器内动态行列表/columnsColumnVirtualizerDynamic容器内动态列横向列表/gridGridVirtualizerDynamic窗口级滚动的动态网格行 × 列双虚拟化/experimentalRowVirtualizerExperimental实验性手动 DOM 更新方案组件代码中对「动态尺寸」的定义值得原文引用These components are usingdynamicsizes. This means that each elements exact dimensions are unknown when rendered. An estimated dimension is used as the initial measurement, then this measurement is readjusted on the fly as each element is rendered.即先用estimateSize给出的估算值完成首次布局元素真正渲染后再用measureElement实测尺寸、实时校正后续所有元素的位置与总高度。这与固定尺寸虚拟化的最大区别在于——任何一行的实测高度都会引发下游start/size重算因此必须配合 ResizeObserver 持续观测。三、动态行虚拟列表RowVirtualizerDynamic这是本示例的核心组件展示了动态列表的最小完整实现。3.1 数据准备const randomNumber (min: number, max: number) faker.number.int({ min, max }) const sentences new Array(10000) .fill(true) .map(() faker.lorem.sentence(randomNumber(20, 70)))10000 条长度随机2070 词的句子保证每行真实高度不同——这正是动态尺寸测试所需要的「不确定高度」数据源。3.2 useVirtualizer 配置const virtualizer useVirtualizer({ count, getScrollElement: () parentRef.current, estimateSize: () 45, // 初始估算值45px enabled, // 可开关 directDomUpdates: true, // 跳过 React 渲染直接写 DOM })参数说明count数据总量 10000getScrollElement返回滚动容器 DOMparentRef.currentestimateSize: () 45所有行的初始估算高度。动态场景下它只是首帧布局的「脚手架」随后会被实测值覆盖enabled布尔开关示例用按钮turn {enabled ? off : on} virtualizer动态切换关闭时虚拟化暂停滚动位置不再被接管对应 core 中的enabled选项directDomUpdates: trueReact 适配器专属优化滚动过程中直接写 DOM 而跳过 React 重渲染原理见第七章。3.3 滚动定位scrollToIndexvirtualizer.scrollToIndex(count - 1, { align: end }) // 挂载后滚到底部 virtualizer.scrollToIndex(0) // 回到顶部 virtualizer.scrollToIndex(count / 2, { behavior: smooth }) // 平滑滚到中间 virtualizer.scrollToIndex(count - 1) // 滚到底部scrollToIndex(index, options)是虚拟化列表的程序化定位 APIalign可取auto | start | center | end示例中align: end让目标行对齐容器底部在 main.tsx 中用于挂载后自动滚到列表末尾模拟聊天场景behavior: smooth启用平滑滚动动画。scrollToIndex属于同步测量路径——core 在measureElement的注释中明确说明「scrollToIndex/scrollToOffset 需要同一帧内的尺寸因此走同步测量」(packages/virtual-core/src/index.ts)确保定位计算使用的是最新实测高度而非过期估算值。3.4 容器与渲染div ref{parentRef} classNameList style{{ height: 400, width: 400, overflowY: auto, contain: strict, // 布局/绘制/尺寸三重隔离减少滚动重排成本 overflowAnchor: none,// 关闭浏览器滚动锚定避免列表向上插入时跳动 }} div ref{virtualizer.containerRef} style{{ width: 100%, position: relative }} {virtualizer.getVirtualItems().map((v) ( div key{v.key} >const virtualizer useVirtualizer({ horizontal: true, count: sentences.length, getScrollElement: () parentRef.current, estimateSize: () 45, })渲染侧div style{{ width: virtualizer.getTotalSize(), height: 100%, position: relative }} {virtualizer.getVirtualItems().map((virtualColumn) ( div key{virtualColumn.key} >const parentOffsetRef React.useRef(0) React.useLayoutEffect(() { parentOffsetRef.current parentRef.current?.offsetTop ?? 0 }, []) const virtualizer useWindowVirtualizer({ count: data.length, estimateSize: () 350, overscan: 5, scrollMargin: parentOffsetRef.current, })useWindowVirtualizer滚动元素固定为windowReact 适配器自动注入observeWindowRect/observeWindowOffset/windowScroll并将初始滚动偏移设为window.scrollY见 packages/react-virtual/src/index.tsxscrollMargin网格容器不在页面顶部时容器顶部相对文档原点的偏移量此处用useLayoutEffect读取offsetTop存入 ref。core 在计算虚拟项start与滚动偏移对齐时必须减去该值否则行位置会整体错位overscan: 5可见区上下各多渲染 5 行作为缓冲降低快速滚动时的白屏感。行渲染时同样通过ref{virtualizer.measureElement}实测每行高度并用transform: translateY(row.start - scrollMargin)定位。5.2 列虚拟化容器内横向 useVirtualizerconst columnVirtualizer useVirtualizer({ horizontal: true, count: columns.length, getScrollElement: () parentRef.current, estimateSize: getColumnWidth, // 由随机宽度表提供 overscan: 5, })列宽来自generateColumns(30)生成的随机 75300px 宽度表。before/after两个占位块保证行内两侧对齐const [before, after] columnItems.length 0 ? [columnItems[0].start, columnVirtualizer.getTotalSize() - columnItems[columnItems.length - 1].end] : [0, 0]before是首列虚拟项之前被裁掉的累计宽度after是末列虚拟项之后剩余的宽度分别以div style{{ width: ... }} /插在行首尾从而让每个可见单元格在行内横向位置与列虚拟化计算结果精确对齐。5.3 单元格内容首行渲染列名表头其余行渲染faker.lorem.lines生成的多行文本(rowIndex colIndex) % 10 1行行数随机模拟动态高度单元格配合minHeight/borderBottom/borderRight呈现表格形态。单元格区域本身不做纵向虚拟化因为每行的整体高度已由行虚拟化器统一管理。六、实验性方案RowVirtualizerExperimental 手动 DOM 更新第四个演示绕开了 React 渲染通过onChange回调 getVirtualIndexes()手动搬运 DOM展示虚拟化的「裸」用法const virtualizer useVirtualizer({ count, getScrollElement: () parentRef.current, estimateSize: () 45, enabled, onChange: (instance) { // 手动写容器总高度 innerRef.current!.style.height ${instance.getTotalSize()}px // 手动给每个虚拟行写位移 instance.getVirtualItems().forEach((virtualRow) { const rowRef rowRefsMap.current.get(virtualRow.index) if (!rowRef) return rowRef.style.transform translateY(${virtualRow.start}px) }) }, }) const indexes virtualizer.getVirtualIndexes()要点onChange每当虚拟化器内部状态变化滚动、测量、尺寸变化都会触发这里用它把「总高度」和「每行位移」直接写入 DOMgetVirtualItems()返回当前窗口内所有虚拟项及其实测startgetVirtualIndexes()返回当前应渲染的索引数组含 overscan用它驱动indexes.map(...)渲染配合rowRefsMapMapnumber, HTMLDivElement在 ref 回调中登记 DOM 节点ref{(el) { if (el) { virtualizer.measureElement(el) // 仍要测量 rowRefsMap.current.set(index, el) } }}因为位移由onChange手动写transform行元素不再需要position: absolute这也是「实验性」写法的自由度所在挂载后调用virtualizer.measure()强制立即测量一轮main.tsx确保首帧就拿到真实尺寸而不是等 ResizeObserver 异步回调。七、源码级原理measureElement 的动态测量机制动态尺寸的核心是「估算 → 实测 → 校正」闭环其实现位于 packages/virtual-core/src/index.ts有 ResizeObserver entry 时优先读entry.borderBoxSize[0]的inlineSize横向或blockSize纵向四舍五入后返回——这是最精确的含边框实测值无 entry同步测量路径时先查itemSizeCache是否有缓存命中则直接返回避免在重渲染时触发offsetHeight等同步布局读性能关键兜底读element.offsetHeight/offsetWidth按horizontal选轴useCachedMeasurements: true时跳过 DOM 读取直接用缓存或estimateSize兜底。实例方法measureElementpackages/virtual-core/src/index.ts负责从data-index反查索引 → 缓存节点到elementsCache→ 用 ResizeObserver 观测尺寸变化 → 在「空闲或程序化滚动」时同步调用resizeItem落库。resizeItem计算新旧尺寸的差值delta非零时更新该行及所有下游行的start与总尺寸并处理「锚定在末尾时追加内容」的特殊情况——这正是动态列表在数据增长时仍能稳定贴底聊天场景的机制。对应的虚拟化选项packages/virtual-core/src/index.ts整理如下选项类型/默认说明countnumber数据总量getScrollElement() TScrollElement \| null返回滚动元素estimateSize(index: number) number每项初始估算尺寸动态测量的起点measureElement自定义测量函数覆盖默认 DOM 测量逻辑overscannumber默认 1窗口外额外渲染的缓冲数量horizontalboolean是否横向虚拟化paddingStart/paddingEndnumber默认 0列表首/尾的固定内边距scrollPaddingStart/scrollPaddingEndnumber定位时给滚动元素留的安全边距scrollMarginnumber默认 0窗口级虚拟化时容器相对文档的偏移initialOffsetnumber \| () number初始滚动偏移useWindowVirtualizer默认取window.scrollYgetItemKey(index) Key自定义虚拟项 keyrangeExtractor自定义自定义窗口索引提取算法gapnumber项间距计入测量indexAttributestring默认data-index虚拟项索引标记属性名lanesnumber多车道/瀑布流布局anchorTostart \| end锚定方向配合追加/前置数据scrollEndThresholdnumber判定「接近底部」的阈值enabledboolean默认 true暂停/恢复虚拟化useCachedMeasurementsboolean跳过 DOM 测量改用缓存isRtlboolean从右向左布局八、源码级原理directDomUpdates 与 React 适配器本示例的行列表开启了directDomUpdates: true这是 React 适配器packages/react-virtual/src/index.tsx独有的优化滚动引起的更新不再走 React 重渲染而是由适配器直接把位移与容器尺寸写入 DOM仅当可见区间startIndex/endIndex或isScrolling变化时才触发 re-render。其内部机制值得展开containerRefpackages/react-virtual/src/index.tsx注册内部尺寸容器并立即把getTotalSize()写入其height/widthapplyDirectStyles遍历虚拟项用WeakMapHTMLElement, number记录上次写入的位移值实现幂等directDomUpdatesMode: transform默认写translate3d(0, y, 0)元素被提升到独立合成层、长列表滚动更平滑position模式则直接写top/leftpackages/react-virtual/src/index.tsxuseFlushSync默认 true非测量期间的同步通知用flushSync(rerender)立即刷新而 ref 回调测量期间跳过 flush 以免 React 开发模式警告packages/react-virtual/src/index.tsx。启用directDomUpdates有三条硬性前提源码注释明确列出虚拟项必须position: absolute且主轴位置top/left或transform交给虚拟化器写入内部尺寸容器必须挂containerRef且不得自行设置height/width多车道网格/瀑布流时交叉轴位置如left: (lane * 100) / lanes%仍需在 JSX 中保留。该开关应在挂载时一次性设定运行期切换可能残留过期内联样式。示例中的 RowVirtualizerDynamic 正是按这套约定书写的教科书式用法。九、测试验证动态尺寸在测试中的表现仓库测试 packages/react-virtual/tests/index.test.tsx 直接覆盖了本示例的核心行为should render given dynamic sizeL136-L137渲染itemSize{100}dynamic的列表并断言行为estimateSize: () 50与实测itemSize的差异正是动态测量要校正的对象测试把measureElement作为 ref 传入测试代码 L72-L94并注解说「measureElement作为 ref 会在 React commit 阶段被调用」这解释了适配器中measuringFromRef标记与flushSync绕行的设计动机L195-L202 还有配合initialOffset的动态列表测试。这说明「估算值 ≠ 实测值」正是被测的核心契约无论估算多不准最终布局必须以实测为准。十、小结通过 examples/react/dynamic 这份示例可以提炼出动态尺寸虚拟化的完整心法估算先行estimateSize只为首帧与未测量项兜底不追求准确实测覆盖ref{virtualizer.measureElement} ResizeObserver 让尺寸实时收敛到真实值定位校正scrollToIndex走同步测量路径确保跳转目标基于最新尺寸按需优化容器滚动用useVirtualizercontain: strict页面滚动用useWindowVirtualizerscrollMargin追求极致滚动帧率再开directDomUpdates组合复用行虚拟化器与列虚拟化器可自由组合成网格before/after占位块负责对齐。更多框架适配Vue、Solid、Svelte 等与完整 API 说明可继续阅读 docs/framework/react/react-virtual.md 与 packages/virtual-core/src/index.ts 中的类型定义与注释。赞分享前端UI组件【免费下载链接】virtual Headless UI for Virtualizing Large Element Lists in JS/TS, React, Solid, Vue and Svelte项目地址https://gitcode.com/gh_mirrors/vi/virtual点击查看免费下载相关推荐TanStack Virtual 的 Lit 适配器实战固定尺寸行/列/网格虚拟化示例深度解析TanStack Virtual 的 Lit 适配器实战固定尺寸行/列/网格虚拟化示例深度解析 本文基于本仓库中的 Lit 固定尺寸fixed示例讲解如前端UI组件Angular 动态尺寸列表虚拟化实战tanstack/angular-virtual 官方示例的运行、构建与源码剖析Angular 动态尺寸列表虚拟化实战tanstack/angular virtual 官方示例的运行、构建与源码剖析 本文围绕仓库中的 Angular 动前端UI组件Angular 固定尺寸虚拟滚动实战基于 tanstack/angular-virtual 的行、列与网格示例Angular 固定尺寸虚拟滚动实战基于 tanstack/angular virtual 的行、列与网格示例 导读 本指南以仓库中的 Angular fi前端UI组件上一篇3步完整解决方案突破Hyper-V虚拟化性能瓶颈下一篇如何打造个人数字记忆库WeChatMsg数据留存完全指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询