万级数据表格卡死?Web Worker 一招搞定

一、前言

后端一次性返回几万~十万条数据时,前端直接渲染会导致:
主线程阻塞、页面卡顿 / 卡死
表格渲染白屏
滚动、点击交互无响应
核心方案:
Web Worker:在后台线程处理数据(过滤、排序、格式化),不阻塞 UI
虚拟滚动:只渲染可视区域的 DOM,而非全部数据
主线程只负责渲染,复杂计算交给 Worker

二、效果图

想实现图中这样5w条数据,怎么保证前端页面不卡顿呢?!


demo

三、方案

3.1 创建 Web Worker(核心:搜索/筛选/排序都在这里)

    const workerBlob = new Blob([`
      // 原始数据
      let originData = [];

      // 接收主线程消息:这里面的代码,都在后台跑!复杂计算、处理数据都放这里!
      self.onmessage = (e) => {
        const { type, data, searchText, status, sortKey, sortOrder } = e.data;

        if (type === 'init') {
          // 初始化:生成模拟 5万 条结构化数据
          originData = data.map(item => ({
            id: item.id,
            name: \`用户-\${item.id}\`,
            age: Math.floor(Math.random() * 50) + 18,
            city: \`城市-\${Math.floor(item.id / 1000)}\`,
            phone: \`138\${Math.random().toString().slice(2, 10)}\`,
            status: item.id % 2 === 0 ? '正常' : '异常'
          }));
          self.postMessage({ type: 'initDone', data: originData });
        }

        if (type === 'filter') {
          // 全部复杂计算在 Worker 执行!
          let result = [...originData];

          // 1. 状态筛选
          if (status) {
            result = result.filter(item => item.status === status);
          }

          // 2. 全局搜索
          if (searchText) {
            const s = searchText.toLowerCase();
            result = result.filter(item => 
              item.id.toString().includes(s) ||
              item.name.toLowerCase().includes(s) ||
              item.city.toLowerCase().includes(s) ||
              item.phone.includes(s)
            );
          }

          // 3. 排序
          if (sortKey) {
            result.sort((a, b) => {
              if (sortOrder === 'asc') {
                return a[sortKey] > b[sortKey] ? 1 : -1;
              } else {
                return a[sortKey] < b[sortKey] ? 1 : -1;
              }
            });
          }

          // 返回给主线程
          self.postMessage({ type: 'filterDone', data: result });
        }
      };
    `], { type: 'application/javascript' });

    const worker = new Worker(URL.createObjectURL(workerBlob)); // new Worker()必须是文件地址!

3.2 模拟请求 50000 条数据

function fetchBigData() {
      const rawData = Array.from({ length: 50000 }, (_, i) => ({ id: i + 1 }));
      worker.postMessage({ type: 'init', data: rawData });
}

3.3 接收 Worker 消息

worker.onmessage = (e) => {
      const { type, data } = e.data;
      if (type === 'initDone' || type === 'filterDone') {
        loading.style.display = 'none';
        displayData = data;
        renderVirtualList();
      }
};

3.4 虚拟滚动渲染

    function renderVirtualList() {
      const totalHeight = displayData.length * ROW_HEIGHT;
      tableContent.style.height = totalHeight + 'px';
      tableContent.style.position = 'relative';

      const scrollTop = container.scrollTop;
      const start = Math.floor(scrollTop / ROW_HEIGHT);
      const end = Math.ceil((scrollTop + VIEW_HEIGHT) / ROW_HEIGHT);
      const visibleList = displayData.slice(start, end);

      let html = `
        <div class="table-header">
          <div class="table-col sort-header" data-sort="id">ID ↑↓</div>
          <div class="table-col sort-header" data-sort="name">姓名 ↑↓</div>
          <div class="table-col sort-header" data-sort="age">年龄 ↑↓</div>
          <div class="table-col">城市</div>
          <div class="table-col">手机号</div>
          <div class="table-col sort-header" data-sort="status">状态 ↑↓</div>
        </div>
      `;

      visibleList.forEach((item, index) => {
        const top = (start + index + 1) * ROW_HEIGHT;
        html += `
          <div class="table-row" style="top:${top}px;position:absolute;">
            <div class="table-col">${item.id}</div>
            <div class="table-col">${item.name}</div>
            <div class="table-col">${item.age}</div>
            <div class="table-col">${item.city}</div>
            <div class="table-col">${item.phone}</div>
            <div class="table-col">${item.status}</div>
          </div>
        `;
      });

      tableContent.innerHTML = html;
    }

