稀土掘金技术社区 2024年11月23日
面试被问到如何一次性渲染十万条数据,我该怎么答?
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文介绍前端开发中渲染大量数据的解决方案,包括时间分片和虚拟列表技术,还提及懒加载、Web Workers等优化方法,以提高页面性能和用户体验。

🎯时间分片核心是分解大任务,用setTimeout或requestAnimationFrame分批次渲染数据

📋用requestAnimationFrame取代setTimeout可解决事件循环与屏幕刷新不同步问题

💻虚拟列表技术只渲染当前可见区域数据,提高性能,包括初始化、计算、渲染等步骤

📈还提到懒加载、Web Workers等其他优化方法

原创 今天一定晴q 2024-11-23 09:02 重庆

点击关注公众号,“技术干货” 及时达!

在前端开发中,性能优化是一个永恒的话题。我们经常需要处理和展示大量的数据,所以当面试官问到:“如何一次性渲染十万条数据而不影响用户体验?”你会怎么回答?直接渲染十万条数据可能会导致页面卡顿、响应迟缓,甚至浏览器崩溃。本篇文章详细介绍时间分片和虚拟列表的解决方案,帮助你轻松拿下面试~

js是单线程的,会有一个同步和异步的概念,为了确保主线程不会被长时间阻塞,js引擎就会依照「事件循环机制」来执行代码:

    先执行同步代码(也属于是宏任务)

    同步执行完毕后,检查是否有异步代码需要执行

    执行所有的微任务

    微任务执行完毕后,若有需要就会渲染页面

    执行宏任务(也就是下一次事件循环开始)

v8引擎执行 js 代码速度很快,然而渲染页面时间相对来说要长很多。如果直接将十万条数据给到渲染引擎,很容易造成页面卡顿或白屏,所以一次性渲染十万条数据的关键在于——要让浏览器的渲染线程尽量均匀流畅地将数据渲染上去。

时间分片的核心思想是将一个大的任务分解成多个小的任务,使用  setTimeoutrequestAnimationFrame 分批次地渲染一部分数据。

使用 setTimeout

    「初始化」:定义总数据条数 total、每次渲染的数据条数 once、需要渲染的总次数 page 和当前渲染的索引 index

    「递归渲染」loop 函数通过递归调用来逐步渲染数据。每次for循环渲染 once 条数据,并使用 setTimeout 将渲染操作放入下一个事件循环中。

    「定时器」setTimeout 确保每次渲染操作不会阻塞主线程,从而保持页面的流畅性和响应性。

    「结束条件」:当所有数据都渲染完毕后(即 curTotal - pageCount <= 0),递归调用停止。


<!---->
<body> <ul id="container"></ul>
<script> let ul = document.getElementById('container'); const total = 100000 // 总数据条数 let once = 20 // 每次渲染条数 let page = total / once // 需要渲染的总次数 let index = 0 // 每条记录的索引,防止数据丢失或没有渲染到最后一条
// 两个参数:剩余需要渲染的数据条数,当前渲染的索引 function loop(curTotal, curIndex) { let pageCount = Math.min(once, curTotal)
setTimeout(() => { for (let i = 0; i < pageCount; i++) { let li = document.createElement('li'); li.innerText = curIndex + i + ':' + ~~(Math.random() * total); ul.appendChild(li); } loop(curTotal - pageCount, curIndex + pageCount) }) } loop(total, index)
</script> </body>

不让v8一次事件循环就把js部分执行掉,浏览器一次性暴力渲染十万条,而是让v8执行五千次事件循环,浏览器每次只渲染二十条。这样v8分摊了浏览器渲染线程的压力,能减少页面加载时间

requestAnimationFrame

使用 setTimeout 将渲染操作放入下一个事件循环会有个小问题:假设浏览器页面刷新时间为 16.7ms,如果v8引擎的性能不够高,进行完一次事件循环的时间比 16.7ms 要长,那么浏览器在 16.7ms 内渲染完了 20 条数据,而 v8引擎还没将下一个20条数据给出来,这样就很可能会造成页面的闪屏或卡顿。

要解决定时器带来的事件循环与屏幕刷新不同步的问题,我们可以用 requestAnimationFrame() 取代 setTimeout()

在此基础上,我们需要尽量人为地减少回流重绘次数。如果每一次for循环都渲染一条数据,那这样高频地操作DOM会浪费开销影响性能。

所以,我们可以使用文档碎片 document.createDocumentFragment()——一个虚拟的DOM, 来批量插入 li 元素。使得生成好一条数据后,先不要往 ul 里面添加,固定每20个li只回流一次


