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

一、前言

后端一次性返回几万~十万条数据时,前端直接渲染会导致:

主线程阻塞、页面卡顿 / 卡死

表格渲染白屏

滚动、点击交互无响应

核心方案: Web Worker:在后台线程处理数据(过滤、排序、格式化),不阻塞 UI

虚拟滚动:只渲染可视区域的 DOM,而非全部数据

主线程只负责渲染,复杂计算交给 Worker

二、效果图

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

三、方案

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

js 复制代码
        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 条数据

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

3.3 接收 Worker 消息

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

3.4 虚拟滚动渲染

js 复制代码
        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 搜索、筛选、排序、重置

js 复制代码
    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 里,主线程只负责渲染,绝对不阻塞页面!

html 复制代码
    <!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写法

html 复制代码
    <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

js 复制代码
    <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>

如果本文对你有所帮助,感谢点一颗小心心,您的支持是我继续创作的动力!

最后:写作不易,如要转裁,请标明转载出处。

相关推荐
阿明在折腾1 小时前
从Canvas到AI模型:我在线工具站里的图片处理实战
前端·后端
CainChen1 小时前
Chrome 远程调试 Android 卡在 Pending authentication 的解决办法
前端
杨运交1 小时前
[030][Web模块]Spring Boot 验证与 OpenAPI 集成实战:从校验规则到文档生成
前端·spring boot·python
用户484526255821 小时前
JavaScript 数组不是数组,是对象
javascript
用户484526255821 小时前
用栈模拟队列:算法题背后的原型链课
javascript
天le1 小时前
基于cocos3.x复刻《猪了个猪》挪了个船:位置生成实现
前端
青木_JS1 小时前
qiankun 子应用重开后仍显示旧数据?问题出在模块顶层的 useStore()
前端
货拉拉技术1 小时前
面向 Agent Skill 的 CLI/SSO 鉴权体系:安全、无感、可追溯
前端·agent
零陵上将军_xdr2 小时前
后端转全栈学习-Day5-JavaScript 基础-3
开发语言·javascript·学习