3.5 搜索、筛选、排序、重置

let currentSort = { key: '', order: 'asc' };

    // 防抖(避免输入卡顿)
    function debounce(fn, delay = 300) {
      let timer = null;
      return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => fn(...args), delay);
      };
    }

    // 发送筛选请求给 Worker
    function sendFilter() {
      loading.style.display = 'block';
      worker.postMessage({
        type: 'filter',
        searchText: searchInput.value.trim(),
        status: statusFilter.value,
        sortKey: currentSort.key,
        sortOrder: currentSort.order
      });
    }

    // 搜索
    searchInput.oninput = debounce(sendFilter);

    // 状态筛选
    statusFilter.onchange = sendFilter;

    // 排序(点击表头)
    tableContent.addEventListener('click', (e) => {
      const el = e.target.closest('.sort-header');
      if (!el) return;
      const key = el.dataset.sort;
      if (currentSort.key === key) {
        currentSort.order = currentSort.order === 'asc' ? 'desc' : 'asc';
      } else {
        currentSort.key = key;
        currentSort.order = 'asc';
      }
      sendFilter();
    });

    // 重置
    resetBtn.onclick = () => {
      searchInput.value = '';
      statusFilter.value = '';
      currentSort = { key: '', order: 'asc' };
      sendFilter();
    };

四、js完整版代码(直接复制运行)

Web Worker + 虚拟滚动 + 搜索 + 多字段筛选 + 排序,全部不卡页面,5 万条数据流畅运行。
所有搜索、筛选、排序逻辑全部跑在 Web Worker 里,主线程只负责渲染,绝对不阻塞页面!

