一、前言
后端一次性返回几万~十万条数据时,前端直接渲染会导致:
主线程阻塞、页面卡顿 / 卡死
表格渲染白屏
滚动、点击交互无响应
核心方案: 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>
如果本文对你有所帮助,感谢点一颗小心心,您的支持是我继续创作的动力!
最后:写作不易,如要转裁,请标明转载出处。