侧边栏壁纸
  • 累计撰写 47 篇文章
  • 累计创建 2 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

性能优化:大数据渲染方案

  1. 无限滚动/分页

无限滚动:监听滚动事件,当用户滚动到底部附近时,加载下一批数据并追加到列表尾部。
优点:实现简单,对用户无缝体验好。
缺点:已渲染的 DOM 会不断累积,长时间滚动后性能依然会下降。需要自行实现“回到顶部”等功能。
实现:监听 scroll 事件 + 计算滚动位置,或使用 Intersection Observer API 监听一个“哨兵”元素。
分页器:
优点:实现极其简单,能精确控制 DOM 数量,内存管理最佳。
缺点:交互体验不连贯,每次翻页都有刷新感。
适用场景:列表、表格、图片流等。不适用于需要快速定位到数据中间某一部分的场景。

  1. 分片渲染(时间分片)

当单次渲染大量 DOM 不可避免,但又不适合用虚拟列表时(例如复杂图表、瀑布流),可以使用时间分片。

核心原理:利用 setTimeout、setInterval 或 requestAnimationFrame 将渲染任务分割成多个小任务,在浏览器的多个空闲帧中分批执行,避免单次长任务阻塞主线程导致页面卡死。
优点:能缓解单次渲染的卡顿,允许用户进行交互。
缺点:总渲染时间变长,用户会看到数据一条条“蹦”出来,体验不佳。本质上并未减少最终渲染的 DOM 总数。

function renderData(data) {
  let index = 0;
  const chunkSize = 50; // 每批渲染的数量
  function renderChunk() {
    for (let i = 0; i < chunkSize && index < data.length; i++, index++) {
      // 创建并插入DOM...
	  //DocumentFragment文档片段
    }
    if (index < data.length) {
      requestAnimationFrame(renderChunk);
    }
  }
  renderChunk();
}
  1. 虚拟滚动

这是处理大规模数据的“黄金标准”。其核心思想是:只渲染当前可视区域(Viewport)及附近缓冲区的数据,非可视区域的数据用空白占位符(Padding)代替。
核心原理:
计算滚动位置:监听容器滚动事件,计算出当前滚动距离 scrollTop。
计算索引范围:根据 scrollTop、容器高度、每一项的预估高度,计算出当前应该显示的数据的起始索引(startIndex)和结束索引(endIndex)。
截取数据:从完整数据源中截取 [startIndex, endIndex]范围内的数据子集。
设置占位:在列表头部和尾部分别设置一个空的 div 作为占位,其高度等于被截取掉的所有项的高度之和,从而保持滚动条高度与实际数据总量一致。
渲染:只渲染这个数据子集对应的 DOM。
优点:
无论总数据量多大(十万、百万级),同时存在的真实 DOM 数量基本恒定(几十个),性能极佳。
保持了流畅滚动的交互体验。
缺点:
实现复杂,需要考虑动态高度的计算、滚动条抖动等问题。
对需要精确获取行高、快速定位到某一行等高级功能支持有挑战。

开源库

React: react-window
Vue: vue-virtual-scroller
通用:@tanstack/virtual-core

虚拟滚动完整示例.html

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>虚拟滚动完整示例</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            min-height: 100vh;
            padding: 20px;
            color: #333;
        }
        
        .container {
            max-width: 1200px;
            margin: 0 auto;
        }
        
        header {
            text-align: center;
            margin-bottom: 30px;
            color: white;
        }
        
        h1 {
            font-size: 2.5rem;
            margin-bottom: 10px;
            text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
        }
        
        .subtitle {
            font-size: 1.2rem;
            opacity: 0.9;
        }
        
        .demo-container {
            display: flex;
            flex-wrap: wrap;
            gap: 20px;
            margin-bottom: 30px;
        }
        
        .demo-card {
            flex: 1;
            min-width: 300px;
            background: white;
            border-radius: 12px;
            box-shadow: 0 10px 40px rgba(0,0,0,0.1);
            overflow: hidden;
        }
        
        .card-header {
            background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
            color: white;
            padding: 20px;
        }
        
        .card-header h2 {
            font-size: 1.5rem;
            margin-bottom: 5px;
        }
        
        .card-body {
            padding: 0;
        }
        
        .controls {
            padding: 20px;
            background: #f8f9fa;
            border-bottom: 1px solid #e9ecef;
        }
        
        .control-group {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            align-items: center;
        }
        
        .control-group label {
            font-weight: 500;
            color: #495057;
        }
        
        input[type="number"], select, button {
            padding: 8px 12px;
            border: 1px solid #ddd;
            border-radius: 6px;
            font-size: 14px;
        }
        
        input[type="number"] {
            width: 120px;
        }
        
        button {
            background: #4facfe;
            color: white;
            border: none;
            cursor: pointer;
            transition: all 0.3s;
            font-weight: 500;
        }
        
        button:hover {
            background: #3a9bfe;
            transform: translateY(-1px);
            box-shadow: 0 4px 12px rgba(79, 172, 254, 0.3);
        }
        
        .stats {
            display: flex;
            justify-content: space-between;
            padding: 15px 20px;
            background: #f8f9fa;
            border-top: 1px solid #e9ecef;
            font-size: 13px;
            color: #666;
        }
        
        .stat-item {
            display: flex;
            flex-direction: column;
        }
        
        .stat-value {
            font-weight: 600;
            color: #333;
            font-size: 16px;
        }
        
        /* 列表容器样式 */
        .list-container {
            height: 400px;
            overflow-y: auto;
            position: relative;
        }
        
        .list-scroll {
            position: relative;
            will-change: transform;
        }
        
        .list-item {
            position: absolute;
            left: 0;
            right: 0;
            border-bottom: 1px solid #e9ecef;
            transition: all 0.2s;
        }
        
        .list-item:hover {
            background: #f8f9fa;
            transform: scale(1.005);
        }
        
        .item-content {
            padding: 15px 20px;
            display: flex;
            align-items: center;
        }
        
        .item-index {
            width: 40px;
            height: 40px;
            background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
            color: white;
            border-radius: 8px;
            display: flex;
            align-items: center;
            justify-content: center;
            font-weight: bold;
            margin-right: 15px;
            flex-shrink: 0;
        }
        
        .item-text {
            flex: 1;
        }
        
        .item-title {
            font-weight: 500;
            margin-bottom: 4px;
            color: #333;
        }
        
        .item-subtitle {
            font-size: 12px;
            color: #666;
        }
        
        .performance-monitor {
            background: #1a1a1a;
            color: #00ff00;
            font-family: 'Courier New', monospace;
            padding: 15px;
            border-radius: 6px;
            margin-top: 20px;
            font-size: 13px;
            line-height: 1.6;
        }
        
        .comparison-section {
            background: white;
            border-radius: 12px;
            padding: 20px;
            box-shadow: 0 10px 40px rgba(0,0,0,0.1);
        }
        
        .comparison-section h2 {
            color: #333;
            margin-bottom: 20px;
            text-align: center;
        }
        
        .comparison-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 20px;
        }
        
        .comparison-item {
            background: #f8f9fa;
            padding: 20px;
            border-radius: 8px;
        }
        
        .comparison-item h3 {
            color: #4facfe;
            margin-bottom: 10px;
        }
        
        .tech-stack {
            display: flex;
            flex-wrap: wrap;
            gap: 10px;
            margin-top: 20px;
        }
        
        .tech-tag {
            background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
            color: white;
            padding: 5px 10px;
            border-radius: 4px;
            font-size: 12px;
        }
        
        /* 滚动条样式 */
        .list-container::-webkit-scrollbar {
            width: 8px;
        }
        
        .list-container::-webkit-scrollbar-track {
            background: #f1f1f1;
            border-radius: 4px;
        }
        
        .list-container::-webkit-scrollbar-thumb {
            background: #4facfe;
            border-radius: 4px;
        }
        
        .list-container::-webkit-scrollbar-thumb:hover {
            background: #3a9bfe;
        }
    </style>