<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8">
  <title>5万条数据表格 + Web Worker + 搜索筛选排序</title>
  <style>
    * { margin: 0; padding: 0; box-sizing: border-box; font-family: system-ui; }
    .container {
      width: 1200px;
      margin: 30px auto;
    }
    .toolbar {
      display: flex;
      gap: 12px;
      margin-bottom: 15px;
      flex-wrap: wrap;
    }
    .search {
      padding: 8px 12px;
      width: 280px;
      border: 1px solid #ddd;
      border-radius: 4px;
    }
    .select {
      padding: 8px 12px;
      border: 1px solid #ddd;
      border-radius: 4px;
    }
    .btn {
      padding: 8px 16px;
      background: #1677ff;
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    .table-container {
      border: 1px solid #eee;
      overflow: auto;
      height: 600px;
      position: relative;
    }
    .table-header {
      display: flex;
      background: #f9f9f9;
      position: sticky;
      top: 0;
      z-index: 10;
      font-weight: 500;
      height: 42px;
      line-height: 42px;
    }
    .table-row {
      display: flex;
      border-bottom: 1px solid #f4f4f4;
      width:100%;
      height: 42px;
      line-height: 42px;
    }
    .table-col {
      flex: 1;
      padding: 0 12px;
      border-right: 1px solid #f4f4f4;
      white-space: nowrap;
      overflow: hidden;
      text-overflow: ellipsis;
    }
    .loading {
      text-align: center;
      padding: 30px;
      color: #666;
    }
    .sort-header {
      cursor: pointer;
      user-select: none;
    }
  </style>
</head>
<body>
  <div class="container">
    <div class="toolbar">
      <input 
        type="text" 
        class="search" 
        id="searchInput" 
        placeholder="搜索 ID / 姓名 / 城市 / 手机号"
      >
      <select class="select" id="statusFilter">
        <option value="">全部状态</option>
        <option value="正常">正常</option>
        <option value="异常">异常</option>
      </select>
      <button class="btn" id="resetBtn">重置</button>
    </div>

    <div class="table-container" id="tableContainer">
      <div class="loading" id="loading">Web Worker 处理数据中...</div>
      <div id="tableContent"></div>
    </div>
  </div>

  <script>
    const container = document.getElementById('tableContainer');
    const tableContent = document.getElementById('tableContent');
    const loading = document.getElementById('loading');
    const searchInput = document.getElementById('searchInput');
    const statusFilter = document.getElementById('statusFilter');
    const resetBtn = document.getElementById('resetBtn');

    const ROW_HEIGHT = 42;
    const VIEW_HEIGHT = 600;
    let totalData = [];       // 原始数据
    let displayData = [];     // 展示数据(搜索筛选后)

    // =============== 1. 创建 Web Worker(核心:搜索/筛选/排序都在这里)===============
    const workerBlob = new Blob([`
      // 原始数据
      let originData = [];

      // 接收主线程消息
      self.onmessage = (e) => {
        const { type, data, searchText, status, sortKey, sortOrder } = e.data;

        if (type === 'init') {
          // 初始化:生成模拟 5万 条结构化数据
          originData = data.map(item => ({
            id: item.id,
            name: \`用户-\${item.id}\`,
            age: Math.floor(Math.random() * 50) + 18,
            city: \`城市-\${Math.floor(item.id / 1000)}\`,
            phone: \`138\${Math.random().toString().slice(2, 10)}\`,
            status: item.id % 2 === 0 ? '正常' : '异常'
          }));
          self.postMessage({ type: 'initDone', data: originData });
        }

        if (type === 'filter') {
          // 全部复杂计算在 Worker 执行!
          let result = [...originData];

          // 1. 状态筛选
          if (status) {
            result = result.filter(item => item.status === status);
          }

          // 2. 全局搜索
          if (searchText) {
            const s = searchText.toLowerCase();
            result = result.filter(item => 
              item.id.toString().includes(s) ||
              item.name.toLowerCase().includes(s) ||
              item.city.toLowerCase().includes(s) ||
              item.phone.includes(s)
            );
          }

          // 3. 排序
          if (sortKey) {
            result.sort((a, b) => {
              if (sortOrder === 'asc') {
                return a[sortKey] > b[sortKey] ? 1 : -1;
              } else {
                return a[sortKey] < b[sortKey] ? 1 : -1;
              }
            });
          }

          // 返回给主线程
          self.postMessage({ type: 'filterDone', data: result });
        }
      };
    `], { type: 'application/javascript' });

    const worker = new Worker(URL.createObjectURL(workerBlob));

    // =============== 2. 模拟请求 50000 条数据 ===============
    function fetchBigData() {
      const rawData = Array.from({ length: 50000 }, (_, i) => ({ id: i + 1 }));
      worker.postMessage({ type: 'init', data: rawData });
    }

    // =============== 3. 接收 Worker 消息 ===============
    worker.onmessage = (e) => {
      const { type, data } = e.data;
      if (type === 'initDone' || type === 'filterDone') {
        loading.style.display = 'none';
        displayData = data;
        renderVirtualList();
      }
    };

    // =============== 4. 虚拟滚动渲染 ===============
    function renderVirtualList() {
      const totalHeight = displayData.length * ROW_HEIGHT;
      tableContent.style.height = totalHeight + 'px';
      tableContent.style.position = 'relative';

      const scrollTop = container.scrollTop;
      const start = Math.floor(scrollTop / ROW_HEIGHT);
      const end = Math.ceil((scrollTop + VIEW_HEIGHT) / ROW_HEIGHT);
      const visibleList = displayData.slice(start, end);

      let html = `
        <div class="table-header">
          <div class="table-col sort-header" data-sort="id">ID ↑↓</div>
          <div class="table-col sort-header" data-sort="name">姓名 ↑↓</div>
          <div class="table-col sort-header" data-sort="age">年龄 ↑↓</div>
          <div class="table-col">城市</div>
          <div class="table-col">手机号</div>
          <div class="table-col sort-header" data-sort="status">状态 ↑↓</div>
        </div>
      `;

      visibleList.forEach((item, index) => {
        const top = (start + index + 1) * ROW_HEIGHT;
        html += `
          <div class="table-row" style="top:${top}px;position:absolute;">
            <div class="table-col">${item.id}</div>
            <div class="table-col">${item.name}</div>
            <div class="table-col">${item.age}</div>
            <div class="table-col">${item.city}</div>
            <div class="table-col">${item.phone}</div>
            <div class="table-col">${item.status}</div>
          </div>
        `;
      });

      tableContent.innerHTML = html;
    }

    // =============== 5. 搜索、筛选、排序、重置 ===============
    let currentSort = { key: '', order: 'asc' };

    // 防抖(避免输入卡顿)
    function debounce(fn, delay = 300) {
      let timer = null;
      return (...args) => {
        clearTimeout(timer);
        timer = setTimeout(() => fn(...args), delay);
      };
    }

    // 发送筛选请求给 Worker
    function sendFilter() {
      loading.style.display = 'block';
      worker.postMessage({
        type: 'filter',
        searchText: searchInput.value.trim(),
        status: statusFilter.value,
        sortKey: currentSort.key,
        sortOrder: currentSort.order
      });
    }

    // 搜索
    searchInput.oninput = debounce(sendFilter);

    // 状态筛选
    statusFilter.onchange = sendFilter;

    // 排序(点击表头)
    tableContent.addEventListener('click', (e) => {
      const el = e.target.closest('.sort-header');
      if (!el) return;
      const key = el.dataset.sort;
      if (currentSort.key === key) {
        currentSort.order = currentSort.order === 'asc' ? 'desc' : 'asc';
      } else {
        currentSort.key = key;
        currentSort.order = 'asc';
      }
      sendFilter();
    });

    // 重置
    resetBtn.onclick = () => {
      searchInput.value = '';
      statusFilter.value = '';
      currentSort = { key: '', order: 'asc' };
      sendFilter();
    };

    // 滚动更新
    container.addEventListener('scroll', renderVirtualList);

    // 启动
    fetchBigData();
  </script>
</body>
</html>

五、Vue3 + Vite

5.1 vue写法

<template>
  <div class="container">
    <!-- 操作栏:搜索、筛选、重置 -->
    <div class="toolbar">
      <input
        v-model="searchText"
        class="search"
        placeholder="搜索 ID / 姓名 / 城市"
      />
      <select v-model="status" class="select">
        <option value="">全部状态</option>
        <option value="正常">正常</option>
        <option value="异常">异常</option>
      </select>
      <button class="btn" @click="handleReset">重置</button>
    </div>

    <!-- 加载提示 -->
    <div v-if="loading" class="loading">数据处理中...</div>

    <!-- 数据表格 -->
    <table v-else>
      <thead>
        <tr>
          <th @click="handleSort('id')">ID {{ sortKey === 'id' ? (sortOrder === 'asc' ? '↑' : '↓') : '↕' }}</th>
          <th @click="handleSort('name')">姓名 {{ sortKey === 'name' ? (sortOrder === 'asc' ? '↑' : '↓') : '↕' }}</th>
          <th>年龄</th>
          <th>城市</th>
          <th @click="handleSort('status')">状态 {{ sortKey === 'status' ? (sortOrder === 'asc' ? '↑' : '↓') : '↕' }}</th>
        </tr>
      </thead>
      <tbody>
        <tr v-for="item in pageData" :key="item.id">
          <td>{{ item.id }}</td>
          <td>{{ item.name }}</td>
          <td>{{ item.age }}</td>
          <td>{{ item.city }}</td>
          <td :class="item.status === '正常' ? 'text-success' : 'text-danger'">
            {{ item.status }}
          </td>
        </tr>
      </tbody>
    </table>

    <!-- 分页区域 -->
    <div class="pagination">
      <button :disabled="pageNum === 1" @click="pageNum = 1">首页</button>
      <button :disabled="pageNum === 1" @click="pageNum--">上一页</button>

      <button
        v-for="p in pageList"
        :key="p"
        :class="{ active: p === pageNum }"
        @click="pageNum = p"
      >
        {{ p }}
      </button>

      <button :disabled="pageNum === totalPage" @click="pageNum++">下一页</button>
      <button :disabled="pageNum === totalPage" @click="pageNum = totalPage">尾页</button>
      <span>共 {{ total }} 条 / {{ totalPage }} 页</span>
    </div>
  </div>
</template>

5.2 vue3

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

// 1. 基础变量
const loading = ref(true)
const searchText = ref('')
const status = ref('')
const pageNum = ref(1)
const pageSize = ref(20)
const sortKey = ref('')
const sortOrder = ref('asc') // asc 升序 / desc 降序

// 全量处理后的数据、分页数据
const allData = ref([])
const pageData = computed(() => {
  const start = (pageNum.value - 1) * pageSize.value
  return allData.value.slice(start, start + pageSize.value)
})

// 分页总条数、总页数
const total = computed(() => allData.value.length)
const totalPage = computed(() => Math.ceil(total.value / pageSize.value) || 1) // 向上取整

// 页码区间(最多展示5个页码)
const pageList = computed(() => {
  const list = []
  const start = Math.max(1, pageNum.value - 2)
  const end = Math.min(totalPage.value, pageNum.value + 2)
  for (let i = start; i <= end; i++) {
    list.push(i)
  }
  return list
})

// 2. 创建 Web Worker
let worker = null
const createWorker = () => {
  const workerCode = `
    let originData = []
    self.onmessage = (e) => {
      const { type, data, searchText, status, sortKey, sortOrder } = e.data
      if (type === 'init') {
        originData = data
        self.postMessage({ type: 'ready' })
      }
      if (type === 'filter') {
        let res = [...originData]
        // 状态筛选
        if (status) res = res.filter(item => item.status === status)
        // 关键词搜索
        if (searchText) {
          const s = searchText.toLowerCase()
          res = res.filter(item => 
            item.id.toString().includes(s) ||
            item.name.toLowerCase().includes(s) ||
            item.city.toLowerCase().includes(s)
          )
        }
        // 排序
        if (sortKey) {
          res.sort((a, b) => {
            if (sortOrder === 'asc') {
              return a[sortKey] > b[sortKey] ? 1 : -1
            } else {
              return a[sortKey] < b[sortKey] ? 1 : -1
            }
          })
        }
        self.postMessage({ type: 'result', list: res })
      }
    }
  `
  const blob = new Blob([workerCode], { type: 'application/javascript' })
  const url = URL.createObjectURL(blob)
  worker = new Worker(url)

  // 监听 Worker 返回消息
  worker.onmessage = (e) => {
    if (e.data.type === 'ready') {
      handleQuery()
    }
    if (e.data.type === 'result') {
      loading.value = false
      allData.value = e.data.list
      pageNum.value = 1 // 筛选/搜索后重置到第一页
    }
  }
}

// 3. 防抖函数
const debounce = (fn, delay = 300) => {
  let timer = null
  return (...args) => {
    clearTimeout(timer)
    timer = setTimeout(() => fn.apply(this, args), delay)
  }
}

// 4. 发起查询(搜索/筛选/排序)
const handleQuery = debounce(() => {
  loading.value = true
  worker.postMessage({
    type: 'filter',
    searchText: searchText.value.trim(),
    status: status.value,
    sortKey: sortKey.value,
    sortOrder: sortOrder.value
  })
})

// 5. 表头排序
const handleSort = (key) => {
  if (sortKey.value === key) {
    sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
  } else {
    sortKey.value = key
    sortOrder.value = 'asc'
  }
  handleQuery()
}

// 6. 重置所有条件
const handleReset = () => {
  searchText.value = ''
  status.value = ''
  sortKey.value = ''
  sortOrder.value = 'asc'
  handleQuery()
}

// 7. 模拟请求十万条数据 + 初始化 Worker
onMounted(() => {
  createWorker()
  // 模拟后端返回 100000 条数据
  const mockData = Array.from({ length: 100000 }, (_, i) => ({
    id: i + 1,
    name: `用户-${i + 1}`,
    age: 18 + (i % 50),
    city: `城市-${Math.floor(i / 1000)}`,
    status: i % 2 === 0 ? '正常' : '异常'
  }))
  worker.postMessage({ type: 'init', data: mockData })
})

// 监听页码变化(自动重新截取分页数据,无需请求Worker)
watch(pageNum, () => {
  // 仅截取数组,纯主线程简单操作,无性能压力
})

// 组件销毁,关闭 Worker 释放资源
onUnmounted(() => {
  if (worker) {
    worker.terminate() // 立刻关掉后台小助手(Web Worker),释放内存
    URL.revokeObjectURL(worker._url)
  }
})
</script>

如果本文对你有所帮助,感谢点一颗小心心,您的支持是我继续创作的动力!
最后:写作不易,如要转裁,请标明转载出处。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容