大数据渲染优化思考
1. 如何减少对应的DOM节点的内存销毁风险
此时的实践操作分为两类思考
1.1 使用基于虚拟列表渲染的机制来实现
1.2 使用基于 canvas 的渲染逻辑的思考
2. 如何提高大数据渲染的时候计算逻辑的优化
webworker 子线程的计算逻辑迁移
wasm 更加高性能的渲染逻辑封装
requestIdleCallback 的空闲时执行实现等
skia 引擎的实现使用吧
3. 如何提高前端的渲染的性能优化
1. 核心利用事件循环实现思考
2. 思考前端的渲染原理的优化思考
3. 思考得到如何提高对应的 GPU 加速的提高吧
4. 复杂数据的持久化方案的迁移
1. storage 的存储方案
2. 复杂数据的向 indexdb 迁移迁移
5. 性能指标量化数据的监控和总结
首次渲染的指标衡量:FP FirstPaint
滚动性能对比:核心就是测量 FPS 的实现吧
内存占用的思考
方案一的实现
使用第三方库来实现开发,比如:
react-window 库的使用
react-virtualized 库的使用
react-virtuoso 库的使用
react-virtual-list 库的使用
核心推荐使用的库是:react-widnow 来实现吧
但是核心需要注意的是库本身的底层也是基于原本的DOM 节点实现的渲染开发吧,也是存在大量的 DOM 阶段具备的一些问题,内存占用的问题,最后的方案思考有
可视区渲染的封装
分页逻辑的封装
核心底层的思考为
利用react的列表渲染实现数据的渲染实现吧
利用 hooks 来进行组件状态的存储和副作用的处理
分页思想来实现封装对应的分页逻辑的实现
排序算法来进行数组的排序实现吧
class PaginationManager {
constructor(total, pageSize, bufferPages = 2) {
this.total = total;
this.pageSize = pageSize;
this.bufferPages = bufferPages;
this.currentPage = 1;
this.totalPages = Math.ceil(total / pageSize);
}
// 计算当前应加载的页
getPagesToLoad(currentScrollPage) {
this.currentPage = currentScrollPage;
const startPage = Math.max(1, currentScrollPage - this.bufferPages);
const endPage = Math.min(this.totalPages, currentScrollPage + this.bufferPages);
return { startPage, endPage };
}
// 根据页码获取数据
async getPageData(page) {
const startIndex = (page - 1) * this.pageSize;
const endIndex = Math.min(startIndex + this.pageSize - 1, this.total - 1);
// 模拟从服务器获取数据
return this.fetchDataFromServer(startIndex, endIndex);
}
fetchDataFromServer(start, end) {
const data = [];
for (let i = start; i <= end; i++) {
data.push({
id: i,
name: `name_${i}`,
age: i % 100,
});
}
return Promise.resolve(data);
}
}class VirtualPagination {
constructor(options) {
this.total = options.total || 1000000;
this.pageSize = options.pageSize || 100;
this.bufferSize = options.bufferSize || 100;
this.rowHeight = options.rowHeight || 50;
// 按需加载的页面缓存
this.pageCache = new Map();
this.visibleRange = { start: 0, end: 0 };
}
// 核心算法:计算虚拟索引 -> 映射到分页数据
calculate(scrollTop, containerHeight) {
// 1. 计算虚拟行索引
const virtualStartRow = Math.floor(scrollTop / this.rowHeight);
const virtualEndRow = Math.ceil((scrollTop + containerHeight) / this.rowHeight);
// 2. 加上缓冲区
const bufferedStartRow = Math.max(0, virtualStartRow - this.bufferSize);
const bufferedEndRow = Math.min(
this.total - 1,
virtualEndRow + this.bufferSize
);
// 3. 映射到页面
const startPage = Math.floor(bufferedStartRow / this.pageSize) + 1;
const endPage = Math.floor(bufferedEndRow / this.pageSize) + 1;
// 4. 计算在页面内的偏移
const startOffset = bufferedStartRow % this.pageSize;
const endOffset = bufferedEndRow % this.pageSize;
return {
virtualRows: { start: bufferedStartRow, end: bufferedEndRow },
pages: { start: startPage, end: endPage },
offsets: { start: startOffset, end: endOffset },
visibleCount: bufferedEndRow - bufferedStartRow + 1
};
}
// 获取需要渲染的数据
getDataToRender(calcResult) {
const { pages, offsets } = calcResult;
const data = [];
// 加载需要的页面
for (let page = pages.start; page <= pages.end; page++) {
const pageData = this.loadPage(page);
// 确定当前页面哪些行需要显示
const startIdx = (page === pages.start) ? offsets.start : 0;
const endIdx = (page === pages.end) ? offsets.end : this.pageSize - 1;
for (let i = startIdx; i <= endIdx; i++) {
if (pageData[i]) {
data.push(pageData[i]);
}
}
}
return data;
}
loadPage(page) {
// 检查缓存
if (!this.pageCache.has(page)) {
const startIdx = (page - 1) * this.pageSize;
const pageData = [];
for (let i = 0; i < this.pageSize; i++) {
const idx = startIdx + i;
if (idx < this.total) {
pageData.push({
id: idx,
name: `name_${idx}`,
age: idx % 100,
});
}
}
this.pageCache.set(page, pageData);
// 简单的缓存清理(LRU策略)
if (this.pageCache.size > 10) {
const firstKey = this.pageCache.keys().next().value;
this.pageCache.delete(firstKey);
}
}
return this.pageCache.get(page);
}
}// 对大数据排序应该使用索引或分治
class SortManager {
constructor(dataSource) {
this.dataSource = dataSource; // 数据源
this.sortedIndexes = null; // 排序后的索引数组
this.sortColumns = []; // 排序列
this.sortDirections = []; // 排序方向
}
// 为大数据创建排序索引(只排序索引,不移动数据)
async createSortIndexes(sortConfigs) {
// 生成所有索引
const indexes = new Array(this.dataSource.total);
for (let i = 0; i < indexes.length; i++) {
indexes[i] = i;
}
// 使用Web Worker在后台排序
return this.sortInWorker(indexes, sortConfigs);
}
// 获取排序后的数据(按需)
getSortedData(startIndex, count) {
if (!this.sortedIndexes) {
// 未排序,返回原始顺序
return this.getOriginalData(startIndex, count);
}
const result = [];
for (let i = 0; i < count; i++) {
const sortedIdx = this.sortedIndexes[startIndex + i];
result.push(this.dataSource.getItem(sortedIdx));
}
return result;
}
getOriginalData(startIndex, count) {
const result = [];
for (let i = 0; i < count; i++) {
result.push(this.dataSource.getItem(startIndex + i));
}
return result;
}
}核心公式
定高的场景下的话
const startIndex = Math.floor(scrollTop / rowHeight); // 计算开始的下标吧
const endIndex = Math.floor((scrollTop + tableHeight) / rowHeight); // 计算结束的下表吧在计算过程中的优化是需要计算得出对应的缓冲区来实现的讷
const startIndex = Math.max(0, Math.floor(scrollTop / rowHeight - Buffer_Size));
const endIndex = Math.min(total, Math.floor((scrollTop + tableHeight) / rowHeight)) + Buffer_Size);不定高和不定宽的场景是
// 初始化一下吧
let rowPositions = [];
let totalHeight = 0;
// 开始进行计算实现吧
function initRowPositions(count, estimatedRowHeight) {
rowPositions = new Array(count);
let total = 0;
for (let i = 0; i < count; i++) {
rowPositions[i] = {
offset: total,
height: estimatedRowHeight,
};
total += estimatedRowHeight;
}
totalHeight = total;
}
// 更新某一行的实际高度
function updateRowHeight(index, actualHeight) {
const oldHeight = rowPositions[index].height;
if (oldHeight !== actualHeight) {
rowPositions[index].height = actualHeight;
// 重新计算后续行的偏移量
for (let i = index + 1; i < rowPositions.length; i++) {
rowPositions[i].offset = rowPositions[i-1].offset + rowPositions[i-1].height;
}
totalHeight = rowPositions[rowPositions.length-1].offset + rowPositions[rowPositions.length-1].height;
}
}
// 根据 scrollTop 和 clientHeight 计算可见行
function getVisibleRows(scrollTop, clientHeight) {
// 二分查找找到第一个偏移量大于等于 scrollTop 的行
let start = 0, end = rowPositions.length - 1;
while (start <= end) {
const mid = Math.floor((start + end) / 2);
if (rowPositions[mid].offset < scrollTop) {
start = mid + 1;
} else {
end = mid - 1;
}
}
const startIndex = Math.max(0, start - 1); // 因为可能不是完全匹配,取前一行为起始
// 从 startIndex 开始,找到第一个偏移量大于 scrollTop + clientHeight 的行
let endIndex = startIndex;
while (endIndex < rowPositions.length && rowPositions[endIndex].offset < scrollTop + clientHeight) {
endIndex++;
}
// 为了缓冲,可以扩展 startIndex 和 endIndex
const bufferedStartIndex = Math.max(0, startIndex - bufferRows);
const bufferedEndIndex = Math.min(rowPositions.length - 1, endIndex + bufferRows);
return { startIndex: bufferedStartIndex, endIndex: bufferedEndIndex };
}统计一下 FPS
衡量页面动画流畅度的指标,通常60fps为佳。
在虚拟滚动中,频繁的滚动事件和重排重绘可能导致FPS下降
使用
requestAnimationFrame来统计每秒内执行的次数
let frameCount = 0;
let lastTime = performance.now();
let fps = 60;
function checkFPS() {
const currentTime = performance.now();
frameCount++;
if (currentTime > lastTime + 1000) {
fps = Math.round((frameCount * 1000) / (currentTime - lastTime));
frameCount = 0;
lastTime = currentTime;
}
console.log(`当前FPS: ${fps}`);
requestAnimationFrame(checkFPS);
}
requestAnimationFrame(checkFPS);FP 性能指标的统计
表示页面首次绘制的时间点。
在虚拟滚动中,由于只渲染部分内容,FP可能会提前。
使用 PerformanceObserver 来监听 paint 事件。
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.name === 'first-paint') {
console.log(`FP: ${entry.startTime}`);
}
}
});
observer.observe({ entryTypes: ['paint'] });
class PerformanceMonitor {
constructor() {
// FPS监控
this.fpsStats = {
current: 0,
average: 0,
min: Infinity,
max: 0,
frames: [],
lastTime: performance.now(),
frameCount: 0,
};
// FP/FCP监控
this.paintMetrics = {
firstPaint: 0,
firstContentfulPaint: 0,
largestContentfulPaint: 0,
};
// 滚动性能
this.scrollMetrics = {
jankFrames: 0, // 掉帧数
scrollDuration: 0, // 滚动持续时间
scrollDistance: 0, // 滚动距离
velocity: 0, // 滚动速度
};
// 启动监控
this.initMonitoring();
}
// FPS监控实现
monitorFPS() {
const now = performance.now();
const delta = now - this.fpsStats.lastTime;
this.fpsStats.frameCount++;
// 每秒计算一次FPS
if (delta >= 1000) {
this.fpsStats.current = Math.round(
(this.fpsStats.frameCount * 1000) / delta
);
// 记录历史数据
this.fpsStats.frames.push(this.fpsStats.current);
if (this.fpsStats.frames.length > 60) {
this.fpsStats.frames.shift();
}
// 计算统计数据
this.fpsStats.average = Math.round(
this.fpsStats.frames.reduce((a, b) => a + b, 0) / this.fpsStats.frames.length
);
this.fpsStats.min = Math.min(this.fpsStats.min, this.fpsStats.current);
this.fpsStats.max = Math.max(this.fpsStats.max, this.fpsStats.current);
// 检测掉帧
if (this.fpsStats.current < 30) {
this.scrollMetrics.jankFrames++;
this.triggerWarning('LOW_FPS', this.fpsStats.current);
}
// 重置
this.fpsStats.frameCount = 0;
this.fpsStats.lastTime = now;
}
// 递归调用
requestAnimationFrame(() => this.monitorFPS());
}
// 首次绘制监控
monitorPaintTiming() {
// 使用PerformanceObserver API
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
switch(entry.name) {
case 'first-paint':
this.paintMetrics.firstPaint = entry.startTime;
break;
case 'first-contentful-paint':
this.paintMetrics.firstContentfulPaint = entry.startTime;
break;
case 'largest-contentful-paint':
this.paintMetrics.largestContentfulPaint = entry.startTime;
break;
}
}
});
observer.observe({ entryTypes: ['paint', 'largest-contentful-paint'] });
}
// 滚动性能监控
monitorScrollPerformance() {
let lastScrollTop = 0;
let lastScrollTime = 0;
let isScrolling = false;
let scrollStartTime = 0;
const onScroll = () => {
const currentTime = performance.now();
const currentScrollTop = window.scrollY;
// 滚动开始
if (!isScrolling) {
isScrolling = true;
scrollStartTime = currentTime;
}
// 计算滚动速度
const deltaTime = currentTime - lastScrollTime;
const deltaScroll = Math.abs(currentScrollTop - lastScrollTop);
if (deltaTime > 0) {
this.scrollMetrics.velocity = deltaScroll / deltaTime;
}
// 更新状态
lastScrollTop = currentScrollTop;
lastScrollTime = currentTime;
// 滚动结束检测
clearTimeout(this.scrollEndTimer);
this.scrollEndTimer = setTimeout(() => {
if (isScrolling) {
isScrolling = false;
this.scrollMetrics.scrollDuration = currentTime - scrollStartTime;
this.scrollMetrics.scrollDistance = Math.abs(currentScrollTop - scrollStartTop);
this.logScrollSession();
}
}, 100);
};
window.addEventListener('scroll', onScroll, { passive: true });
}
// 渲染性能分析
analyzeRenderPerformance() {
// 使用Performance Timeline API
const renderEntries = performance.getEntriesByType('measure');
return {
// 布局计算时间
layoutDuration: this.getMetric('layout', renderEntries),
// 样式计算时间
styleRecalcDuration: this.getMetric('style', renderEntries),
// 绘制时间
paintDuration: this.getMetric('paint', renderEntries),
// 合成时间
compositeDuration: this.getMetric('composite', renderEntries),
// JS执行时间
scriptDuration: this.getMetric('script', renderEntries),
};
}
// 生成性能报告
generateReport() {
return {
// FPS报告
fps: {
current: this.fpsStats.current,
average: this.fpsStats.average,
min: this.fpsStats.min,
max: this.fpsStats.max,
stability: this.calculateFPSStability(),
},
// 绘制时间报告
paint: this.paintMetrics,
// 滚动性能
scroll: {
...this.scrollMetrics,
smoothness: this.calculateScrollSmoothness(),
},
// 内存使用
memory: performance.memory ? {
usedJSHeapSize: performance.memory.usedJSHeapSize,
totalJSHeapSize: performance.memory.totalJSHeapSize,
} : null,
// 建议优化
recommendations: this.generateOptimizationSuggestions(),
};
}
}性能监控的核心步骤是
初始化监控器:创建监控实例,设置监控参数和阈值。
绑定监控事件:监听页面滚动、渲染、资源加载等事件。
收集性能数据:利用浏览器提供的Performance API和自定义的测量点收集数据。
分析数据:对收集到的数据进行分析,判断是否达到阈值,触发警告或记录。
输出报告:将分析结果以日志、控制台输出或发送到服务器的方式呈现。