前端虚拟列表深度拆解

虚拟列表是为了解决什么问题

真实项目中的痛点:

想象一个后台系统:用户列表:10 万条;订单列表:20 万条;日志列表:百万级;表格里还有:多列、复杂 DOM、hover、操作按钮、状态标签

直接 map 渲染:

data.map(item => <Row key={item.id} />)

会遇到:首次渲染卡死、滚动严重掉帧、内存暴涨和浏览器直接崩

根因只有一个:DOM 太多浏览器不是怕 JS,浏览器最怕的是成千上万个 DOM 节点

总的来说虚拟列表就是只渲染可视区域内的列表项,而其余的用占位高度“假装存在”

虚拟列表的核心思想

我总结主要要理解这四点:
1.可视区域(viewport):屏幕当前能看到的那一段高度

2.列表总高度(total height):假设所有 item 都渲染后的总高度(假的,但要算出来)

3.起始索引和结束索引:根据滚动距离,计算现在应该显示哪几条

4. 偏移量(offset / translateY):让当前渲染的 items 看起来在正确的位置

虚拟列表的本质实现原理

假设每一项高度固定,这个是最简单的,真是项目中大量列表数据一般都是每一项数据都是高度固定的

itemHeight = 50px 容器高度 = 500px

那屏幕最多能显示:500 / 50 = 10 条

通常会多渲染几条(缓冲区):实际渲染 = 10 + 4 = 14 条

根据滚动距离算索引

startIndex = Math.floor(scrollTop / itemHeight) endIndex = startIndex + visibleCount

不渲染所有 DOM,但要让滚动条是对的

<div> <div></div> <!-- 撑开高度 --> <div></div> <!-- 只放可见项 --> </div>
.phantom { height: totalCount * itemHeight; }

偏移当前渲染区域

offsetY = startIndex * itemHeight
.list { transform: translateY(offsetY); }

视觉效果:DOM 只有十几条、滚动条像真的有十万条

真实项目React中使用demo整体代码(可直接使用)