<body> <ul id="container"></ul>
<script> let ul = document.getElementById('container'); const total = 100000 let once = 20 let page = total / once let index = 0
function loop(curTotal, curIndex) { let pageCount = Math.min(once, curTotal)
requestAnimationFrame(() => { // 创建一个文档碎片,是一个虚拟的DOM结构 let fragment = document.createDocumentFragment();
for (let i = 0; i < pageCount; i++) { let li = document.createElement('li'); li.innerText = curIndex + i + ':' + ~~(Math.random() * total); fragment.appendChild(li); } // 固定每20个li只回流一次 ul.appendChild(fragment);
loop(curTotal - pageCount, curIndex + pageCount) }) } loop(total, index) </script> </body>

虚拟列表技术通过「只渲染当前可见区域的数据」来提高性能,而不是一次性渲染所有数据。这样可以显著减少 DOM 元素的数量,从而提高页面的加载速度和滚动流畅性。

核心思想

    「初始化容器和数据」:创建固定高度的容器,准备数据源。

    「计算可视区域」:获取容器高度,计算每个项的高度和可视区域的数据条数。

    「渲染初始可见区域」:计算起始和结束索引,渲染初始数据。

    「监听滚动事件」:绑定滚动事件,计算新的起始和结束索引,更新渲染数据。

    「调整样式」:计算偏移量,处理实际列表跟随父容器一起移动的情况

接下来用 vue 技术栈展示虚拟列表实现步骤:

根组件App.vue中:


<!---->
<template> <div class="app"> <virtualList :listData="data" /> </div> </template>
<script setup> import { ref } from 'vue'; import virtualList from './components/virtualList.vue';
// 数据源 const data = ref([])
for(let i = 0; i < 1000; i++) { data.value.push({id: i, value: i}) }
</script>
<style lang="css" scoped> .app{ width: 300px; height: 400px; border: 1px solid #000; } </style>

自定义virtualList组件中:

「模板部分:」


<!---->
<template> <div ref="listRef" class="infinite-list-container" @scroll="scrollEvent()"> <div class="infinite-list-phantom" :style="{height: listHeight + 'px'}"></div>
<div class="infinite-list" :style="{transform: getTransform}"> <div class="infinite-list-item" v-for="item in visibleData" :key="item.id" :style="{height: itemSize + 'px', lineHeight: itemSize + 'px'}" > {{ item.value }} </div> </div> </div> </template>

「脚本及样式部分:」

  <script setup>    import { onMounted, reactive, ref, computed } from 'vue';    const props = defineProps({        listData: [],        // 每个item的高度        itemSize: {            type: Number,            default: 50        }    })
const state = reactive({ screenHeight: 0, // 可视区域的高度 startOffset: 0, // 偏移量 start: 0, // 起始数据下标 end: 0 // 结束数据下标 })
// 可视区域显示的数据条数 const visibleCount = computed(() => { return state.screenHeight / props.itemSize })
// 可视区域显示的真实数据 const visibleData = computed(() => { return props.listData.slice(state.start, Math.min(props.listData.length, state.end)) })
// 当前列表总高度 const listHeight = computed(() => { return props.listData.length * props.itemSize })
// list跟着父容器移动了,现在列表要移回来 const getTransform = computed(() => { return `translateY(${state.startOffset}px)` })

// 获取可视区域的高度 const listRef = ref(null) onMounted(() => { state.screenHeight = listRef.value.clientHeight state.end = state.start + visibleCount.value })

const scrollEvent = () => { let scrollTop = listRef.value.scrollTop state.start = Math.floor(scrollTop / props.itemSize) state.end = state.start + visibleCount.value state.startOffset = scrollTop - (scrollTop % props.itemSize) }
</script>
<style lang="css" scoped> .infinite-list-container { height: 100%; overflow: auto; position: relative; -webkit-overflow-scrolling: touch; /* 启用触摸滚动 */ } .infinite-list-phantom{ position: absolute; left: 0; top: 0; right: 0; z-index: -1; /* 置于背景层 **/ } .infinite-list{ position: absolute; left: 0; top: 0; right: 0; text-align: center; } .infinite-list-item { border-bottom: 1px, solid, #000; box-sizing: border-box; }
</style>

「效果:」

除了以上这两种方法,还可以采用懒加载、Web Workers等方法对渲染大量数据的操作进行优化,希望对你有所帮助。

              (都看到这了,点赞收藏一下再走吧~)


点击关注公众号,“技术干货” 及时达!

阅读原文

跳转微信打开

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

前端性能优化 时间分片 虚拟列表 懒加载
相关文章