跟AI学一手之虚拟滚动

当前端需要展示的数据量比较大时,比如5万条,如果把全部数据都渲染到界面上,可能出现卡顿,虚拟滚动就是通过计算可见行,只渲染一小部分数据,达到提高性能的目的,下面是用 ai 写的一个vue3版的支持虚拟滚动的大数据表格,使用了 web worker技术,支持行高变化场景,有需要的可以参考下

javascript 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>大数据表格 - CPU 优化版</title>
    
    <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
    <script src="https://unpkg.com/dexie@3.2.4/dist/dexie.js"></script>
    
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }
        
        body {
            font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Arial, sans-serif;
            background: #f5f7fa;
            padding: 20px;
        }
        
        #app {
            max-width: 1400px;
            margin: 0 auto;
        }
        
        .header {
            display: flex;
            justify-content: space-between;
            align-items: center;
            padding: 16px 20px;
            background: white;
            border-radius: 8px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
            margin-bottom: 16px;
            flex-wrap: wrap;
            gap: 12px;
        }
        
        .header h1 {
            font-size: 20px;
            color: #1a2332;
        }
        
        .header-controls {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
            align-items: center;
        }
        
        .btn {
            padding: 8px 16px;
            border: none;
            border-radius: 6px;
            cursor: pointer;
            font-size: 14px;
            font-weight: 500;
            transition: all 0.2s;
        }
        
        .btn-primary {
            background: #4f6ef7;
            color: white;
        }
        .btn-primary:hover {
            background: #3a56d4;
        }
        .btn-primary:disabled {
            background: #a0b3f0;
            cursor: not-allowed;
        }
        
        .btn-success {
            background: #34c759;
            color: white;
        }
        .btn-success:hover {
            background: #28a745;
        }
        
        .btn-danger {
            background: #ff6b6b;
            color: white;
        }
        .btn-danger:hover {
            background: #e55a5a;
        }
        
        .btn-warning {
            background: #ffa94d;
            color: white;
        }
        .btn-warning:hover {
            background: #f59f3e;
        }
        
        .btn-outline {
            background: transparent;
            color: #4f6ef7;
            border: 1px solid #4f6ef7;
        }
        .btn-outline:hover {
            background: #4f6ef7;
            color: white;
        }
        
        .status-bar {
            padding: 10px 20px;
            background: white;
            border-radius: 8px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
            margin-bottom: 16px;
            display: flex;
            justify-content: space-between;
            align-items: center;
            flex-wrap: wrap;
            gap: 8px;
            font-size: 14px;
            color: #4a5568;
        }
        
        .status-bar .badge {
            display: inline-block;
            padding: 2px 10px;
            border-radius: 12px;
            font-size: 12px;
            font-weight: 600;
        }
        .badge-success {
            background: #d4edda;
            color: #155724;
        }
        .badge-warning {
            background: #fff3cd;
            color: #856404;
        }
        .badge-danger {
            background: #f8d7da;
            color: #721c24;
        }
        .badge-info {
            background: #d1ecf1;
            color: #0c5460;
        }
        
        .progress-bar {
            width: 200px;
            height: 6px;
            background: #e9ecef;
            border-radius: 3px;
            overflow: hidden;
        }
        .progress-bar .fill {
            height: 100%;
            background: linear-gradient(90deg, #4f6ef7, #34c759);
            transition: width 0.3s;
            border-radius: 3px;
        }
        
        /* ===== 表格 ===== */
        .table-container {
            background: white;
            border-radius: 8px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
            overflow: hidden;
            position: relative;
        }
        
        /* ✅ 使用 will-change 提示浏览器优化 */
        .table-scroll {
            overflow-y: auto;
            overflow-x: auto;
            height: 600px;
            position: relative;
            will-change: scroll-position;
        }
        
        .table-scroll::-webkit-scrollbar {
            width: 8px;
            height: 8px;
        }
        .table-scroll::-webkit-scrollbar-track {
            background: #f1f1f1;
        }
        .table-scroll::-webkit-scrollbar-thumb {
            background: #c1c7cd;
            border-radius: 4px;
        }
        .table-scroll::-webkit-scrollbar-thumb:hover {
            background: #a0a7ae;
        }
        
        .virtual-container {
            position: relative;
            width: 100%;
        }
        
        table {
            width: 100%;
            border-collapse: collapse;
            font-size: 14px;
            table-layout: fixed;
        }
        
        table colgroup .col-id { width: 60px; }
        table colgroup .col-name { width: 18%; }
        table colgroup .col-category { width: 12%; }
        table colgroup .col-price { width: 12%; }
        table colgroup .col-stock { width: 12%; }
        table colgroup .col-date { width: 16%; }
        table colgroup .col-action { width: 10%; }
        
        thead {
            position: sticky;
            top: 0;
            z-index: 10;
        }
        
        thead th {
            background: #f8f9fa;
            padding: 12px 16px;
            text-align: left;
            font-weight: 600;
            color: #1a2332;
            border-bottom: 2px solid #e9ecef;
            user-select: none;
            position: relative;
        }
        
        thead th.sortable {
            cursor: pointer;
        }
        thead th.sortable:hover {
            background: #e9ecef;
        }
        thead th .sort-icon {
            margin-left: 4px;
            font-size: 12px;
        }
        
        /* ✅ GPU 加速 */
        tbody tr {
            transition: background 0.1s;
            will-change: transform;
            transform: translateZ(0);
        }
        tbody tr:hover {
            background: #f7f9fc;
        }
        
        tbody td {
            padding: 8px 16px;
            border-bottom: 1px solid #f0f2f5;
            color: #2d3748;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
            vertical-align: middle;
            height: 44px;
        }
        
        tbody td .badge-info {
            background: #d1ecf1;
            color: #0c5460;
            padding: 2px 8px;
            border-radius: 4px;
            font-size: 12px;
            display: inline-block;
        }
        
        .expand-btn {
            background: transparent;
            border: none;
            cursor: pointer;
            font-size: 18px;
            color: #4f6ef7;
            padding: 4px 8px;
            border-radius: 4px;
            transition: background 0.2s;
        }
        .expand-btn:hover {
            background: #eef2ff;
        }
        
        .empty-state {
            text-align: center;
            padding: 60px 20px;
            color: #a0aec0;
        }
        .empty-state .icon {
            font-size: 48px;
            margin-bottom: 16px;
        }
        
        .spacer-row td {
            border: none;
            padding: 0;
        }
        
        .detail-row {
            background: #f8faff;
        }
        .detail-row td {
            padding: 12px 16px 16px 16px;
            border-bottom: 2px solid #e9ecef;
            white-space: normal;
            height: auto;
        }
        .detail-content {
            background: white;
            padding: 16px;
            border-radius: 6px;
            border: 1px solid #e9ecef;
            font-size: 13px;
            line-height: 1.8;
            color: #4a5568;
        }
        .detail-content .label {
            font-weight: 600;
            color: #1a2332;
            display: inline-block;
            width: 80px;
        }
        .detail-content .tag {
            display: inline-block;
            padding: 1px 8px;
            border-radius: 12px;
            font-size: 12px;
            margin-right: 4px;
        }
        .tag-blue { background: #dbeafe; color: #1e40af; }
        .tag-green { background: #d1fae5; color: #065f46; }
        .tag-red { background: #fee2e2; color: #991b1b; }
        .tag-yellow { background: #fef3c7; color: #92400e; }
        
        @media (max-width: 768px) {
            .header {
                flex-direction: column;
                align-items: stretch;
            }
            .header-controls {
                justify-content: stretch;
            }
            .header-controls .btn {
                flex: 1;
                text-align: center;
                font-size: 12px;
                padding: 6px 10px;
            }
            .status-bar {
                flex-direction: column;
                align-items: stretch;
                font-size: 12px;
            }
            .table-scroll {
                height: 400px;
            }
            table {
                font-size: 12px;
            }
            thead th, tbody td {
                padding: 6px 10px;
            }
        }
    </style>
</head>
<body>

<div id="app">
    <div class="header">
        <h1>📊 CPU 优化版 - 虚拟滚动</h1>
        <div class="header-controls">
            <button class="btn btn-primary" @click="() => generateData()" :disabled="loading">
                🚀 生成测试数据
            </button>
            <button class="btn btn-success" @click="() => loadAllData()" :disabled="loading || !hasData">
                📥 加载全部数据
            </button>
            <button class="btn btn-danger" @click="() => clearData()" :disabled="!hasData">
                🗑️ 清空
            </button>
        </div>
    </div>

    <div class="status-bar">
        <div>
            <span>📦 总记录: <strong>{{ totalCount.toLocaleString() }}</strong></span>
            <span style="margin-left: 16px;">📄 可见行: <strong>{{ visibleItems.length }}</strong></span>
            <span style="margin-left: 16px;">
                状态: 
                <span class="badge" :class="statusClass">{{ statusText }}</span>
            </span>
            <span style="margin-left: 16px; font-size: 12px; color: #999;" v-if="allLoaded">
                ✅ 已全部加载
            </span>
        </div>
        <div style="display: flex; align-items: center; gap: 12px;">
            <span style="font-size: 12px; color: #888;">{{ progressText }}</span>
            <div class="progress-bar" v-if="loading">
                <div class="fill" :style="{ width: progress + '%' }"></div>
            </div>
            <span style="font-size: 12px; color: #888;">⏱️ {{ queryTime }}ms</span>
            <span style="font-size: 12px; color: #888; background: #f0f0f0; padding: 2px 8px; border-radius: 4px;">
                🔥 CPU: {{ cpuUsage }}%
            </span>
        </div>
    </div>

    <div class="table-container">
        <!-- ✅ 滚动容器 - ref 直接绑定,不需要 querySelector -->
        <div class="table-scroll" ref="scrollContainer" @scroll="onScroll">
            <div class="virtual-container">
                <table>
                    <colgroup>
                        <col class="col-id" />
                        <col class="col-name" />
                        <col class="col-category" />
                        <col class="col-price" />
                        <col class="col-stock" />
                        <col class="col-date" />
                        <col class="col-action" />
                    </colgroup>
                    <thead>
                        <tr>
                            <th>#</th>
                            <th>名称</th>
                            <th>分类</th>
                            <th style="text-align: right;">价格</th>
                            <th style="text-align: right;">库存</th>
                            <th>创建时间</th>
                            <th style="text-align: center;">操作</th>
                        </tr>
                    </thead>
                    <tbody>
                        <!-- ✅ 顶部占位 -->
                        <tr class="spacer-row" v-if="topSpacer > 0">
                            <td colspan="7" :style="{ height: topSpacer + 'px', padding: 0 }"></td>
                        </tr>
                        
                        <!-- ✅ 可见行 -->
                        <tr 
                            v-for="item in visibleItems" 
                            :key="item.id"
                        >
                            <td>{{ item.id }}</td>
                            <td>{{ item.name }}</td>
                            <td>
                                <span class="badge-info">{{ item.category }}</span>
                            </td>
                            <td style="text-align: right; font-weight: 600;">
                                ¥{{ item.price.toFixed(2) }}
                            </td>
                            <td style="text-align: right;">
                                <span :style="{ 
                                    color: (item.stock || 0) < 10 ? '#e53e3e' : '#2d3748' 
                                }">
                                    {{ item.stock }}
                                </span>
                            </td>
                            <td style="font-size: 12px; color: #888;">
                                {{ formatDate(item.createdAt) }}
                            </td>
                            <td style="text-align: center;">
                                <button 
                                    class="expand-btn" 
                                    @click="() => toggleExpand(item.id)"
                                >
                                    {{ expandedSet.has(item.id) ? '🔽' : '▶️' }}
                                </button>
                            </td>
                        </tr>
                        
                        <!-- ✅ 底部占位 -->
                        <tr class="spacer-row" v-if="bottomSpacer > 0">
                            <td colspan="7" :style="{ height: bottomSpacer + 'px', padding: 0 }"></td>
                        </tr>
                        
                        <!-- 空状态 -->
                        <tr v-if="!hasData && !loading">
                            <td colspan="7">
                                <div class="empty-state">
                                    <div class="icon">📭</div>
                                    <p>暂无数据,点击 "生成测试数据" 开始</p>
                                </div>
                            </td>
                        </tr>
                    </tbody>
                </table>
            </div>
        </div>
    </div>
</div>

<script>
    // ============================================================
    // 1. Web Worker
    // ============================================================
    const workerCode = `
        importScripts('https://unpkg.com/dexie@3.2.4/dist/dexie.js');
        
        const db = new Dexie('BigDataDB');
        db.version(1).stores({
            items: '++id, name, category, price, stock, createdAt'
        });
        
        function generateMockData(count) {
            const categories = ['电子产品', '服装服饰', '食品饮料', '家居用品', '图书文具', '运动户外'];
            const names = ['商品', '产品', '物品', '货品', '精品', '优品'];
            const data = [];
            
            for (let i = 1; i <= count; i++) {
                const category = categories[Math.floor(Math.random() * categories.length)];
                const name = names[Math.floor(Math.random() * names.length)] + ' ' + i;
                data.push({
                    id: i,
                    name: name,
                    category: category,
                    price: Math.round((Math.random() * 900 + 100) * 100) / 100,
                    stock: Math.floor(Math.random() * 500),
                    createdAt: Date.now() - Math.floor(Math.random() * 90 * 24 * 60 * 60 * 1000),
                });
            }
            return data;
        }
        
        self.addEventListener('message', async function(e) {
            const { action, payload, requestId } = e.data;
            
            try {
                let result;
                switch (action) {
                    case 'GENERATE': {
                        const data = generateMockData(payload.count);
                        const BATCH_SIZE = 5000;
                        for (let i = 0; i < data.length; i += BATCH_SIZE) {
                            const batch = data.slice(i, i + BATCH_SIZE);
                            await db.items.bulkPut(batch);
                            const progress = Math.min(100, Math.round((i + batch.length) / data.length * 100));
                            self.postMessage({ type: 'PROGRESS', requestId, progress });
                        }
                        result = { count: data.length };
                        break;
                    }
                    
                    case 'LOAD_ALL': {
                        const items = await db.items.orderBy('id').toArray();
                        result = { items, total: items.length };
                        break;
                    }
                    
                    case 'COUNT': {
                        result = await db.items.count();
                        break;
                    }
                    
                    case 'CLEAR': {
                        await db.items.clear();
                        result = { cleared: true };
                        break;
                    }
                    
                    default:
                        throw new Error('未知操作: ' + action);
                }
                
                self.postMessage({ type: 'RESULT', requestId, result });
                
            } catch (error) {
                self.postMessage({ type: 'ERROR', requestId, error: error.message });
            }
        });
        
        self.postMessage({ type: 'READY' });
    `;

    const blob = new Blob([workerCode], { type: 'application/javascript' });
    const workerUrl = URL.createObjectURL(blob);

    // ============================================================
    // 2. Vue 3 应用
    // ============================================================
    const { createApp, ref, computed, onMounted, onBeforeUnmount, nextTick } = Vue;

    const app = createApp({
        setup() {
            // ---------- 状态 ----------
            const loading = ref(false);
            const progress = ref(0);
            const progressText = ref('');
            const totalCount = ref(0);
            const queryTime = ref(0);
            const statusText = ref('就绪');
            const cpuUsage = ref(0);
            
            const allItems = ref([]);
            const allLoaded = ref(false);
            
            const expandedSet = ref(new Set());
            const visibleItems = ref([]);
            const topSpacer = ref(0);
            const bottomSpacer = ref(0);
            
            // ---------- 滚动优化相关 ----------
            const scrollContainer = ref(null);
            let rafId = null;
            let scrollTimer = null;
            let isScrolling = false;
            let lastStart = -1;
            let lastEnd = -1;
            
            // CPU 监控
            let cpuMonitorTimer = null;
            let updateCount = 0;
            let cpuStartTime = 0;
            
            // 固定参数
            const ROW_HEIGHT = 44;
            const BUFFER_SIZE = 5; // ✅ 减少缓冲区
            let containerHeight = 600;
            
            // Worker
            let worker = null;
            let requestIdCounter = 0;
            const pendingRequests = new Map();
            
            // ---------- 计算 ----------
            const hasData = computed(() => totalCount.value > 0);
            
            const statusClass = computed(() => {
                if (loading.value) return 'badge-warning';
                if (totalCount.value > 0) return 'badge-success';
                return 'badge-info';
            });
            
            // ---------- 辅助 ----------
            function formatDate(timestamp) {
                if (!timestamp) return '-';
                const date = new Date(timestamp);
                return date.toLocaleDateString('zh-CN');
            }
            
            // ---------- Worker 通信 ----------
            function sendToWorker(action, payload = {}) {
                return new Promise((resolve, reject) => {
                    if (!worker) {
                        reject(new Error('Worker 未初始化'));
                        return;
                    }
                    const requestId = ++requestIdCounter;
                    pendingRequests.set(requestId, { resolve, reject });
                    worker.postMessage({ action, payload, requestId });
                    
                    setTimeout(() => {
                        if (pendingRequests.has(requestId)) {
                            pendingRequests.delete(requestId);
                            reject(new Error('请求超时'));
                        }
                    }, 60000);
                });
            }
            
            // ---------- ✅ 核心:优化后的虚拟滚动 ----------
            function updateVisibleItems() {
                const container = scrollContainer.value;
                if (!container || allItems.value.length === 0) {
                    visibleItems.value = [];
                    topSpacer.value = 0;
                    bottomSpacer.value = 0;
                    return;
                }
                
                const scrollTop = container.scrollTop;
                const height = container.clientHeight || containerHeight;
                const data = allItems.value;
                const totalRows = data.length;
                
                // 计算可见范围
                const start = Math.max(0, Math.floor(scrollTop / ROW_HEIGHT) - BUFFER_SIZE);
                const end = Math.min(
                    totalRows,
                    Math.ceil((scrollTop + height) / ROW_HEIGHT) + BUFFER_SIZE
                );
                
                // ✅ 只有范围变化时才更新
                if (start === lastStart && end === lastEnd) {
                    return;
                }
                
                lastStart = start;
                lastEnd = end;
                
                // 截取数据
                visibleItems.value = data.slice(start, end);
                
                // 计算占位高度
                topSpacer.value = start * ROW_HEIGHT;
                bottomSpacer.value = Math.max(0, (totalRows - end) * ROW_HEIGHT);
                
                // CPU 使用率统计
                updateCount++;
            }
            
            // ✅ 滚动事件 - 完整的优化方案
            function onScroll() {
                // 1. 使用 RAF 节流
                if (rafId) return;
                
                rafId = requestAnimationFrame(() => {
                    updateVisibleItems();
                    rafId = null;
                });
                
                // 2. 滚动停止后更新
                clearTimeout(scrollTimer);
                if (!isScrolling) {
                    isScrolling = true;
                }
                scrollTimer = setTimeout(() => {
                    isScrolling = false;
                    requestAnimationFrame(() => {
                        updateVisibleItems();
                    });
                }, 150);
            }
            
            // ✅ 刷新虚拟滚动
            function refreshVirtualScroll() {
                lastStart = -1;
                lastEnd = -1;
                nextTick(() => {
                    updateVisibleItems();
                });
            }
            
            // ---------- 展开/折叠 ----------
            function toggleExpand(id) {
                if (expandedSet.value.has(id)) {
                    expandedSet.value.delete(id);
                } else {
                    expandedSet.value.add(id);
                }
                // 展开/折叠会影响行高,需要刷新
                refreshVirtualScroll();
            }
            
            // ---------- 数据操作 ----------
            async function generateData() {
                if (loading.value) return;
                
                const count = 30000;
                if (!confirm(`确定生成 ${count.toLocaleString()} 条测试数据吗?`)) return;
                
                loading.value = true;
                progress.value = 0;
                progressText.value = '正在生成数据...';
                statusText.value = '生成中';
                allLoaded.value = false;
                
                const startTime = performance.now();
                
                try {
                    const result = await sendToWorker('GENERATE', { count });
                    
                    queryTime.value = Math.round(performance.now() - startTime);
                    totalCount.value = result.count;
                    statusText.value = `✅ 已生成 ${result.count.toLocaleString()} 条`;
                    progress.value = 100;
                    progressText.value = '完成!';
                    
                    await loadAllData();
                    
                } catch (error) {
                    console.error('生成失败:', error);
                    statusText.value = '❌ 生成失败';
                    alert('生成数据失败: ' + error.message);
                } finally {
                    loading.value = false;
                }
            }
            
            async function loadAllData() {
                if (loading.value) return;
                
                if (totalCount.value === 0) {
                    try {
                        const count = await sendToWorker('COUNT');
                        if (count === 0) {
                            statusText.value = '暂无数据,请先生成';
                            return;
                        }
                        totalCount.value = count;
                    } catch (e) {
                        console.warn('读取总数失败:', e);
                        return;
                    }
                }
                
                loading.value = true;
                progressText.value = '正在加载全部数据到内存...';
                statusText.value = '加载中';
                
                const startTime = performance.now();
                
                try {
                    const result = await sendToWorker('LOAD_ALL');
                    
                    allItems.value = result.items;
                    totalCount.value = result.total;
                    allLoaded.value = true;
                    
                    queryTime.value = Math.round(performance.now() - startTime);
                    statusText.value = `✅ 已加载 ${result.total.toLocaleString()} 条`;
                    progressText.value = '';
                    
                    expandedSet.value = new Set();
                    
                    if (scrollContainer.value) {
                        scrollContainer.value.scrollTop = 0;
                    }
                    
                    refreshVirtualScroll();
                    
                } catch (error) {
                    console.error('加载失败:', error);
                    statusText.value = '❌ 加载失败';
                } finally {
                    loading.value = false;
                }
            }
            
            async function clearData() {
                if (!confirm('确定清空所有数据吗?')) return;
                
                loading.value = true;
                progressText.value = '正在清空...';
                statusText.value = '清空中';
                
                try {
                    await sendToWorker('CLEAR');
                    totalCount.value = 0;
                    allItems.value = [];
                    visibleItems.value = [];
                    allLoaded.value = false;
                    statusText.value = '已清空';
                    progressText.value = '';
                    expandedSet.value = new Set();
                    topSpacer.value = 0;
                    bottomSpacer.value = 0;
                    lastStart = -1;
                    lastEnd = -1;
                } catch (error) {
                    console.error('清空失败:', error);
                    statusText.value = '❌ 清空失败';
                } finally {
                    loading.value = false;
                }
            }
            
            // ---------- CPU 监控(模拟) ----------
            function startCpuMonitor() {
                cpuStartTime = performance.now();
                updateCount = 0;
                
                cpuMonitorTimer = setInterval(() => {
                    const elapsed = (performance.now() - cpuStartTime) / 1000;
                    if (elapsed > 0) {
                        // 估算 CPU 使用率(基于更新频率)
                        const updatesPerSecond = updateCount / elapsed;
                        // 每帧更新 60 次 = 100% CPU(理论值)
                        const usage = Math.min(100, Math.round((updatesPerSecond / 60) * 100));
                        cpuUsage.value = usage;
                    }
                }, 1000);
            }
            
            // ---------- 生命周期 ----------
            onMounted(() => {
                // 创建 Worker
                worker = new Worker(workerUrl);
                
                worker.onmessage = (event) => {
                    const { type, requestId, result, error, progress: prog } = event.data;
                    
                    if (type === 'READY') {
                        console.log('✅ Worker 已就绪');
                        statusText.value = 'Worker 就绪';
                        return;
                    }
                    
                    if (type === 'PROGRESS') {
                        progress.value = prog;
                        progressText.value = `生成中... ${prog}%`;
                        return;
                    }
                    
                    if (type === 'RESULT') {
                        const pending = pendingRequests.get(requestId);
                        if (pending) {
                            pending.resolve(result);
                            pendingRequests.delete(requestId);
                        }
                        return;
                    }
                    
                    if (type === 'ERROR') {
                        const pending = pendingRequests.get(requestId);
                        if (pending) {
                            pending.reject(new Error(error));
                            pendingRequests.delete(requestId);
                        }
                        return;
                    }
                };
                
                worker.onerror = (error) => {
                    console.error('Worker 错误:', error);
                    statusText.value = '❌ Worker 错误';
                };
                
                // 初始化滚动容器高度
                if (scrollContainer.value) {
                    containerHeight = scrollContainer.value.clientHeight || 600;
                }
                
                // 启动 CPU 监控
                startCpuMonitor();
                
                // 尝试恢复数据
                setTimeout(async () => {
                    try {
                        const count = await sendToWorker('COUNT');
                        if (count > 0) {
                            totalCount.value = count;
                            await loadAllData();
                        }
                    } catch (e) {
                        // 忽略
                    }
                }, 500);
            });
            
            onBeforeUnmount(() => {
                // 清理所有资源
                if (rafId) {
                    cancelAnimationFrame(rafId);
                    rafId = null;
                }
                clearTimeout(scrollTimer);
                if (cpuMonitorTimer) {
                    clearInterval(cpuMonitorTimer);
                    cpuMonitorTimer = null;
                }
                if (worker) {
                    worker.terminate();
                    worker = null;
                }
                URL.revokeObjectURL(workerUrl);
            });
            
            // ---------- 返回值 ----------
            return {
                loading,
                progress,
                progressText,
                totalCount,
                queryTime,
                statusText,
                statusClass,
                hasData,
                allLoaded,
                visibleItems,
                topSpacer,
                bottomSpacer,
                scrollContainer,
                expandedSet,
                cpuUsage,
                
                generateData,
                loadAllData,
                clearData,
                toggleExpand,
                onScroll,
                formatDate
            };
        }
    });

    app.mount('#app');
    
    // ============================================================
    // 3. 控制台辅助 - 测试滚动事件频率
    // ============================================================
    console.log('💡 在控制台运行以下代码测试滚动性能:');
    console.log(`
    // 测试滚动事件频率
    let count = 0;
    let lastTime = Date.now();
    const container = document.querySelector('.table-scroll');
    if (container) {
        container.addEventListener('scroll', () => {
            count++;
            const now = Date.now();
            if (now - lastTime > 1000) {
                console.log(\`每秒触发 \${count} 次滚动事件\`);
                count = 0;
                lastTime = now;
            }
        });
        console.log('✅ 已开始监听,滚动试试看');
    } else {
        console.log('❌ 未找到滚动容器');
    }
    `);
</script>

</body>
</html>