</head>
<body>
    <div class="container">
        <header>
            <h1>虚拟滚动完整示例</h1>
            <p class="subtitle">高性能大数据列表渲染解决方案</p>
        </header>
        
        <div class="demo-container">
            <!-- 虚拟滚动示例 -->
            <div class="demo-card">
                <div class="card-header">
                    <h2>虚拟滚动列表</h2>
                    <p>只渲染可见区域,支持海量数据</p>
                </div>
                <div class="controls">
                    <div class="control-group">
                        <label>数据量:</label>
                        <input type="number" id="dataCount" value="10000" min="100" max="1000000" step="1000">
                        <button onclick="updateData()">更新数据</button>
                        <select id="themeSelect" onchange="changeTheme(this.value)">
                            <option value="default">默认主题</option>
                            <option value="dark">深色主题</option>
                            <option value="colorful">多彩主题</option>
                        </select>
                    </div>
                </div>
                <div class="list-container" id="virtualListContainer">
                    <!-- 列表内容由JS动态生成 -->
                </div>
                <div class="stats">
                    <div class="stat-item">
                        <span>总数据量</span>
                        <span class="stat-value" id="totalCount">0</span>
                    </div>
                    <div class="stat-item">
                        <span>可见项目</span>
                        <span class="stat-value" id="visibleCount">0</span>
                    </div>
                    <div class="stat-item">
                        <span>渲染时间</span>
                        <span class="stat-value" id="renderTime">0ms</span>
                    </div>
                    <div class="stat-item">
                        <span>FPS</span>
                        <span class="stat-value" id="fps">60</span>
                    </div>
                </div>
            </div>
            
            <!-- 传统滚动对比 -->
            <div class="demo-card">
                <div class="card-header">
                    <h2>传统滚动对比</h2>
                    <p>渲染所有DOM元素</p>
                </div>
                <div class="controls">
                    <div class="control-group">
                        <label>对比数据量:</label>
                        <input type="number" id="compareCount" value="1000" min="100" max="10000" step="100">
                        <button onclick="updateCompareData()">更新对比</button>
                    </div>
                </div>
                <div class="list-container" id="normalListContainer">
                    <!-- 传统列表内容 -->
                </div>
                <div class="stats">
                    <div class="stat-item">
                        <span>DOM元素数</span>
                        <span class="stat-value" id="domCount">0</span>
                    </div>
                    <div class="stat-item">
                        <span>内存占用</span>
                        <span class="stat-value" id="memoryUsage">0MB</span>
                    </div>
                    <div class="stat-item">
                        <span>性能评分</span>
                        <span class="stat-value" id="performanceScore">-</span>
                    </div>
                </div>
            </div>
        </div>
        
        <!-- 性能监控 -->
        <div class="performance-monitor" id="performanceMonitor">
            <div>🎯 虚拟滚动性能监控</div>
            <div>├─ 可见区域: <span id="visibleArea">0-0</span></div>
            <div>├─ 缓冲区大小: <span id="bufferSize">0</span></div>
            <div>├─ 滚动位置: <span id="scrollPosition">0px</span></div>
            <div>└─ 上次渲染: <span id="lastRender">0ms</span></div>
        </div>
        
        <!-- 技术对比 -->
        <div class="comparison-section">
            <h2>技术对比</h2>
            <div class="comparison-grid">
                <div class="comparison-item">
                    <h3>虚拟滚动</h3>
                    <p><strong>优点:</strong></p>
                    <ul>
                        <li>⚡ 高性能,支持百万级数据</li>
                        <li>💾 内存占用低</li>
                        <li>🚀 滚动流畅,无卡顿</li>
                        <li>📱 移动端友好</li>
                    </ul>
                    <p><strong>缺点:</strong></p>
                    <ul>
                        <li>🔧 实现相对复杂</li>
                        <li>🔍 快速滚动时可能出现空白</li>
                        <li>📏 需要预估项目高度</li>
                    </ul>
                </div>
                
                <div class="comparison-item">
                    <h3>传统滚动</h3>
                    <p><strong>优点:</strong></p>
                    <ul>
                        <li>🎯 实现简单</li>
                        <li>🔧 兼容性好</li>
                        <li>📊 精确高度控制</li>
                        <li>⚡ 小数据量时性能好</li>
                    </ul>
                    <p><strong>缺点:</strong></p>
                    <ul>
                        <li>🐌 大数据量时性能差</li>
                        <li>💥 内存占用高</li>
                        <li>📱 移动端卡顿明显</li>
                        <li>⚡ 首次加载慢</li>
                    </ul>
                </div>
                
                <div class="comparison-item">
                    <h3>适用场景</h3>
                    <p><strong>使用虚拟滚动:</strong></p>
                    <ul>
                        <li>📈 大数据列表/表格</li>
                        <li>💬 聊天记录</li>
                        <li>📱 长列表Feed流</li>
                        <li>🎮 游戏排行榜</li>
                    </ul>
                    <p><strong>使用传统滚动:</strong></p>
                    <ul>
                        <li>📄 静态内容页面</li>
                        <li>📊 小数据量列表</li>
                        <li>🔧 需要精确控制的项目</li>
                        <li>📱 移动端简单列表</li>
                    </ul>
                </div>
            </div>
            
            <div class="tech-stack">
                <span class="tech-tag">JavaScript</span>
                <span class="tech-tag">DOM 操作</span>
                <span class="tech-tag">性能优化</span>
                <span class="tech-tag">Intersection Observer</span>
                <span class="tech-tag">RequestAnimationFrame</span>
                <span class="tech-tag">滚动优化</span>
            </div>
        </div>
    </div>

    <script>
        // ==================== 虚拟滚动类 ====================
        class VirtualScroll {
            constructor(container, options = {}) {
                this.container = container;
                this.options = {
                    itemHeight: 60,
                    bufferSize: 5,
                    renderItem: null,
                    ...options
                };
                
                this.data = [];
                this.visibleItems = [];
                this.isScrolling = false;
                this.scrollTimer = null;
                this.lastScrollTop = 0;
                
                // 性能监控
                this.frameCount = 0;
                this.lastFrameTime = performance.now();
                this.fps = 60;
                
                this.init();
                this.bindEvents();
                this.startFPSMonitor();
            }
            
            init() {
                // 创建滚动容器
                this.scrollContainer = document.createElement('div');
                this.scrollContainer.className = 'list-scroll';
                this.container.appendChild(this.scrollContainer);
                
                // 设置容器样式
                this.container.style.position = 'relative';
                this.container.style.overflowY = 'auto';
                
                // 设置滚动容器尺寸
                this.updateScrollHeight();
            }
            
            setData(data) {
                this.data = data;
                this.updateScrollHeight();
                this.render();
            }
            
            updateScrollHeight() {
                const totalHeight = this.data.length * this.options.itemHeight;
                this.scrollContainer.style.height = totalHeight + 'px';
            }
            
            getVisibleRange() {
                const scrollTop = this.container.scrollTop;
                const clientHeight = this.container.clientHeight;
                
                const startIndex = Math.max(0, Math.floor(scrollTop / this.options.itemHeight) - this.options.bufferSize);
                const endIndex = Math.min(
                    this.data.length - 1,
                    Math.floor((scrollTop + clientHeight) / this.options.itemHeight) + this.options.bufferSize
                );
                
                return { startIndex, endIndex };
            }
            
            render() {
                const startTime = performance.now();
                
                const { startIndex, endIndex } = this.getVisibleRange();
                const visibleData = this.data.slice(startIndex, endIndex + 1);
                
                // 更新性能监控
                this.updatePerformanceMonitor(startIndex, endIndex);
                
                // 清除现有内容
                this.scrollContainer.innerHTML = '';
                
                // 渲染可见项目
                visibleData.forEach((item, index) => {
                    const absoluteIndex = startIndex + index;
                    const top = absoluteIndex * this.options.itemHeight;
                    
                    const itemElement = document.createElement('div');
                    itemElement.className = 'list-item';
                    itemElement.style.top = top + 'px';
                    itemElement.style.height = this.options.itemHeight + 'px';
                    
                    if (this.options.renderItem) {
                        itemElement.innerHTML = this.options.renderItem(item, absoluteIndex);
                    } else {
                        itemElement.innerHTML = this.defaultRenderItem(item, absoluteIndex);
                    }
                    
                    this.scrollContainer.appendChild(itemElement);
                });
                
                // 更新统计信息
                this.updateStats(startTime, visibleData.length);
            }
            
            defaultRenderItem(item, index) {
                const colors = ['#667eea', '#764ba2', '#f093fb', '#4facfe', '#00f2fe'];
                const color = colors[index % colors.length];
                
                return `
                    <div class="item-content">
                        <div class="item-index" style="background: ${color}">
                            ${index + 1}
                        </div>
                        <div class="item-text">
                            <div class="item-title">项目 #${index + 1}</div>
                            <div class="item-subtitle">ID: ${item.id} | 值: ${item.value}</div>
                        </div>
                    </div>
                `;
            }
            
            bindEvents() {
                // 防抖滚动处理
                this.container.addEventListener('scroll', () => {
                    this.isScrolling = true;
                    this.lastScrollTop = this.container.scrollTop;
                    
                    if (this.scrollTimer) {
                        clearTimeout(this.scrollTimer);
                    }
                    
                    this.scrollTimer = setTimeout(() => {
                        this.isScrolling = false;
                    }, 100);
                    
                    this.render();
                    
                    // 更新滚动位置显示
                    document.getElementById('scrollPosition').textContent = 
                        Math.round(this.container.scrollTop) + 'px';
                });
                
                // 窗口大小变化时重新渲染
                window.addEventListener('resize', () => {
                    this.render();
                });
            }
            
            updateStats(startTime, visibleCount) {
                const renderTime = performance.now() - startTime;
                
                document.getElementById('totalCount').textContent = 
                    this.data.length.toLocaleString();
                document.getElementById('visibleCount').textContent = 
                    visibleCount.toLocaleString();
                document.getElementById('renderTime').textContent = 
                    renderTime.toFixed(2) + 'ms';
                document.getElementById('fps').textContent = 
                    Math.round(this.fps);
                
                document.getElementById('lastRender').textContent = 
                    renderTime.toFixed(2) + 'ms';
            }
            
            updatePerformanceMonitor(startIndex, endIndex) {
                document.getElementById('visibleArea').textContent = 
                    `${startIndex + 1}-${endIndex + 1}`;
                document.getElementById('bufferSize').textContent = 
                    this.options.bufferSize;
            }
            
            startFPSMonitor() {
                const calculateFPS = () => {
                    const now = performance.now();
                    this.frameCount++;
                    
                    if (now >= this.lastFrameTime + 1000) {
                        this.fps = this.frameCount;
                        this.frameCount = 0;
                        this.lastFrameTime = now;
                    }
                    
                    requestAnimationFrame(calculateFPS);
                };
                
                requestAnimationFrame(calculateFPS);
            }
            
            destroy() {
                this.container.removeEventListener('scroll', () => {});
                window.removeEventListener('resize', () => {});
            }
        }
        
        // ==================== 传统滚动类 ====================
        class NormalScroll {
            constructor(container) {
                this.container = container;
                this.data = [];
            }
            
            setData(data) {
                this.data = data;
                this.render();
            }
            
            render() {
                const startTime = performance.now();
                
                this.container.innerHTML = '';
                
                this.data.forEach((item, index) => {
                    const itemElement = document.createElement('div');
                    itemElement.className = 'list-item';
                    itemElement.style.position = 'relative';
                    itemElement.style.height = '60px';
                    
                    const colors = ['#667eea', '#764ba2', '#f093fb', '#4facfe', '#00f2fe'];
                    const color = colors[index % colors.length];
                    
                    itemElement.innerHTML = `
                        <div class="item-content">
                            <div class="item-index" style="background: ${color}">
                                ${index + 1}
                            </div>
                            <div class="item-text">
                                <div class="item-title">项目 #${index + 1}</div>
                                <div class="item-subtitle">ID: ${item.id} | 值: ${item.value}</div>
                            </div>
                        </div>
                    `;
                    
                    this.container.appendChild(itemElement);
                });
                
                const renderTime = performance.now() - startTime;
                
                // 更新统计信息
                document.getElementById('domCount').textContent = 
                    this.data.length.toLocaleString();
                document.getElementById('performanceScore').textContent = 
                    this.calculatePerformanceScore(renderTime, this.data.length);
                
                // 估算内存占用
                const memoryEstimate = (this.data.length * 0.2).toFixed(1);
                document.getElementById('memoryUsage').textContent = 
                    memoryEstimate + 'MB';
            }
            
            calculatePerformanceScore(renderTime, dataCount) {
                if (dataCount <= 1000) return '优秀 ⭐⭐⭐⭐⭐';
                if (dataCount <= 5000) return renderTime < 1000 ? '良好 ⭐⭐⭐⭐' : '一般 ⭐⭐⭐';
                return renderTime < 2000 ? '及格 ⭐⭐' : '较差 ⭐';
            }
        }
        
        // ==================== 工具函数 ====================
        function generateData(count) {
            const data = [];
            for (let i = 0; i < count; i++) {
                data.push({
                    id: `ITEM_${String(i + 1).padStart(6, '0')}`,
                    value: Math.floor(Math.random() * 10000),
                    timestamp: new Date(Date.now() - Math.random() * 10000000000)
                });
            }
            return data;
        }
        
        // ==================== 全局变量 ====================
        let virtualScroll = null;
        let normalScroll = null;
        
        // ==================== 初始化 ====================
        function init() {
            const virtualContainer = document.getElementById('virtualListContainer');
            const normalContainer = document.getElementById('normalListContainer');
            
            // 初始化虚拟滚动
            virtualScroll = new VirtualScroll(virtualContainer, {
                itemHeight: 60,
                bufferSize: 3,
                renderItem: (item, index) => {
                    const colors = ['#667eea', '#764ba2', '#f093fb', '#4facfe', '#00f2fe'];
                    const color = colors[index % colors.length];
                    const theme = document.getElementById('themeSelect').value;
                    
                    let bgColor = color;
                    let textColor = 'white';
                    
                    if (theme === 'dark') {
                        bgColor = '#333';
                        textColor = '#fff';
                    } else if (theme === 'colorful') {
                        const hue = (index * 137) % 360;
                        bgColor = `hsl(${hue}, 70%, 60%)`;
                    }
                    
                    return `
                        <div class="item-content">
                            <div class="item-index" style="background: ${bgColor}; color: ${textColor}">
                                ${index + 1}
                            </div>
                            <div class="item-text">
                                <div class="item-title">项目 #${index + 1}</div>
                                <div class="item-subtitle">ID: ${item.id} | 值: ${item.value}</div>
                            </div>
                        </div>
                    `;
                }
            });
            
            // 初始化传统滚动
            normalScroll = new NormalScroll(normalContainer);
            
            // 生成初始数据
            const initialCount = parseInt(document.getElementById('dataCount').value);
            const compareCount = parseInt(document.getElementById('compareCount').value);
            
            updateVirtualData(initialCount);
            updateNormalData(compareCount);
        }
        
        // ==================== 数据更新函数 ====================
        function updateVirtualData(count) {
            const data = generateData(count);
            virtualScroll.setData(data);
        }
        
        function updateNormalData(count) {
            const data = generateData(count);
            normalScroll.setData(data);
        }
        
        function updateData() {
            const count = parseInt(document.getElementById('dataCount').value);
            updateVirtualData(count);
        }
        
        function updateCompareData() {
            const count = parseInt(document.getElementById('compareCount').value);
            updateNormalData(count);
        }
        
        function changeTheme(theme) {
            const container = document.getElementById('virtualListContainer');
            const items = container.querySelectorAll('.list-item');
            
            items.forEach((item, index) => {
                const indexElem = item.querySelector('.item-index');
                if (indexElem) {
                    if (theme === 'dark') {
                        indexElem.style.background = '#333';
                        indexElem.style.color = 'white';
                    } else if (theme === 'colorful') {
                        const hue = (index * 137) % 360;
                        indexElem.style.background = `hsl(${hue}, 70%, 60%)`;
                        indexElem.style.color = 'white';
                    } else {
                        const colors = ['#667eea', '#764ba2', '#f093fb', '#4facfe', '#00f2fe'];
                        indexElem.style.background = colors[index % colors.length];
                        indexElem.style.color = 'white';
                    }
                }
            });
        }
        
        // ==================== 性能测试函数 ====================
        function runPerformanceTest() {
            const testSizes = [100, 1000, 10000, 50000, 100000];
            const results = [];
            
            testSizes.forEach(size => {
                console.log(`\n测试数据量: ${size}`);
                
                // 虚拟滚动测试
                const virtualData = generateData(size);
                const virtualStart = performance.now();
                virtualScroll.setData(virtualData);
                const virtualTime = performance.now() - virtualStart;
                
                // 传统滚动测试
                const normalData = generateData(Math.min(size, 10000)); // 限制传统滚动数据量
                const normalStart = performance.now();
                normalScroll.setData(normalData);
                const normalTime = performance.now() - normalStart;
                
                results.push({
                    size,
                    virtualTime,
                    normalTime,
                    performanceRatio: normalTime / virtualTime
                });
                
                console.log(`虚拟滚动: ${virtualTime.toFixed(2)}ms`);
                console.log(`传统滚动: ${normalTime.toFixed(2)}ms`);
                console.log(`性能比: ${(normalTime / virtualTime).toFixed(1)}x`);
            });
            
            return results;
        }
        
        // ==================== 页面加载完成 ====================
        document.addEventListener('DOMContentLoaded', () => {
            init();
            
            // 添加性能测试按钮
            const controls = document.querySelector('.controls');
            const testButton = document.createElement('button');
            testButton.textContent = '运行性能测试';
            testButton.onclick = () => {
                const results = runPerformanceTest();
                console.table(results);
                alert('性能测试完成!请查看控制台输出。');
            };
            controls.appendChild(testButton);
            
            // 添加重置按钮
            const resetButton = document.createElement('button');
            resetButton.textContent = '重置到默认';
            resetButton.onclick = () => {
                document.getElementById('dataCount').value = 10000;
                document.getElementById('compareCount').value = 1000;
                document.getElementById('themeSelect').value = 'default';
                updateData();
                updateCompareData();
                changeTheme('default');
            };
            controls.appendChild(resetButton);
            
            // 添加滚动到底部按钮
            const scrollButton = document.createElement('button');
            scrollButton.textContent = '滚动到底部';
            scrollButton.onclick = () => {
                const container = document.getElementById('virtualListContainer');
                container.scrollTop = container.scrollHeight;
            };
            controls.appendChild(scrollButton);
        });
        
        // ==================== 内存监控 ====================
        function updateMemoryUsage() {
            if (window.performance && performance.memory) {
                const memory = performance.memory;
                const usedMB = (memory.usedJSHeapSize / 1024 / 1024).toFixed(1);
                const totalMB = (memory.totalJSHeapSize / 1024 / 1024).toFixed(1);
                
                console.log(`内存使用: ${usedMB}MB / ${totalMB}MB`);
            }
        }
        
        // 定期更新内存使用情况
        setInterval(updateMemoryUsage, 5000);
    </script>
