在web前端开发中,需要渲染大量数据是很常见的需求。拿一般的业务系统来说,一个模块中往往需要显示成百上千条记录,这已经属于比较大的数据量。而一些大型系统,如数据分析平台、监控系统等,需要同时渲染的 数据量可能达到几十万甚至上百万。
面对大数据量渲染的需求,前端开发工程师就面临巨大的性能优化压力。如果直接渲染全部数据,页面会出现明显的卡顿甚至假死,严重影响用户体验。那么如何在实现功能的同时,保证页面的流畅和响应呢?这需要我们采取各种优化方案。
本文将围绕“给你10万条数据,请顺滑地渲染出来”这一需求展开,讨论前端性能优化的方方面面,以供同行参考。
在具体分析优化大数据量渲染方案之前,我们先来看看影响网页性能的一般因素有哪些。优化网页性能可以从多方面进行,统筹多种优化手段才能取得事半功倍的效果。
页面包含的资源数量越多,尤其是较大的资源,加载时间越长。减少请求数量可以有效优化页面性能。具体可以从以下几点着手:
(1)合理设置页面缓存,对不经常变动的资源启用缓存,避免每次加载都从服务器请求。
(2)资源按需加载,只在需要时加载。
(3)代码按功能拆分,避免把所有代码打包在一起。
(4)图片懒加载,按需异步加载。
(5)合理使用图片 Sprite。
(6)使用 HTTP/2,支持多路复用,更有效地使用连接。
压缩 JavaScript、CSS、图片等资源文件,减少文件体积,能明显减少加载时间。常用的压缩方法有:
(1)移除代码中的注释、调试信息。
(2)缩小变量名长度。
(3)删除不必要的空格、缩进。
(4)图片压缩,转换格式等。
CDN 网络覆盖广、访问速度快,使用 CDN 加载静态资源能显著提升加载速度。
启用 GZIP 压缩,能够减小文件体积,减轻服务器负载。
对 CSS、JS、图片等静态资源开启缓存,避免每次请求都访问服务器,可大幅提升重复加载速度。
使用 SSD 固态硬盘替代传统硬盘,能显著提升服务器响应速度。
使用如 Nginx、Varnish 等缓存代理,利用其缓存能力减少请求到达应用服务器。
合理设计数据库表结构,添加索引,优化查询语句,提升数据读取速度。
优化后端应用程序源码,减少不必要的计算和 I/O 操作。
综上所述,优化网页性能需要从网络资源、服务器端、应用程序等全方位进行优化,才能取得明显效果。接下来我们看看针对大数据量渲染的前端优化手段。
当需要前端渲染上万条以上数据时,常见的优化手段包括:
不要直接渲染全部数据,可以每次只渲染数据的一个分页,比如每页 100 条记录。当用户滚动到分页底部时,再异步加载下一页数据。
虚拟列表技术只渲染用户当前可见区域的数据,不渲染整个列表,复用 DOM 元素,避免大量 DOM 操作。例如 Vue 中的 vue-virtual-scroller 组件。
对滚动、输入等高频事件进行防抖处理,避免触发过多无意义的计算。
使用 keep-alive 缓存组件,避免重复渲染,提升切换效率。
对于需频繁更新的数据,避免直接操作 DOM,使用诸如 virtual DOM、dirty checking 等机制进行优化。
图片只在将要进入可视区域时才加载,减少不必要的提前加载。
优化过渡动画,避免动画过程中产生大量中间帧,导致卡顿。
在开发和生产环境监控页面性能,分析找到优化方向。
避免把所有代码打包在一起,按需异步导入,减少初始包体积。
使用服务端渲染,使得初始状态更快呈现,优化载入速度。
利用 Web Worker 进行计算任务,避免主线程被占用,影响页面响应。
启用 Tree Shaking,移除没有使用的代码,减少打包后的代码量。
根据以上的分析,下面我们可以给出一些具体的大数据量渲染优化方案:
这个优化点也是虚拟列表的核心 —— 只渲染用户当前可见的区域,不渲染整个巨大列表。
具体来说,我们可以在列表容器添加滚动监听,当滚动结束后,计算出当前可见区域的起始和结束索引,然后只渲染这个截断后的范围,复用之前的 DOM 元素。
这样就避免了大量 DOM 操作和不必要的渲染工作,优化性能。
并不需要将全部数据一次性加载完成,可以按分页逐步加载。一开始只加载第一页数据,当用户滚动到底部时,再加载下一页数据。
前端向服务器请求下一页数据时,传递下一页的页码,服务器根据页码返回对应数据。
分页加载可以分担服务器一次性承受巨大请求的压力,也减少了前端渲染全部数据的性能损耗。
使用 vue-router 的路由缓存功能,对组件进行缓存,避免每次路由切换都重新渲染:
const router = new VueRouter({
routes: [...],
scrollBehavior (to, from, savedPosition) {
// 始终滚动到顶部
return { x: 0, y: 0 }
}
})
然后在router-view处使用 keep-alive 包裹:
<keep-alive>
<router-view />
keep-alive>
这样就可以利用缓存提高页面切换效率。
使用图片懒加载,只有当图片进入可视区域时才加载。常用的实现方式是先使用一张低质量占位图,当滚动到可视区域时再加载真实图片。
这样可以减少页面初始化时加载大量图片产生的带宽和计算pressure。
监听长列表的滚动事件,分析触发频率:
let lastScrollTime = 0
list.addEventListener('scroll', event => {
const now = Date.now()
// 使用防抖,100ms内只记录一次
if (now - lastScrollTime > 100) {
lastScrollTime = now
// 对滚动事件进行采样分析
}
})
如果监测到一定时间内滚动事件过于频繁,可能表示出现了界面卡顿,可以及时定位问题所在。
结合前面的动态渲染可视区域和分页加载概念,我们可以进一步实现一个虚拟列表组件:
// VirtualList.js
data() {
return {
rawList: [], // 原始数据
visibleData: [] // 当前渲染的列表项数据
}
},
// 当滚动时计算 visibleData
calculateVisibleData() {
// ..
},
render() {
return (
<div ref="container" onScroll={this.handleScroll}>
{this.visibleData.map(item => (
<Item>{item}</Item>
))}
</div>
)
}
使用该组件时,只需要渲染 container,滚动时组件内部实现优化,大大减少 DOM 操作。
对于含大量图表的页面,我们也可以只渲染当前可视区域的图表,其他图表用一个 div 占位。当滚动到某个占位图表时,再实际渲染该图表。
图表组件可以这样实现:
data() {
return {
placeholder: <div className="placeholder" />
}
},
checkInView() {
if (inViewport(this)) {
// 触发真实渲染
} else {
return this.placeholder;
}
}
render() {
return this.checkInView();
}
这样只渲染当前可见的图表,避免大量图表绘制工作占用资源。
对于特别复杂的图表,我们甚至可以只渲染简化的低像素占位预览图,真正渲染高清图表只有当图表可见时才进行。
const preview = renderLowResChart(); // 绘制简化预览图
document.addEventListener('scroll', () => {
const chart = findVisibleChart();
if (chart) {
// 渲染高清图表
}
});
这种策略可以极大减少初始化图表绘制时间。
对于超大数据量的表格,我们也可以进行分块渲染:
let renderedRows = 0;
function renderMoreRows() {
// 每次渲染 20 条
for (let i = renderedRows; i < renderedRows + 20; i++) {
drawRow(data[i])
}
renderedRows += 20;
}
document.addEventListener('scroll', () => {
if (nearBottom()) {
renderMoreRows();
}
})
按需分块渲染可大幅提升超大表格的流畅性。
大型页面可以把复杂组件按区域划分,每个区域渲染可以独立进行,互不影响。
比如 conversataional UI 中,可以把对话内容和使用信息分别包裹在不同容器内:
<div class="chat-area">
div>
<div class="info-area">
div>
两个区域互不影响,拆分成两个较小的渲染作用域。
类似的,我们可以把页面分成头部、内容区、侧边栏等区块进行渲染,提升整体效率。
优化是持续的工作,我们需要监控应用在生产环境中的实际性能表现,以便获知优化效果和发现问题。
FPS(每秒帧数)是衡量界面的流畅程度的主要指标。FPS 过低通常会让用户感觉到界面卡顿。
我们可以通过 requestAnimationFrame 进行 FPS 监控:
let fps = 0;
let lastUpdate = Date.now();
function tick() {
// 每秒更新一次 FPS
if (Date.now() - lastUpdate >= 1000) {
console.log(fps); // 打印出每秒 FPS
fps = 0;
lastUpdate = Date.now();
}
fps++;
requestAnimationFrame(tick);
}
观察 FPS 的变化情况,当出现大幅波动时可以探查原因。
主线程空闲时间可以看出主线程是否过于繁忙,导致响应卡顿。
使用 Performance API 可以获取该指标:
// 每秒采样一次
setInterval(() => {
const idleTime = performance.getIdleTime();
console.log(idleTime);
}, 1000);
如果空闲时间持续过低,表示存在优化空间。
长任务也会占用主线程时间片,导致干扰响应。
可以通过监听长任务事件进行分析:
const observer = new PerformanceObserver(list => {
list.getEntries().forEach(entry => {
// entry.attribution 包含了长任务的信息
console.log(entry.attribution);
})
});
observer.observe({ entryTypes: ['longtask'] });
常见的长任务包括复杂计算、大数据量渲染等。
使用模拟工具模拟 2G、3G 等慢网络环境,观察应用性能表现是否存在问题。
使用工具模拟不同的负载情况,观察应用的承载能力上限。
本文围绕大数据量渲染的性能优化进行了比较全面的讨论。实现高效渲染需要从页面整体出发,进行深入理解和细致优化。主要手段包括:
实际开发中,需要根据具体场景进行技术选型和权衡利弊。本文提供的思路和方法供同行参考借鉴,也欢迎与大家进行更深入探讨与交流,让我们共同进步!