Skip to content

千万行表格展示优化

项目场景

  • 项目中需要展示一个大量行的排班表表格,表格中每一行都有多个单元格,同时每个单元格中都是一个下拉选择框。
  • 排班表中行是人员,列是时间(每行有90列,代表一个月内30天每天的早上、下午、晚上的排班班次)。
  • 当班组内的人员数量超过100人时,表格的性能会下降,导致页面卡顿。

出现性能瓶颈的核心原因是一次性渲染大量 DOM 元素(9000 个下拉框)会导致浏览器 DOM 树构建、重排重绘压力剧增,直接表现为页面加载卡顿、操作(如滚动、点击)延迟。

  1. DOM 数量过载:90列×100行=9000个单元格,每个单元格包含下拉框(通常由 <select>+多个<option> 组成),实际 DOM 数量远超9000,浏览器解析和渲染这些元素会占用大量内存和 CPU。

  2. 重排重绘频繁:大量元素同时存在时,任何微小操作(如下拉框展开/收起、滚动页面)都可能触发浏览器重排(计算元素位置)和重绘(像素渲染),导致界面响应变慢。

  3. 事件绑定与内存占用:每个下拉框可能需要绑定点击、change 等事件,9000个元素的事件绑定会占用额外内存,且事件触发时的处理也会增加性能开销。

所以需要优化表格的展示,在不影响基础功能的前提下,提升页面性能。

需求分析

实现一个能够高效展示千万行数据的表格组件,并支持点击下拉修改功能。

有三种不同的技术方案:

  1. 使用原生table元素
  2. 使用虚拟滚动技术
  3. 使用Canvas渲染

方案设计与实现

1. 原生Table元素方案

设计思路

直接使用HTML的table元素渲染所有数据,简单但性能较差,不适合大数据量。

核心代码

vue
<template>
  <div class="table-container">
    <table>
      <thead>
        <tr>
          <!-- 遍历列配置,渲染表头 -->
          <th v-for="col in columns" :key="col.key">{{ col.title }}</th>
        </tr>
      </thead>
      <tbody>
        <!-- 遍历数据行 -->
        <tr v-for="(row, index) in data" :key="index">
          <!-- 遍历列配置,渲染单元格 -->
          <td v-for="col in columns" :key="col.key">
            <!-- 非可编辑列显示文本 -->
            <span v-if="!col.editable">{{ row[col.key] }}</span>
            <!-- 可编辑列显示下拉选择框 -->
            <select v-else v-model="row[col.key]" @change="updateCell(row, col.key)">
              <option v-for="option in col.options" :value="option.value">
                {{ option.label }}
              </option>
            </select>
          </td>
        </tr>
      </tbody>
    </table>
  </div>
</template>

<script setup>
import { ref } from 'vue'

// 接收列配置和数据
const props = defineProps({
  columns: Array,
  data: Array
})

// 单元格更新处理函数
const updateCell = (row, key) => {
  console.log('Updated:', row, key)
  // 在实际应用中,这里可以发送更新请求等操作
}
</script>

2. 虚拟表格方案

设计思路

只渲染可视区域内的行,通过计算滚动位置动态更新显示内容,极大提升性能。

核心代码

vue
<template>
  <div class="virtual-table" @scroll="handleScroll">
    <!-- 表头部分 -->
    <div class="table-header">
      <div v-for="col in columns" :key="col.key" class="header-cell">
        {{ col.title }}
      </div>
    </div>
    
    <!-- 表格主体,高度为所有行的总高度 -->
    <div class="table-body" :style="{ height: totalHeight + 'px' }">
      <!-- 只渲染可见区域的行 -->
      <div 
        v-for="index in visibleData" 
        :key="index" 
        class="table-row" 
        :style="{ top: (index * rowHeight) + 'px' }"
      >
        <!-- 渲染每个单元格 -->
        <div v-for="col in columns" :key="col.key" class="table-cell">
          <!-- 非可编辑列显示文本 -->
          <span v-if="!col.editable">{{ data[index][col.key] }}</span>
          <!-- 可编辑列显示下拉选择框 -->
          <select 
            v-else 
            v-model="data[index][col.key]" 
            @change="updateCell(data[index], col.key)"
          >
            <option v-for="option in col.options" :value="option.value">
              {{ option.label }}
            </option>
          </select>
        </div>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'

// 接收列配置和数据
const props = defineProps({
  columns: Array,
  data: Array
})

// 定义常量:行高
const rowHeight = 40
// 可见行数
const visibleCount = ref(0)
// 起始行索引
const startIndex = ref(0)
// 滚动位置
const scrollTop = ref(0)

// 计算属性:表格总高度
const totalHeight = computed(() => props.data.length * rowHeight)

// 计算属性:可见数据范围
const visibleData = computed(() => {
  const endIndex = startIndex.value + visibleCount.value
  return props.data.slice(startIndex.value, endIndex)
})

// 滚动事件处理
const handleScroll = (e) => {
  scrollTop.value = e.target.scrollTop
  // 根据滚动位置计算起始行索引
  startIndex.value = Math.floor(scrollTop.value / rowHeight)
}

// 组件挂载后计算可见行数
onMounted(() => {
  const container = document.querySelector('.virtual-table')
  visibleCount.value = Math.ceil(container.clientHeight / rowHeight)
})
</script>

3. Canvas表格方案

设计思路

使用Canvas绘制整个表格,完全绕过DOM操作,性能最佳。

核心代码

vue
<template>
  <div class="canvas-container">
    <!-- Canvas元素用于绘制表格 -->
    <canvas ref="canvas" @click="handleClick" @scroll="handleScroll"></canvas>
    
    <!-- 下拉选择框,用于编辑单元格 -->
    <div v-if="showDropdown" class="dropdown" :style="dropdownStyle">
      <select v-model="selectedValue" @change="updateSelectedCell" size="5">
        <option v-for="option in currentOptions" :value="option.value">
          {{ option.label }}
        </option>
      </select>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, watch, computed } from 'vue'