</body>
</html>
  1. canvas

对于超大规模数据可视化(如十万个散点图、地图标记、关系图谱),DOM 的性能瓶颈是无法突破的。此时需要用更底层的图形技术。

Canvas 2D: 通过绘制 API 直接控制像素,性能远超 DOM。适用于图表、热力图等。
WebGL: 利用 GPU 进行高性能图形计算,适用于 3D 场景、粒子系统、海量几何图形的渲染。
优点:
极限性能,可渲染百万级数据点。
图形绘制能力强大。
缺点:
实现复杂,需要图形学知识。
完全脱离 DOM 生态:无法使用 CSS、无法方便地绑定事件、文本渲染和 SEO 不友好。通常需要自建一套交互和布局系统。

开源库:
图表/可视化: ECharts, AntV G2, D3.js(部分结合 Canvas)
WebGL 引擎: Three.js, Babylon.js

Canvas 大数据表格.html

<!DOCTYPE html>
<html>
<head>
    <title>Canvas 大数据表格</title>
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { padding: 20px; background: #f0f2f5; }
        .container { 
            width: 800px; 
            height: 500px; 
            border: 2px solid #1890ff; 
            background: white;
            margin: 0 auto;
            position: relative;
            overflow: hidden;
        }
        #info { 
            margin-top: 10px; 
            padding: 10px; 
            background: white; 
            border: 1px solid #ddd; 
        }
        .controls {
            text-align: center;
            margin: 10px 0;
            padding: 10px;
        }
        button {
            margin: 0 5px;
            padding: 8px 16px;
            background: #1890ff;
            color: white;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-size: 14px;
        }
        button:hover {
            background: #108ee9;
        }
    </style>
