网站开发多语言烟台网站建设技术支持
2026/3/29 17:18:07 网站建设 项目流程
网站开发多语言,烟台网站建设技术支持,服务号开发,wordpress安装时数据库错误先问你一个问题你有没有想过:为什么淘宝能流畅展示几万件商品?为什么抖音能无限刷下去永远不卡?我刚学前端那会儿,特别天真地认为:浏览器这么强大,渲染个几千条数据应该没问题吧?于是我写了这样的代码:// 年少无知的我 const data await fetch(/api/products?…先问你一个问题你有没有想过:为什么淘宝能流畅展示几万件商品?为什么抖音能无限刷下去永远不卡?我刚学前端那会儿,特别天真地认为:浏览器这么强大,渲染个几千条数据应该没问题吧?于是我写了这样的代码:// 年少无知的我 const data await fetch(/api/products?limit5000); data.forEach(item { const div document.createElement(div); div.innerHTML h3${item.title}/h3...; container.appendChild(div); });结果上线后,用户疯狂投诉:你们网站是不是挂了?卡得要死!我这才意识到:浏览器不是万能的!浏览器的性能极限在哪?让我用实测数据告诉你真相:DOM节点数量 渲染时间 用户体验 ───────────────────────────────────── 100个 ~10ms ✅ 丝滑流畅 500个 ~50ms ✅ 能接受 1000个 ~200ms ⚠️ 开始卡顿 3000个 ~800ms ❌ 明显延迟 5000个 ~2s ❌ 基本卡死 10000个 崩溃 直接白屏为什么会这样?每次你往页面添加DOM元素,浏览器都要做三件事:重排(Reflow)- 计算元素位置和尺寸重绘(Repaint)- 绘制元素到屏幕上合成(Composite)- 处理图层叠加DOM越多,这三步的计算量就呈指数级增长!那怎么办?大厂们用了两招:第一招:无限滚动(先解决加载问题)用一个外卖场景理解想象你点了100份外卖(别问为什么这么多)。笨方法:外卖小哥一次性把100份堆你门口 → 你家门口爆了!聪明方法:先送10份,你快吃完了再送下一批 → 完美!无限滚动就是这个思路:滚到哪里,加载到哪里。滚动触发的原理用户视角: ┌──────────────┐ │ 商品1 │ ← 用户正在看 │ 商品2 │ │ 商品3 │ │ 商品4 │ │ 商品5 │ └──────────────┘ ↓ 继续往下滚 ┌──────────────┐ │ 商品3 │ │ 商品4 │ │ 商品5 │ │ 商品6 │ │ 商品7 │ │ 商品8 │ │ 商品9 │ │ 商品10 │ ← 快到底了! └──────────────┘ ↓ 触发加载 ┌──────────────┐ │ 商品9 │ │ 商品10 │ │ 商品11 │ ← 新加载的! │ 商品12 │ ← 新加载的! │ 商品13 │ ← 新加载的! └──────────────┘代码实现(超简单版)const container document.getElementById(container); let page 1; let isLoading false; // 防止重复加载 function loadItems() { if (isLoading) return; isLoading true; // 从美团API获取商品 fetch(https://api.meituan.com/products?page${page}limit20) .then(response response.json()) .then(data { data.forEach(item { const div document.createElement(div); div.innerHTML img src${item.image} / h3${item.title}/h3 p¥${item.price}/p ; container.appendChild(div); }); page; isLoading false; }); } // 核心:监听滚动 container.addEventListener(scroll, () { // 三个关键变量 const scrollTop container.scrollTop; // 已滚动的距离 const clientHeight container.clientHeight; // 可见区域高度 const scrollHeight container.scrollHeight; // 总内容高度 // 快到底部了,开始加载! if (scrollTop clientHeight scrollHeight - 100) { loadItems(); } }); // 首次加载 loadItems();看懂了吗?关键是这个判断:scrollTop clientHeight scrollHeight - 100 // 翻译成人话: // 如果(已滚动距离 可见高度) (总高度 - 100px) // 说明快到底了,该加载新数据了!但这招有个致命缺陷!用户滚动了100次,你就加载了2000条数据。这2000个DOM一直在页面上!结果:页面越来越卡,最终还是会崩溃。怎么办?这就需要第二招了!第二招:虚拟滚动(解决渲染问题)先理解一个反直觉的事实用户同时能看到的商品,最多也就十几个!你手机屏幕就这么高,假设每个商品占50px,屏幕高度600px:能同时看到的商品 600 ÷ 50 12个那为什么要渲染10000个DOM呢?太浪费了!酒店旋转门的比喻虚拟滚动就像酒店的旋转门:外面排队的人(数据): 10000人 旋转门里的人(DOM): 只有4个! ┌─────────────┐ │ 外面 │ ← 9996人在等待(没渲染) ├─────────────┤ │ 人1 │ ← 刚进来(刚渲染) │ 人2 │ ← 在门里(渲染中) │ 人3 │ ← 在门里(渲染中) │ 人4 │ ← 快出去了(即将销毁) ├─────────────┤ │ 里面 │ ← 已经进去的人(已销毁) └─────────────┘关键点:门里永远只有4个人(DOM)但外面看起来像有10000人在排队(滚动条很长)人在不断进出,但门里的人数永远固定虚拟滚动的三个核心组件在讲代码前,先理解虚拟滚动的三层结构:┌─────────────────────────────────┐ │ 外层容器(container) │ │ 作用:监听滚动事件 │ │ ┌───────────────────────────┐ │ │ │ 占位容器(scrollContent) │ │ │ │ 高度 总数据量 × 单项高度 │ │ │ │ 作用:撑开滚动条 │ │ │ │ ┌─────────────────────┐ │ │ │ │ │ 渲染容器(viewport) │ │ │ │ │ │ 作用:实际渲染DOM │ │ │ │ │ │ ┌─────────────────┐ │ │ │ │ │ │ │ Item 10 │ │ │ │ │ │ │ │ Item 11 │ │ │ │ │ │ │ │ Item 12 │ │ │ │ │ │ │ └─────────────────┘ │ │ │ │ │ └─────────────────────┘ │ │ │ └───────────────────────────┘ │ └─────────────────────────────────┘最简化的代码实现我先写一个最简版本,只保留核心逻辑:// 第一步:准备数据 const totalItems 10000; // 总共10000条数据 const itemHeight 50; // 每项高度50px const containerHeight 600; // 容器高度600px // 计算可见数量 600 ÷ 50 12项 const visibleCount Math.ceil(containerHeight / itemHeight); // 第二步:创建结构 const container document.getElementById(container); // 占位容器:撑开滚动条 const scrollContent document.createElement(div); scrollContent.style.height ${totalItems * itemHeight}px; // 10000 × 50 500000px scrollContent.style.position relative; container.appendChild(scrollContent); // 渲染容器:实际显示的DOM const viewport document.createElement(div); viewport.style.position absolute; viewport.style.top 0; viewport.style.left 0; viewport.style.right 0; scrollContent.appendChild(viewport); // 第三步:渲染函数 function render(startIndex) { // 清空旧的DOM viewport.innerHTML ; // 只渲染可见的12项 for (let i startIndex; i startIndex visibleCount; i) { if (i totalItems) break; // 防止越界 const item document.createElement(div); item.textContent 商品 ${i 1}; item.style.height ${itemHeight}px; item.style.position absolute; item.style.top ${i * itemHeight}px; // 设置正确的位置! viewport.appendChild(item); } } // 第四步:监听滚动 container.addEventListener(scroll, () { const scrollTop container.scrollTop; // 核心计算!根据滚动距离算出起始索引 const startIndex Math.floor(scrollTop / itemHeight); render(startIndex); }); // 首次渲染 render(0);初学者最大的疑问:向上滚动能正常显示吗?**这是个超级好的问题!**很多人第一次看虚拟滚动都会困惑:我向下滚动,老的DOM被删除了如果我再向上滚回去,还能看到之前的内容吗?答案:完全可以!让我详细解释原理:初始状态(scrollTop 0): ┌──────────────┐ │ 商品0 │ ← startIndex 0 │ 商品1 │ │ 商品2 │ │ ... │ │ 商品11 │ └──────────────┘ DOM: 渲染了0-11号商品 向下滚动到 scrollTop 500: ┌──────────────┐ │ 商品10 │ ← startIndex 500÷50 10 │ 商品11 │ │ 商品12 │ │ ... │ │ 商品21 │ └──────────────┘ DOM: 删除了0-9号,重新渲染10-21号 再向上滚回 scrollTop 200: ┌──────────────┐ │ 商品4 │ ← startIndex 200÷50 4 │ 商品5 │ │ 商品6 │ │ ... │ │ 商品15 │ └──────────────┘ DOM: 删除了10-21号,重新渲染4-15号看到了吗?秘密在这行代码:const startIndex Math.floor(scrollTop / itemHeight);无论你往哪个方向滚动:向下滚:scrollTop变大 →startIndex变大 → 渲染后面的项向上滚:scrollTop变小 →startIndex变小 → 渲染前面的项虚拟滚动不记忆之前渲染过什么,它只关心:当前滚动位置应该显示哪些项?用图示理解双向滚动完整数据(10000项): ┌─────────┐ │ Item 0 │ ← scrollTop0时显示 │ Item 1 │ │ ... │ │ Item 10 │ ← scrollTop500时显示 │ Item 11 │ │ ... │ │ Item 50 │ │ ... │ │ Item 99 │ │ ... │ │ Item 999│ └─────────┘ 滚动监听: scrollTop变化 → 实时计算startIndex → 重新render 就像一个移动的窗口: [窗口向下移动] ↓ ┌─────────────┐ │ │ │ ┌─────────┐ │ ← 窗口在这里,渲染Item 0-11 │ │ Item 0 │ │ │ │ Item 1 │ │ │ │ ... │ │ │ │ Item 11 │ │ │ └─────────┘ │ │ │ │ │ │ ┌─────────┐ │ ← 窗口移到这里,渲染Item 10-21 │ │ Item 10 │ │ │ │ Item 11 │ │ │ │ ... │ │ │ │ Item 21 │ │ │ └─────────┘ │ │ │ └─────────────┘ [窗口可以向上移动] ↑ 完全对称!完整版代码(带缓冲区)实际项目中,我们还要加上缓冲区,防止快速滚动时出现白屏:class VirtualScroller { constructor(container, totalItems, itemHeight) { this.container container; this.totalItems totalItems; this.itemHeight itemHeight; // 可见数量 this.visibleCount Math.ceil(container.clientHeight / itemHeight); // 缓冲区:前后各多渲染3项 this.bufferSize 3; this.startIndex 0; this.init(); } init() { // 占位容器 this.scrollContent document.createElement(div); this.scrollContent.style.height ${this.totalItems * this.itemHeight}px; this.scrollContent.style.position relative; this.container.appendChild(this.scrollContent); // 渲染容器 this.viewport document.createElement(div); this.viewport.style.position absolute; this.viewport.style.top 0; this.viewport.style.left 0; this.viewport.style.right 0; this.scrollContent.appendChild(this.viewport); // 监听滚动 this.container.addEventListener(scroll, () { this.handleScroll(); }); // 首次渲染 this.render(); } handleScroll() { const scrollTop this.container.scrollTop; // 计算起始索引 this.startIndex Math.floor(scrollTop / this.itemHeight); // 重新渲染 this.render(); } render() { // 计算实际渲染范围(包含缓冲区) const start Math.max(0, this.startIndex - this.bufferSize); const end Math.min( this.totalItems, this.startIndex this.visibleCount this.bufferSize ); // 清空并重新渲染 this.viewport.innerHTML ; const fragment document.createDocumentFragment(); for (let i start; i end; i) { const item document.createElement(div); item.textContent 商品 ${i 1}; item.style.height ${this.itemHeight}px; item.style.position absolute; item.style.top ${i * this.itemHeight}px; item.style.borderBottom 1px solid #ddd; fragment.appendChild(item); } this.viewport.appendChild(fragment); } } // 使用:10000条数据,只渲染十几个DOM! const scroller new VirtualScroller( document.getElementById(container), 10000, // 10000条数据 50 // 每项50px高度 );缓冲区的作用假设可见12项,缓冲区3项: 不加缓冲区: ┌─────────────┐ │ Item 10 │ ← 可见 │ Item 11 │ ← 可见 │ Item 12 │ ← 可见 │ ... │ │ Item 21 │ ← 可见 └─────────────┘ 快速滚动 → 立即白屏! ❌ 加了缓冲区: ┌─────────────┐ │ Item 7 │ ← 缓冲区(提前渲染) │ Item 8 │ ← 缓冲区 │ Item 9 │ ← 缓冲区 ├─────────────┤ │ Item 10 │ ← 可见区域开始 │ Item 11 │ │ ... │ │ Item 21 │ ← 可见区域结束 ├─────────────┤ │ Item 22 │ ← 缓冲区(提前渲染) │ Item 23 │ ← 缓冲区 │ Item 24 │ ← 缓冲区 └─────────────┘ 快速滚动 → 依然流畅! ✅性能对比:数据说话我在本地测试了三种方案(Chrome 120, MacBook Pro M1):**测试场景:**10000条商品数据┌──────────────┬──────────┬──────────┬──────────┐ │ 方案 │ 初始渲染 │ 滚动FPS │ 内存占用 │ ├──────────────┼──────────┼──────────┼──────────┤ │ 全部渲染 │ 2.3s │ 15fps │ 450MB │ │ 无限滚动 │ 0.15s │ 45fps │ 180MB │ │ 虚拟滚动 │ 0.08s │ 60fps │ 85MB │ └──────────────┴──────────┴──────────┴──────────┘结论:虚拟滚动快了28倍内存省了80%滚动达到满帧60fpsReact中的实战应用实际项目中,我们不需要手写,用现成的库:react-window(推荐)import { FixedSizeList } from react-window; function ProductList({ items }) { // 每一项的渲染函数 const Row ({ index, style }) ( div style{style} classNameproduct-item img src{items[index].image} / h3{items[index].title}/h3 p¥{items[index].price}/p /div ); return ( FixedSizeList height{600} // 容器高度 itemCount{10000} // 总数据量 itemSize{80} // 单项高度 width100% {Row} /FixedSizeList ); }就这么简单!react-window帮你处理了所有的滚动计算。无限滚动 虚拟滚动的组合拳最强方案:import { FixedSizeList } fromreact-window; import InfiniteLoader fromreact-window-infinite-loader; function InfiniteVirtualList() { const [items, setItems] useState([]); const [hasMore, setHasMore] useState(true); // 加载更多数据 const loadMore async () { const newItems await fetchData(); setItems(prev [...prev, ...newItems]); }; return ( InfiniteLoader isItemLoaded{index index items.length} itemCount{hasMore ? items.length 1 : items.length} loadMoreItems{loadMore} {({ onItemsRendered, ref }) ( FixedSizeList height{600} itemCount{items.length} itemSize{80} onItemsRendered{onItemsRendered} ref{ref} {Row} /FixedSizeList )} /InfiniteLoader ); }这套方案能支撑百万级数据!新手最容易踩的5个坑坑1:忘记设置容器高度// ❌ 错误:没有设置高度 div idcontainer !-- 虚拟滚动 -- /div // ✅ 正确:必须设置固定高度 div idcontainer styleheight: 600px; overflow-y: auto; !-- 虚拟滚动 -- /div**为什么?**因为虚拟滚动需要知道可见区域有多高!坑2:每项高度不一致虚拟滚动要求每项高度固定,但现实中商品标题有长有短:// ❌ 问题:高度不固定 div classitem h3这是一个很长很长很长的标题.../h3 /div // ✅ 解决方案1:限制高度 .item h3 { height: 60px; overflow: hidden; text-overflow: ellipsis; } // ✅ 解决方案2:使用react-window的VariableSizeList import { VariableSizeList } from react-window;坑3:图片懒加载冲突虚拟滚动会销毁DOM,普通的懒加载库可能失效:// ✅ 在Row组件内处理图片加载 const Row ({ index, style }) { const [loaded, setLoaded] useState(false); return ( div style{style} img src{loaded ? items[index].image : placeholder.jpg} onLoad{() setLoaded(true)} / /div ); };坑4:滚动位置丢失用户滚动到第1000项,刷新页面回到顶部,体验很差:// 保存滚动位置 window.addEventListener(beforeunload, () { sessionStorage.setItem(scrollTop, container.scrollTop); }); // 恢复滚动位置 window.addEventListener(load, () { const savedPosition sessionStorage.getItem(scrollTop); if (savedPosition) { container.scrollTop savedPosition; } });坑5:忘记清理事件监听// ❌ 会导致内存泄漏 container.addEventListener(scroll, handleScroll); // ✅ 组件销毁时清理 class VirtualScroller { destroy() { this.container.removeEventListener(scroll, this.handleScroll); } }写在最后:给初学者的建议虚拟滚动和无限滚动,看起来复杂,本质上就是两个简单的思想:无限滚动:别一次加载太多,分批加载虚拟滚动:别一次渲染太多,只渲染可见的学习路径建议:第1步: 理解为什么需要优化(DOM太多会卡) 第2步: 先学无限滚动(比较简单) 第3步: 理解虚拟滚动的核心概念(窗口偏移) 第4步: 自己实现一个最简版本(加深理解) 第5步: 在实际项目中使用成熟库(react-window)记住:小项目( 500项):不需要优化,直接渲染就行中型项目(500-2000项):用无限滚动大型项目( 2000项):用虚拟滚动超大项目( 10000项):无限滚动 虚拟滚动在字节、阿里、腾讯,这些技术已经是标配。淘宝的商品列表、抖音的视频流、飞书的文档,背后都有虚拟滚动的身影。掌握它,你就掌握了大厂的核心优化技术!如果这篇文章对你有帮助,欢迎关注《前端达人》,我会持续分享更多适合初学者的硬核前端技术!

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询