import React, { useRef, useState, useEffect, useMemo,useCallback } from 'react' interface VirtualListProps<T> { data: T[] height: number // 容器高度 itemHeight: number // 每一项高度(固定) renderItem: (item: T, index: number) => React.ReactNode buffer?: number // 缓冲区 } export function VirtualList<T>({ data, height, itemHeight, renderItem, buffer = 5, }: VirtualListProps<T>) { const containerRef = useRef<HTMLDivElement>(null) const [scrollTop, setScrollTop] = useState(0) /** 可视区能显示的条数 */ const visibleCount = Math.ceil(height / itemHeight) /** 开始索引 */ const startIndex = Math.max( Math.floor(scrollTop / itemHeight) - buffer, 0 ) /** 结束索引 */ const endIndex = Math.min( startIndex + visibleCount + buffer * 2, data.length ) /** 当前渲染的数据 */ const visibleData = useMemo( () => data.slice(startIndex, endIndex), [data, startIndex, endIndex] ) /** 偏移量 */ const offsetY = startIndex * itemHeight /** 总高度(关键:撑开滚动条) */ const totalHeight = data.length * itemHeight /** 滚动事件 */ const onScroll = useCallback(() => { if (!containerRef.current) return setScrollTop(containerRef.current.scrollTop) }, []) return ( <div ref={containerRef} style={{ height, overflowY: 'auto', position: 'relative', border: '1px solid #ddd', }} onScroll={onScroll} > {/* 撑开高度 */} <div style={{ height: totalHeight }} /> {/* 实际渲染内容 */} <div style={{ position: 'absolute', top: 0, left: 0, right: 0, transform: `translateY(${offsetY}px)`, }} > {visibleData.map((item, index) => ( <div key={startIndex + index} style={{ height: itemHeight, boxSizing: 'border-box', borderBottom: '1px solid #eee', }} > {renderItem(item, startIndex + index)} </div> ))} </div> </div> ) }

如何使用这个 VirtualList

const data = Array.from({ length: 100000 }, (_, i) => `Row ${i}`) export default function App() { return ( <VirtualList data={data} height={500} itemHeight={50} renderItem={(item) => <div>{item}</div>} /> ) }

针对这个demo,我总结出几个关键点

1.下面这行代码,不显示任何内容,只是为了撑开滚动条

<div style={{ height: totalHeight }} />

2.为什么 content 要 absolute + translateY?因为transform不会出发布局重排,性能比设置top好

transform: translateY(offsetY)

3.buffer 的意义,是防止滚动过快出现白屏,提前渲染上下几条

buffer = 5

4.为什么 key 用startIndex + index因为:同一条数据在不同 scrollTop 下会复用 DOM,key 必须全局唯一

可以优化的地方:可以对scroll进行一个节流处理

用 ref 记录 RAF 状态

const rafIdRef = useRef<number | null>(null)

改变 onScroll函数

const onScroll = useCallback(() => { if (!containerRef.current) return // 如果当前帧已经有任务了,直接 return if (rafIdRef.current !== null) return rafIdRef.current = requestAnimationFrame(() => { setScrollTop(containerRef.current!.scrollTop) rafIdRef.current = null }) }, [])

Read more

从零开始:Xilinx FPGA实现RISC-V五级流水线CPU手把手教程

从一块FPGA开始,亲手造一颗CPU:RISC-V五级流水线实战全记录 你还记得第一次点亮LED时的兴奋吗?那种“我真正控制了硬件”的感觉,让人上瘾。但如果你能 自己设计一颗处理器 ,让它跑起第一条指令——那才是数字世界的终极浪漫。 今天,我们就来做这件“疯狂”的事:在一块Xilinx FPGA上,用Verilog从零实现一个 完整的RISC-V五级流水线CPU 。不是调用IP核,不是简化版demo,而是包含取指、译码、执行、访存、写回五大阶段,并解决真实数据冒险与控制冒险的可运行核心。 这不仅是一次教学实验,更是一场对计算机本质的深度探索。 为什么是 RISC-V + FPGA? 别误会,我们不是为了赶潮流才选RISC-V。恰恰相反,它是目前最适合学习CPU设计的指令集。 * 开放免费 :没有授权费,文档齐全,连寄存器编码都写得明明白白。 * 简洁清晰 :RV32I只有40多条指令,没有x86那样层层嵌套的历史包袱。 * 模块化扩展 :基础整数指令够用,后续想加浮点、压缩指令、向量扩展,都可以一步步来。

Cesium 无人机智能航线规划:航点动作组与AI识别实战

1. 从“点”到“任务”:理解智能航线规划的核心 如果你用过一些基础的无人机航线规划工具,可能觉得“不就是在地图上点几个点,连成线让飞机飞过去”吗?确实,早期的航点飞行就是这么简单。但当你真正投入到巡检、测绘、安防这类复杂任务时,你会发现,单纯的“点对点”飞行远远不够。 想象一下电力巡检的场景:无人机飞到第3号铁塔时,需要悬停、调整云台角度对准绝缘子串拍照;飞到第5号铁塔时,需要切换变焦镜头拍摄细节;在跨越河流的航线段,需要启动AI识别算法,自动监测河道漂浮物。这就不再是一条简单的“线”,而是一个由航点、动作、智能决策共同构成的三维空间任务流。 这就是Cesium在无人机应用开发中的独特价值。它不仅仅是一个三维地球可视化库,更是一个强大的空间任务编排平台。基于Cesium,我们可以将地理空间坐标(航点)与丰富的动作指令(Action) 以及AI识别逻辑绑定在一起,生成一个无人机能读懂、可执行的复杂任务剧本。 我刚开始做这类项目时,也走过弯路,以为把航线画漂亮就行了。结果真机测试时,要么动作没执行,

飞书机器人实战:5分钟搞定图片消息发送(含常见报错解决方案)

飞书机器人实战:5分钟搞定图片消息发送(含常见报错解决方案) 你是否遇到过这样的场景:服务器监控系统捕捉到一个异常峰值,你希望它能自动将一张清晰的图表截图,直接推送到团队的飞书群里,而不是一封冰冷的邮件;或者,你的自动化日报系统生成了精美的数据可视化图片,你希望它能无缝地出现在每日的晨会通知中。对于许多开发者和运维工程师来说,将图片消息集成到自动化流程中,是一个能极大提升信息传达效率和体验的“刚需”。 飞书机器人提供了强大的消息推送能力,但初次接触其图片消息发送功能时,你可能会发现它比预想的要“曲折”一些——它不像发送文本那样直接丢一个图片链接就行,而是需要经过一个“上传-获取密钥-发送”的流程。这个过程里,权限配置、tenant_access_token获取、图片上传格式、image_key的使用,每一步都可能藏着一个小坑。别担心,这篇文章就是为你准备的“避坑指南”。我们将抛开官方文档那略显冰冷的步骤罗列,从一个实战者的角度,带你用大约5分钟的时间,彻底打通从零到一发送飞书图片消息的全链路,并重点剖析那些你可能马上就会遇到的报错及其根因解决方案。我们的目标是:让你看完就能用,用了

深入解析OpenClaw Skills:从原理到实战,打造专属机器人技能

深入解析OpenClaw Skills:从原理到实战,打造专属机器人技能

一、OpenClaw Skills:机器人行为的“最小执行单元” 1.1 什么是OpenClaw Skills? OpenClaw是面向开源机械爪/小型机器人的控制框架(核心仓库:openclaw/openclaw),旨在降低机器人行为开发的门槛。而Skills(技能) 是OpenClaw框架中对机器人“单一可执行行为”的封装模块——它将机器人完成某一特定动作的逻辑(如“夹取物体”“释放物体”“移动到指定坐标”)抽象为独立、可复用、可组合的代码单元。 简单来说: * 粒度:一个Skill对应一个“原子行为”(如“单指闭合”)或“组合行为”(如“夹取→移动→释放”); * 特性:跨硬件兼容(适配不同型号机械爪)、可插拔(直接集成到OpenClaw主框架)、可扩展(支持自定义参数); * 核心价值:避免重复开发,让开发者聚焦“