https://bigdata-atw.pages.dev/method3

大数据渲染优化思考

  • 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和自定义的测量点收集数据。

    • 分析数据:对收集到的数据进行分析,判断是否达到阈值,触发警告或记录。

    • 输出报告:将分析结果以日志、控制台输出或发送到服务器的方式呈现。