</head>
<body>
    <h2 style="text-align: center; margin-bottom: 10px; color: #1890ff;">
        Canvas 大数据表格
    </h2>
    
    <div class="controls" id="controls"></div>
    
    <div class="container" id="tableContainer"></div>
    <div id="info">状态: 初始化中...</div>
    
    <script>
        // ==================== 滚动条类 ====================
        class Scrollbar {
            constructor(viewport, canvas, renderer) {
                this.viewport = viewport;
                this.canvas = canvas;
                this.renderer = renderer;
                this.isDraggingH = false;
                this.isDraggingV = false;
                this.isHoveredH = false;
                this.isHoveredV = false;
                this.dragStartX = 0;
                this.dragStartY = 0;
                this.dragScrollStartX = 0;
                this.dragScrollStartY = 0;
                
                // 滚动条样式
                this.scrollbarSize = 12;
                this.trackColor = '#f5f5f5';
                this.thumbColor = '#c1c1c1';
                this.thumbHoverColor = '#a8a8a8';
                this.thumbActiveColor = '#888888';
                this.cornerColor = '#e8e8e8';
                
                // 初始化尺寸
                this.verticalVisible = false;
                this.horizontalVisible = false;
                this.availableWidth = 0;
                this.availableHeight = 0;
                
                this.bindEvents();
            }
            
            // 计算滚动条尺寸
            updateDimensions(data, columns) {
                if (!data || data.length === 0) return;
                
                const viewport = this.viewport;
                const canvas = this.canvas;
                const scrollbarSize = this.scrollbarSize;
                
                const totalRows = data.length;
                const totalCols = columns.length;
                
                // 计算内容总尺寸
                const contentHeight = totalRows * viewport.rowHeight + viewport.headerHeight;
                const contentWidth = totalCols * viewport.colWidth;
                
                // 检查是否需要滚动条
                this.verticalVisible = contentHeight > canvas.height;
                this.horizontalVisible = contentWidth > canvas.width;
                
                // 可用区域(减去滚动条占用的空间)
                this.availableWidth = canvas.width - (this.verticalVisible ? scrollbarSize : 0);
                this.availableHeight = canvas.height - (this.horizontalVisible ? scrollbarSize : 0);
                
                // 垂直滚动条尺寸
                if (this.verticalVisible) {
                    this.vTrackHeight = this.availableHeight;
                    this.vThumbHeight = Math.max(
                        scrollbarSize * 2,
                        (canvas.height / contentHeight) * this.vTrackHeight
                    );
                    this.vThumbTop = (viewport.scrollTop / contentHeight) * this.vTrackHeight;
                    this.vThumbY = Math.max(0, Math.min(this.vThumbTop, this.vTrackHeight - this.vThumbHeight));
                }
                
                // 水平滚动条尺寸
                if (this.horizontalVisible) {
                    this.hTrackWidth = this.availableWidth;
                    this.hThumbWidth = Math.max(
                        scrollbarSize * 2,
                        (canvas.width / contentWidth) * this.hTrackWidth
                    );
                    this.hThumbLeft = (viewport.scrollLeft / contentWidth) * this.hTrackWidth;
                    this.hThumbX = Math.max(0, Math.min(this.hThumbLeft, this.hTrackWidth - this.hThumbWidth));
                }
            }
            
            // 绘制滚动条
            draw(ctx, data, columns) {
                if (!data || data.length === 0) return;
                
                this.updateDimensions(data, columns);
                
                const canvas = this.canvas;
                const width = canvas.width;
                const height = canvas.height;
                const scrollbarSize = this.scrollbarSize;
                
                // 绘制角落填充
                if (this.verticalVisible && this.horizontalVisible) {
                    ctx.fillStyle = this.cornerColor;
                    ctx.fillRect(
                        width - scrollbarSize,
                        height - scrollbarSize,
                        scrollbarSize,
                        scrollbarSize
                    );
                }
                
                // 绘制垂直滚动条
                if (this.verticalVisible) {
                    // 轨道
                    ctx.fillStyle = this.trackColor;
                    ctx.fillRect(
                        width - scrollbarSize,
                        0,
                        scrollbarSize,
                        this.vTrackHeight
                    );
                    
                    // 滑块
                    const thumbColor = this.isDraggingV ? this.thumbActiveColor : 
                                      (this.isHoveredV ? this.thumbHoverColor : this.thumbColor);
                    ctx.fillStyle = thumbColor;
                    
                    // 圆角滑块
                    this.drawRoundedRect(
                        ctx,
                        width - scrollbarSize + 2,
                        this.vThumbY + 2,
                        scrollbarSize - 4,
                        this.vThumbHeight - 4,
                        4
                    );
                    ctx.fill();
                }
                
                // 绘制水平滚动条
                if (this.horizontalVisible) {
                    // 轨道
                    ctx.fillStyle = this.trackColor;
                    ctx.fillRect(
                        0,
                        height - scrollbarSize,
                        this.hTrackWidth,
                        scrollbarSize
                    );
                    
                    // 滑块
                    const thumbColor = this.isDraggingH ? this.thumbActiveColor : 
                                      (this.isHoveredH ? this.thumbHoverColor : this.thumbColor);
                    ctx.fillStyle = thumbColor;
                    
                    // 圆角滑块
                    this.drawRoundedRect(
                        ctx,
                        this.hThumbX + 2,
                        height - scrollbarSize + 2,
                        this.hThumbWidth - 4,
                        scrollbarSize - 4,
                        4
                    );
                    ctx.fill();
                }
            }
            
            // 绘制圆角矩形
            drawRoundedRect(ctx, x, y, width, height, radius) {
                ctx.beginPath();
                ctx.moveTo(x + radius, y);
                ctx.lineTo(x + width - radius, y);
                ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
                ctx.lineTo(x + width, y + height - radius);
                ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
                ctx.lineTo(x + radius, y + height);
                ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
                ctx.lineTo(x, y + radius);
                ctx.quadraticCurveTo(x, y, x + radius, y);
                ctx.closePath();
            }
            
            // 绑定事件
            bindEvents() {
                // 事件委托给主控制器处理
            }
            
            // 检查是否点击了滚动条
            checkClick(x, y) {
                if (!this.viewport.data || this.viewport.data.length === 0) return null;
                
                const canvas = this.canvas;
                const scrollbarSize = this.scrollbarSize;
                
                // 检查垂直滚动条
                if (this.verticalVisible) {
                    const thumbRect = {
                        x: canvas.width - scrollbarSize + 2,
                        y: this.vThumbY + 2,
                        width: scrollbarSize - 4,
                        height: this.vThumbHeight - 4
                    };
                    
                    if (this.pointInRect(x, y, thumbRect)) {
                        return { type: 'v-thumb' };
                    }
                    
                    const trackRect = {
                        x: canvas.width - scrollbarSize,
                        y: 0,
                        width: scrollbarSize,
                        height: this.vTrackHeight
                    };
                    
                    if (this.pointInRect(x, y, trackRect)) {
                        return { type: 'v-track', y: y };
                    }
                }
                
                // 检查水平滚动条
                if (this.horizontalVisible) {
                    const thumbRect = {
                        x: this.hThumbX + 2,
                        y: canvas.height - scrollbarSize + 2,
                        width: this.hThumbWidth - 4,
                        height: scrollbarSize - 4
                    };
                    
                    if (this.pointInRect(x, y, thumbRect)) {
                        return { type: 'h-thumb' };
                    }
                    
                    const trackRect = {
                        x: 0,
                        y: canvas.height - scrollbarSize,
                        width: this.hTrackWidth,
                        height: scrollbarSize
                    };
                    
                    if (this.pointInRect(x, y, trackRect)) {
                        return { type: 'h-track', x: x };
                    }
                }
                
                return null;
            }
            
            // 检查悬停状态
            checkHover(x, y) {
                this.isHoveredV = false;
                this.isHoveredH = false;
                
                if (!this.viewport.data || this.viewport.data.length === 0) return;
                
                const canvas = this.canvas;
                const scrollbarSize = this.scrollbarSize;
                
                if (this.verticalVisible) {
                    const thumbRect = {
                        x: canvas.width - scrollbarSize + 2,
                        y: this.vThumbY + 2,
                        width: scrollbarSize - 4,
                        height: this.vThumbHeight - 4
                    };
                    
                    this.isHoveredV = this.pointInRect(x, y, thumbRect);
                }
                
                if (this.horizontalVisible) {
                    const thumbRect = {
                        x: this.hThumbX + 2,
                        y: canvas.height - scrollbarSize + 2,
                        width: this.hThumbWidth - 4,
                        height: scrollbarSize - 4
                    };
                    
                    this.isHoveredH = this.pointInRect(x, y, thumbRect);
                }
            }
            
            // 开始拖动
            startDrag(type, x, y) {
                if (type === 'v-thumb') {
                    this.isDraggingV = true;
                    this.dragStartY = y;
                    this.dragScrollStartY = this.viewport.scrollTop;
                } else if (type === 'h-thumb') {
                    this.isDraggingH = true;
                    this.dragStartX = x;
                    this.dragScrollStartX = this.viewport.scrollLeft;
                }
            }
            
            // 处理拖动
            handleDrag(x, y) {
                if (this.isDraggingV) {
                    const deltaY = y - this.dragStartY;
                    const totalRows = this.viewport.data.length;
                    const contentHeight = totalRows * this.viewport.rowHeight + this.viewport.headerHeight;
                    const scrollRatio = deltaY / this.vTrackHeight;
                    
                    this.viewport.scrollTop = this.dragScrollStartY + (scrollRatio * contentHeight);
                    this.viewport.clampScroll();
                    return true;
                }
                
                if (this.isDraggingH) {
                    const deltaX = x - this.dragStartX;
                    const totalCols = this.viewport.columns.length;
                    const contentWidth = totalCols * this.viewport.colWidth;
                    const scrollRatio = deltaX / this.hTrackWidth;
                    
                    this.viewport.scrollLeft = this.dragScrollStartX + (scrollRatio * contentWidth);
                    this.viewport.clampScroll();
                    return true;
                }
                
                return false;
            }
            
            // 处理轨道点击
            handleTrackClick(type, pos) {
                if (type === 'v-track') {
                    const totalRows = this.viewport.data.length;
                    const contentHeight = totalRows * this.viewport.rowHeight + this.viewport.headerHeight;
                    const clickRatio = pos / this.vTrackHeight;
                    this.viewport.scrollTop = clickRatio * contentHeight;
                    this.viewport.clampScroll();
                    return true;
                }
                
                if (type === 'h-track') {
                    const totalCols = this.viewport.columns.length;
                    const contentWidth = totalCols * this.viewport.colWidth;
                    const clickRatio = pos / this.hTrackWidth;
                    this.viewport.scrollLeft = clickRatio * contentWidth;
                    this.viewport.clampScroll();
                    return true;
                }
                
                return false;
            }
            
            // 停止拖动
            stopDrag() {
                this.isDraggingV = false;
                this.isDraggingH = false;
            }
            
            // 检查点是否在矩形内
            pointInRect(x, y, rect) {
                return x >= rect.x && 
                       x <= rect.x + rect.width && 
                       y >= rect.y && 
                       y <= rect.y + rect.height;
            }
            
            // 获取鼠标样式
            getCursorStyle(x, y) {
                const clickResult = this.checkClick(x, y);
                if (clickResult && (clickResult.type === 'v-thumb' || clickResult.type === 'h-thumb')) {
                    return 'pointer';
                }
                
                if (this.isHoveredV || this.isHoveredH || this.isDraggingV || this.isDraggingH) {
                    return 'pointer';
                }
                
                return 'default';
            }
        }
        
        // ==================== 视口类 ====================
        class SimpleViewport {
            constructor(canvasWidth, canvasHeight) {
                this.scrollTop = 0;
                this.scrollLeft = 0;
                this.width = canvasWidth;
                this.height = canvasHeight;
                this.rowHeight = 30;
                this.colWidth = 100;
                this.headerHeight = 40;
                this.data = null;
                this.columns = null;
                this.onRender = null;
            }
            
            // 计算最大滚动范围
            getMaxScroll() {
                if (!this.data || !this.columns) {
                    return { maxTop: 0, maxLeft: 0 };
                }
                
                const totalHeight = this.data.length * this.rowHeight + this.headerHeight;
                const totalWidth = this.columns.length * this.colWidth;
                
                return {
                    maxTop: Math.max(0, totalHeight - this.height),
                    maxLeft: Math.max(0, totalWidth - this.width)
                };
            }
            
            // 限制滚动范围
            clampScroll() {
                const maxScroll = this.getMaxScroll();
                this.scrollTop = Math.max(0, Math.min(this.scrollTop, maxScroll.maxTop));
                this.scrollLeft = Math.max(0, Math.min(this.scrollLeft, maxScroll.maxLeft));
            }
        }
        
        // ==================== 渲染器类 ====================
        class SimpleRenderer {
            constructor(canvas) {
                this.canvas = canvas;
                this.ctx = canvas.getContext('2d');
                this.data = [];
                this.columns = [];
                this.scrollbar = null;
            }
            
            setScrollbar(scrollbar) {
                this.scrollbar = scrollbar;
            }
            
            drawDataTable(data, columns, viewport) {
                this.data = data;
                this.columns = columns;
                
                const ctx = this.ctx;
                const canvas = this.canvas;
                const width = canvas.width;
                const height = canvas.height;
                
                // 清空并绘制背景
                ctx.clearRect(0, 0, width, height);
                ctx.fillStyle = '#ffffff';
                ctx.fillRect(0, 0, width, height);
                
                if (!data || data.length === 0) {
                    this.drawTest();
                    return;
                }
                
                const headerHeight = viewport.headerHeight;
                const rowHeight = viewport.rowHeight;
                const colWidth = viewport.colWidth;
                
                // 获取滚动条可用区域
                const scrollbarSize = this.scrollbar?.scrollbarSize || 0;
                const availableWidth = width - (this.scrollbar?.verticalVisible ? scrollbarSize : 0);
                const availableHeight = height - (this.scrollbar?.horizontalVisible ? scrollbarSize : 0);
                
                // 计算可见区域
                const startRow = Math.max(0, Math.floor(viewport.scrollTop / rowHeight));
                const endRow = Math.min(data.length - 1, 
                    Math.floor((viewport.scrollTop + availableHeight - headerHeight) / rowHeight)
                );
                
                const startCol = Math.max(0, Math.floor(viewport.scrollLeft / colWidth));
                const endCol = Math.min(columns.length - 1,
                    Math.floor((viewport.scrollLeft + availableWidth) / colWidth)
                );
                
                // 绘制表头
                this.drawHeader(ctx, columns, viewport, startCol, endCol, availableWidth, headerHeight);
                
                // 绘制数据行
                if (endRow >= startRow) {
                    for (let r = startRow; r <= endRow; r++) {
                        const rowY = headerHeight + r * rowHeight - viewport.scrollTop;
                        const rowData = data[r];
                        
                        // 行背景
                        ctx.fillStyle = r % 2 === 0 ? '#ffffff' : '#f8f9fa';
                        ctx.fillRect(0, rowY, availableWidth, rowHeight);
                        
                        // 单元格数据
                        ctx.fillStyle = '#333';
                        ctx.font = '13px Arial';
                        ctx.textAlign = 'left';
                        ctx.textBaseline = 'middle';
                        
                        for (let c = startCol; c <= endCol; c++) {
                            if (c >= columns.length) break;
                            
                            const colX = c * colWidth - viewport.scrollLeft;
                            if (colX + colWidth < 0 || colX > availableWidth) continue;
                            
                            const column = columns[c];
                            const value = rowData[column.field];
                            
                            // 文本裁剪
                            ctx.save();
                            ctx.beginPath();
                            ctx.rect(colX + 5, rowY + 1, colWidth - 10, rowHeight - 2);
                            ctx.clip();
                            
                            const text = this.formatValue(value, column);
                            ctx.fillText(text, colX + 8, rowY + rowHeight/2);
                            
                            ctx.restore();
                        }
                    }
                    
                    // 绘制网格线
                    this.drawGridLines(ctx, viewport, startRow, endRow, startCol, endCol, 
                                     data.length, columns.length, availableWidth, headerHeight);
                }
                
                // 绘制滚动条
                if (this.scrollbar) {
                    this.scrollbar.draw(ctx, data, columns);
                }
                
                // 更新信息
                this.updateInfo(data.length, startRow, endRow, startCol, endCol, viewport);
            }
            
            drawHeader(ctx, columns, viewport, startCol, endCol, width, headerHeight) {
                const colWidth = viewport.colWidth;
                
                // 表头背景
                ctx.fillStyle = '#1890ff';
                ctx.fillRect(0, 0, width, headerHeight);
                
                // 表头文字
                ctx.fillStyle = 'white';
                ctx.font = 'bold 14px Arial';
                ctx.textAlign = 'center';
                ctx.textBaseline = 'middle';
                
                for (let c = startCol; c <= endCol; c++) {
                    if (c >= columns.length) break;
                    
                    const colX = c * colWidth - viewport.scrollLeft;
                    if (colX + colWidth < 0 || colX > width) continue;
                    
                    const column = columns[c];
                    ctx.fillText(
                        column.title || column.field,
                        colX + colWidth/2,
                        headerHeight/2
                    );
                    
                    // 列分隔线
                    ctx.strokeStyle = 'rgba(255,255,255,0.3)';
                    ctx.lineWidth = 1;
                    ctx.beginPath();
                    ctx.moveTo(colX + colWidth + 0.5, 5);
                    ctx.lineTo(colX + colWidth + 0.5, headerHeight - 5);
                    ctx.stroke();
                }
                
                // 表头底部边框
                ctx.strokeStyle = '#108ee9';
                ctx.lineWidth = 2;
                ctx.beginPath();
                ctx.moveTo(0, headerHeight + 0.5);
                ctx.lineTo(width, headerHeight + 0.5);
                ctx.stroke();
            }
            
            drawGridLines(ctx, viewport, startRow, endRow, startCol, endCol, 
                         totalRows, totalCols, width, headerHeight) {
                const rowHeight = viewport.rowHeight;
                const colWidth = viewport.colWidth;
                
                ctx.strokeStyle = '#e8e8e8';
                ctx.lineWidth = 1;
                
                // 水平线
                for (let r = Math.max(startRow, 0); r <= Math.min(endRow + 1, totalRows - 1); r++) {
                    const y = headerHeight + r * rowHeight - viewport.scrollTop + 0.5;
                    const startX = Math.max(startCol, 0) * colWidth - viewport.scrollLeft;
                    const endX = Math.min(endCol + 1, totalCols - 1) * colWidth - viewport.scrollLeft;
                    
                    ctx.beginPath();
                    ctx.moveTo(startX, y);
                    ctx.lineTo(endX, y);
                    ctx.stroke();
                }
                
                // 垂直线
                for (let c = Math.max(startCol, 0); c <= Math.min(endCol + 1, totalCols - 1); c++) {
                    const x = c * colWidth - viewport.scrollLeft + 0.5;
                    const startY = headerHeight + Math.max(startRow, 0) * rowHeight - viewport.scrollTop;
                    const endY = headerHeight + Math.min(endRow, totalRows - 1) * rowHeight - viewport.scrollTop;
                    
                    ctx.beginPath();
                    ctx.moveTo(x, startY);
                    ctx.lineTo(x, endY);
                    ctx.stroke();
                }
            }
            
            drawTest() {
                const ctx = this.ctx;
                const width = this.canvas.width;
                const height = this.canvas.height;
                
                ctx.clearRect(0, 0, width, height);
                ctx.fillStyle = '#ffffff';
                ctx.fillRect(0, 0, width, height);
                
                ctx.fillStyle = '#1890ff';
                ctx.font = 'bold 20px Arial';
                ctx.textAlign = 'center';
                ctx.fillText('Canvas 表格演示', width/2, height/2 - 20);
                
                ctx.fillStyle = '#666';
                ctx.font = '16px Arial';
                ctx.fillText('点击上方按钮加载数据', width/2, height/2 + 20);
            }
            
            formatValue(value, column) {
                if (value == null) return '';
                
                if (typeof value === 'number') {
                    if (column.format === 'int') {
                        return Math.floor(value).toLocaleString();
                    }
                    if (column.format === 'percent') {
                        return (value * 100).toFixed(1) + '%';
                    }
                    return value.toLocaleString('zh-CN', { 
                        minimumFractionDigits: 2, 
                        maximumFractionDigits: 2 
                    });
                }
                
                if (typeof value === 'boolean') {
                    return value ? '✓' : '✗';
                }
                
                const text = String(value);
                return text.length > 15 ? text.substring(0, 15) + '...' : text;
            }
            
            updateInfo(totalRows, startRow, endRow, startCol, endCol, viewport) {
                const info = document.getElementById('info');
                if (info) {
                    info.innerHTML = `
                        <strong>表格状态:</strong> ${totalRows.toLocaleString()} 行数据 | 
                        可见行: ${startRow+1}-${endRow+1} (${endRow-startRow+1}行) | 
                        可见列: ${startCol+1}-${endCol+1} (${endCol-startCol+1}列) |
                        滚动: (${Math.round(viewport.scrollTop)}, ${Math.round(viewport.scrollLeft)}) |
                        提示: 可以使用滚动条、鼠标滚轮或拖动表格
                    `;
                }
            }
        }
        
        // ==================== 主控制器 ====================
        class SimpleTable {
            constructor(containerId) {
                this.container = document.getElementById(containerId);
                if (!this.container) {
                    throw new Error(`找不到容器: ${containerId}`);
                }
                
                // 创建 Canvas
                this.canvas = document.createElement('canvas');
                this.container.appendChild(this.canvas);
                
                // 设置 Canvas 尺寸
                this.updateCanvasSize();
                
                // 创建视口
                this.viewport = new SimpleViewport(this.canvas.width, this.canvas.height);
                
                // 创建渲染器
                this.renderer = new SimpleRenderer(this.canvas);
                
                // 创建滚动条
                this.scrollbar = new Scrollbar(this.viewport, this.canvas, this.renderer);
                this.renderer.setScrollbar(this.scrollbar);
                
                // 设置渲染回调
                this.viewport.onRender = () => this.render();
                
                // 绑定事件
                this.bindEvents();
                
                // 初始绘制
                this.render();
                
                console.log('表格初始化完成');
            }
            
            updateCanvasSize() {
                const rect = this.container.getBoundingClientRect();
                this.canvas.width = rect.width;
                this.canvas.height = rect.height;
                this.canvas.style.width = '100%';
                this.canvas.style.height = '100%';
                this.canvas.style.display = 'block';
                
                if (this.viewport) {
                    this.viewport.width = rect.width;
                    this.viewport.height = rect.height;
                }
            }
            
            bindEvents() {
                // 鼠标滚轮事件
                this.canvas.addEventListener('wheel', this.handleWheel.bind(this));
                
                // 鼠标事件
                this.isDraggingTable = false;
                this.dragStart = { x: 0, y: 0 };
                this.scrollStart = { x: 0, y: 0 };
                
                this.canvas.addEventListener('mousedown', this.handleMouseDown.bind(this));
                this.canvas.addEventListener('mousemove', this.handleMouseMove.bind(this));
                this.canvas.addEventListener('mouseup', this.handleMouseUp.bind(this));
                this.canvas.addEventListener('mouseleave', this.handleMouseLeave.bind(this));
                
                // 窗口大小变化
                window.addEventListener('resize', () => {
                    this.updateCanvasSize();
                    this.render();
                });
            }
            
            handleWheel(e) {
                e.preventDefault();
                
                // 平滑滚动
                const scrollSpeed = 0.5;
                const deltaX = e.deltaX * scrollSpeed;
                const deltaY = e.deltaY * scrollSpeed;
                
                // 更新滚动位置
                this.viewport.scrollLeft += deltaX;
                this.viewport.scrollTop += deltaY;
                this.viewport.clampScroll();
                
                this.render();
            }
            
            handleMouseDown(e) {
                e.preventDefault();
                
                const rect = this.canvas.getBoundingClientRect();
                const x = e.clientX - rect.left;
                const y = e.clientY - rect.top;
                
                // 先检查是否点击了滚动条
                const scrollbarClick = this.scrollbar.checkClick(x, y);
                
                if (scrollbarClick) {
                    if (scrollbarClick.type === 'v-thumb' || scrollbarClick.type === 'h-thumb') {
                        // 开始拖动滚动条滑块
                        this.scrollbar.startDrag(scrollbarClick.type, x, y);
                    } else if (scrollbarClick.type === 'v-track' || scrollbarClick.type === 'h-track') {
                        // 点击轨道,快速滚动
                        this.scrollbar.handleTrackClick(scrollbarClick.type, scrollbarClick.x || scrollbarClick.y);
                        this.render();
                    }
                } else {
                    // 点击表格区域,开始拖动表格
                    this.isDraggingTable = true;
                    this.dragStart = { x: e.clientX, y: e.clientY };
                    this.scrollStart = { 
                        x: this.viewport.scrollLeft, 
                        y: this.viewport.scrollTop 
                    };
                    this.canvas.style.cursor = 'grabbing';
                }
            }
            
            handleMouseMove(e) {
                e.preventDefault();
                
                const rect = this.canvas.getBoundingClientRect();
                const x = e.clientX - rect.left;
                const y = e.clientY - rect.top;
                
                // 更新悬停状态
                this.scrollbar.checkHover(x, y);
                
                // 设置鼠标样式
                const cursor = this.scrollbar.getCursorStyle(x, y);
                this.canvas.style.cursor = this.isDraggingTable ? 'grabbing' : cursor;
                
                // 处理滚动条拖动
                if (this.scrollbar.isDraggingV || this.scrollbar.isDraggingH) {
                    if (this.scrollbar.handleDrag(x, y)) {
                        this.render();
                    }
                    return;
                }
                
                // 处理表格拖动
                if (this.isDraggingTable) {
                    const deltaX = e.clientX - this.dragStart.x;
                    const deltaY = e.clientY - this.dragStart.y;
                    
                    this.viewport.scrollLeft = this.scrollStart.x - deltaX;
                    this.viewport.scrollTop = this.scrollStart.y - deltaY;
                    this.viewport.clampScroll();
                    
                    this.render();
                }
            }
            
            handleMouseUp() {
                this.scrollbar.stopDrag();
                this.isDraggingTable = false;
                
                // 重置鼠标样式
                const rect = this.canvas.getBoundingClientRect();
                const x = event.clientX - rect.left;
                const y = event.clientY - rect.top;
                this.scrollbar.checkHover(x, y);
                const cursor = this.scrollbar.getCursorStyle(x, y);
                this.canvas.style.cursor = cursor;
            }
            
            handleMouseLeave() {
                this.scrollbar.stopDrag();
                this.isDraggingTable = false;
                this.canvas.style.cursor = 'default';
            }
            
            setData(data, columns) {
                this.data = data;
                this.columns = columns;
                
                // 更新视口中的数据和列引用
                this.viewport.data = data;
                this.viewport.columns = columns;
                
                this.render();
            }
            
            render() {
                if (this.renderer) {
                    this.renderer.drawDataTable(this.data, this.columns, this.viewport);
                }
            }
            
            scrollToTop() {
                this.viewport.scrollTop = 0;
                this.viewport.scrollLeft = 0;
                this.render();
            }
        }
        
        // ==================== 工具函数 ====================
        function generateTestData(rows, cols) {
            const data = [];
            const firstNames = ['张', '李', '王', '刘', '陈', '杨', '赵', '黄', '周', '吴'];
            const lastNames = ['伟', '芳', '娜', '秀英', '敏', '静', '丽', '强', '磊', '洋'];
            const cities = ['北京', '上海', '广州', '深圳', '杭州', '成都', '武汉', '南京', '西安', '重庆'];
            const products = ['手机', '电脑', '平板', '电视', '冰箱', '空调', '洗衣机', '相机', '耳机', '手表'];
            
            for (let i = 0; i < rows; i++) {
                const row = {
                    id: i + 1,
                    name: firstNames[i % firstNames.length] + lastNames[i % lastNames.length],
                    age: Math.floor(Math.random() * 50) + 18,
                    city: cities[i % cities.length],
                    product: products[i % products.length],
                    sales: Math.random() * 10000,
                    profit: Math.random() * 2000,
                    growth: Math.random() - 0.3,
                    completed: Math.random() > 0.5,
                    score: Math.floor(Math.random() * 100)
                };
                
                // 添加更多列以确保出现水平滚动条
                for (let j = 9; j < cols; j++) {
                    row[`col${j}`] = `数据${i+1}-${j+1}-${Math.random().toString(36).substr(2, 5)}`;
                }
                
                data.push(row);
            }
            
            return data;
        }
        
        function generateColumns(cols) {
            const baseColumns = [
                { field: 'id', title: 'ID', width: 80, format: 'int' },
                { field: 'name', title: '姓名', width: 120 },
                { field: 'age', title: '年龄', width: 80, format: 'int' },
                { field: 'city', title: '城市', width: 100 },
                { field: 'product', title: '产品', width: 120 },
                { field: 'sales', title: '销售额', width: 120 },
                { field: 'profit', title: '利润', width: 120 },
                { field: 'growth', title: '增长率', width: 100, format: 'percent' },
                { field: 'completed', title: '完成', width: 80 },
                { field: 'score', title: '评分', width: 80, format: 'int' }
            ];
            
            const columns = baseColumns.slice(0, Math.min(cols, baseColumns.length));
            
            for (let i = baseColumns.length; i < cols; i++) {
                columns.push({
                    field: `col${i}`,
                    title: `列 ${i + 1}`,
                    width: 150  // 增加列宽以确保出现水平滚动条
                });
            }
            
            return columns;
        }
        
        function createButton(text, onClick) {
            const btn = document.createElement('button');
            btn.textContent = text;
            btn.onclick = onClick;
            return btn;
        }
        
        // ==================== 页面初始化 ====================
        document.addEventListener('DOMContentLoaded', function() {
            let table;
            
            try {
                table = new SimpleTable('tableContainer');
                
                // 添加控制按钮
                const controls = document.getElementById('controls');
                
                controls.appendChild(createButton('加载 1K 数据 (8列)', () => {
                    const data = generateTestData(1000, 8);
                    const cols = generateColumns(8);
                    table.setData(data, cols);
                }));
                
                controls.appendChild(createButton('加载 10K 数据 (12列)', () => {
                    const data = generateTestData(10000, 12);
                    const cols = generateColumns(12);
                    table.setData(data, cols);
                }));
                
                controls.appendChild(createButton('加载 50K 数据 (20列)', () => {
                    const data = generateTestData(50000, 20);
                    const cols = generateColumns(20);
                    table.setData(data, cols);
                }));
                
                controls.appendChild(createButton('滚动到顶部', () => {
                    table.scrollToTop();
                }));
                
                controls.appendChild(createButton('清空数据', () => {
                    table.setData([], []);
                }));
                
                // 添加功能说明
                const tip = document.createElement('div');
                tip.style.cssText = 'margin-top: 10px; color: #666; font-size: 12px;';
                tip.innerHTML = '功能: 1.鼠标滚轮滚动 2.拖动表格区域 3.拖动滚动条滑块 4.点击轨道快速滚动';
                controls.appendChild(tip);
                
                // 初始加载更多列的数据以确保出现水平滚动条
                setTimeout(() => {
                    const data = generateTestData(1000, 15);
                    const cols = generateColumns(15);
                    table.setData(data, cols);
                }, 100);
                
            } catch (error) {
                console.error('初始化错误:', error);
                document.getElementById('info').innerHTML = `
                    <strong style="color: red;">错误:</strong> ${error.message}
                `;
            }
        });
    </script>