// 接收列配置和数据
const props = defineProps({
  columns: Array,
  data: Array
})

// Canvas相关引用
const canvas = ref(null)
const ctx = ref(null)

// 下拉框状态
const showDropdown = ref(false)
const selectedValue = ref('')
const currentCell = ref(null)
const scrollTop = ref(0)

// 计算属性:当前单元格的可选选项
const currentOptions = computed(() => {
  const col = props.columns.find(col => col.key === currentCell.value?.key)
  return col?.options || []
})

// 计算属性:下拉框位置样式
const dropdownStyle = computed(() => {
  if (!currentCell.value) return {}
  return {
    left: currentCell.value.x + 'px',
    top: (currentCell.value.y - scrollTop.value) + 'px'
  }
})

// 组件挂载后初始化Canvas
onMounted(() => {
  ctx.value = canvas.value.getContext('2d')
  initCanvas()
  renderTable()
})

// 初始化Canvas尺寸
const initCanvas = () => {
  canvas.value.width = canvas.value.offsetWidth
  canvas.value.height = canvas.value.offsetHeight
}

// 渲染表格内容
const renderTable = () => {
  if (!ctx.value) return
  
  // 清空画布
  ctx.value.clearRect(0, 0, canvas.value.width, canvas.value.height)
  
  // 绘制表头
  ctx.value.fillStyle = '#f5f5f5'
  ctx.value.fillRect(0, 0, canvas.value.width, 40)
  
  let x = 0
  props.columns.forEach(col => {
    ctx.value.fillStyle = '#333'
    ctx.value.font = '14px Arial'
    ctx.value.fillText(col.title, x + 10, 25)
    
    // 绘制列宽
    const colWidth = col.width || 100
    x += colWidth
  })
  
  // 计算可见行范围
  const startRow = Math.floor(scrollTop.value / 40)
  const visibleRowCount = Math.ceil(canvas.value.height / 40)
  
  // 绘制数据行(只绘制可见部分)
  for (let i = startRow; i < Math.min(startRow + visibleRowCount, props.data.length); i++) {
    const row = props.data[i]
    const y = i * 40 - scrollTop.value
    
    // 交替行背景色
    ctx.value.fillStyle = i % 2 === 0 ? '#fff' : '#f9f9f9'
    ctx.value.fillRect(0, y, canvas.value.width, 40)
    
    // 绘制单元格内容
    let cellX = 0
    props.columns.forEach(col => {
      const colWidth = col.width || 100
      
      ctx.value.fillStyle = '#333'
      ctx.value.font = '14px Arial'
      ctx.value.fillText(row[col.key], cellX + 10, y + 25)
      
      cellX += colWidth
    })
  }
}

// 处理Canvas点击事件
const handleClick = (e) => {
  const rect = canvas.value.getBoundingClientRect()
  const x = e.clientX - rect.left
  const y = e.clientY - rect.top + scrollTop.value
  
  // 计算点击的单元格行索引
  const rowIndex = Math.floor(y / 40)
  // 排除表头
  if (rowIndex < 1 || rowIndex >= props.data.length) return
  
  // 计算点击的单元格列索引
  let colX = 0
  let colIndex = 0
  for (let i = 0; i < props.columns.length; i++) {
    const colWidth = props.columns[i].width || 100
    if (x >= colX && x < colX + colWidth) {
      colIndex = i
      break
    }
    colX += colWidth
  }
  
  // 获取列配置
  const column = props.columns[colIndex]
  // 如果是可编辑列,显示下拉框
  if (column.editable) {
    currentCell.value = {
      row: rowIndex,
      key: column.key,
      x: colX,
      y: rowIndex * 40
    }
    selectedValue.value = props.data[rowIndex][column.key]
    showDropdown.value = true
  } else {
    showDropdown.value = false
  }
}

// 更新选中的单元格
const updateSelectedCell = () => {
  if (currentCell.value) {
    props.data[currentCell.value.row][currentCell.value.key] = selectedValue.value
    showDropdown.value = false
    // 重新渲染表格
    renderTable()
  }
}

// 处理滚动事件
const handleScroll = (e) => {
  scrollTop.value = e.target.scrollTop
  // 滚动时重新渲染表格
  renderTable()
}
</script>

Canvas方案的优势

  1. 极致性能

    • 完全绕过DOM操作,避免大量节点创建和渲染
    • 只绘制可视区域内容,内存占用极低
    • 滚动时只需重新绘制,无需重新计算布局
  2. 内存效率

    • 千万行数据只需存储数据本身,无需创建对应DOM节点
    • 无论数据量多大,Canvas渲染开销基本恒定
  3. 渲染控制

    • 可以精细控制渲染过程,实现各种优化策略
    • 支持增量渲染和脏矩形优化
  4. 灵活性

    • 可以轻松实现复杂表格样式和交互效果
    • 支持自定义绘制逻辑和动画效果
  5. 跨平台一致性

    • Canvas在不同浏览器和设备上表现一致
    • 不受CSS兼容性问题影响

总结

对于千万行数据的表格展示,Canvas方案具有明显优势:

  • 性能远超DOM方案,特别是在大数据量场景下
  • 内存占用极低,不会因数据量增加而线性增长
  • 提供更灵活的渲染控制和优化空间

虚拟表格方案是次优选择,在性能和开发复杂度之间取得平衡,而原生table方案只适用于少量数据场景。

其次当要使用Canvas方案处理超大数据集,可以结合虚拟表格化技术实现最佳性能。