</body>
</html>
  1. canvas+ 瓦片技术

瓦片技术(也称分块渲染)是一种将大画布分割成多个小"瓦片"(tiles)进行独立渲染和管理的技术,常用于地图、大图像、大数据可视化等场景。

多级瓦片:根据缩放级别选择合适的瓦片大小

将大的画布或数据集分割成多个固定大小的矩形块(瓦片),每个瓦片可以:
独立渲染:只渲染可见区域的瓦片
独立缓存:渲染过的瓦片可以缓存复用
按需加载:只在需要时才渲染或加载
并行处理:多个瓦片可以并行渲染

  1. Skia+WebAssembly

    Skia 是 Google 开发的 2D 图形引擎,WebAssembly 是高性能的 Web 二进制格式。它们的结合为 Web 图形渲染带来了革命性的性能提升。

Skia 是一个跨平台的 2D 图形库,被 Chrome、Firefox、Flutter、Android 等使用

主要特点:
硬件加速渲染(OpenGL、Vulkan、Metal)
丰富的图形 API(路径、渐变、滤镜、文本)
高性能的文本渲染(支持复杂文本布局)
跨平台(Windows、macOS、Linux、Android、iOS)
开源(BSD 许可证)

WebAssembly (Wasm) 是低级的类汇编语言,在浏览器中以接近原生速度运行

主要优势:
接近原生的性能(比 JS 快 5-10 倍)
紧凑的二进制格式
内存安全沙箱
跨平台(所有现代浏览器支持)

0

评论区