WebSocket数据推送导致前端卡顿的问题,下面是如何处理高频WebSocket数据,包括数据批处理和节流等技术。
以下是关键的优化实现:
- 数据批处理以减少DOM更新频率
- 节流机制限制更新速率
- 大数据集的虚拟滚动
- 高效的数据结构管理
/**
* WebSocket数据处理工具类
* 用于处理高频数据推送,避免前端卡顿
*/
export class WebSocketHandler {
private ws: WebSocket | null = null;
private dataBuffer: any[] = []; // 数据缓冲区
private batchSize: number = 50; // 批处理大小
private updateInterval: number = 100; // 更新间隔(ms)
private updateTimer: any = null;
private onDataCallback: ((data: any[]) => void) | null = null;
private maxBufferSize: number = 1000; // 最大缓冲区大小
constructor(batchSize?: number, updateInterval?: number) {
if (batchSize) this.batchSize = batchSize;
if (updateInterval) this.updateInterval = updateInterval;
}
/**
* 连接到WebSocket服务器
* @param url WebSocket服务器地址
*/
connect(url: string) {
this.ws = new WebSocket(url);
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
this.addData(data);
};
this.ws.onopen = () => {
console.log('WebSocket连接已建立');
};
this.ws.onclose = () => {
console.log('WebSocket连接已关闭');
};
this.ws.onerror = (error) => {
console.error('WebSocket错误:', error);
};
}
/**
* 设置数据处理回调函数
* @param callback 处理数据的回调函数
*/
setOnDataCallback(callback: (data: any[]) => void) {
this.onDataCallback = callback;
}
/**
* 添加数据到缓冲区
* @param data 新数据
*/
private addData(data: any) {
this.dataBuffer.push(data);
// 限制缓冲区大小,避免内存溢出
if (this.dataBuffer.length > this.maxBufferSize) {
// 保留最新的数据
this.dataBuffer = this.dataBuffer.slice(-this.maxBufferSize);
}
// 如果是第一个数据,启动定时器
if (this.dataBuffer.length === 1) {
this.startUpdateTimer();
}
}
/**
* 启动更新定时器
*/
private startUpdateTimer() {
if (this.updateTimer) {
clearTimeout(this.updateTimer);
}
this.updateTimer = setTimeout(() => {
this.processData();
}, this.updateInterval);
}
/**
* 处理缓冲区数据
*/
private processData() {
if (this.dataBuffer.length === 0) return;
// 取出一批数据进行处理
const batchData = this.dataBuffer.splice(0, this.batchSize);
if (this.onDataCallback) {
this.onDataCallback(batchData);
}
// 如果还有数据,继续处理
if (this.dataBuffer.length > 0) {
this.startUpdateTimer();
}
}
/**
* 发送数据到服务器
* @param data 要发送的数据
*/
send(data: any) {
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
}
}
/**
* 关闭WebSocket连接
*/
close() {
if (this.ws) {
this.ws.close();
this.ws = null;
}
if (this.updateTimer) {
clearTimeout(this.updateTimer);
this.updateTimer = null;
}
}
}
<template>
<div class="virtual-list" ref="container" @scroll="onScroll">
<div class="virtual-list-phantom" :style="{ height: phantomHeight + 'px' }"></div>
<div class="virtual-list-content" :style="{ transform: `translateY(${scrollTop}px)` }">
<div
class="virtual-list-item"
v-for="item in visibleData"
:key="item.id"
:style="{ height: itemHeight + 'px' }"
>
<slot :item="item"></slot>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
// 定义组件属性
const props = defineProps<{
items: any[];
itemHeight?: number;
visibleCount?: number;
}>();
// 默认值
const itemHeight = props.itemHeight || 50;
const visibleCount = props.visibleCount || 10;
// 响应式数据
const container = ref<HTMLElement | null>(null);
const scrollTop = ref(0);
const containerHeight = ref(0);
// 计算属性
const phantomHeight = computed(() => props.items.length * itemHeight);
const visibleStart = computed(() => Math.floor(scrollTop.value / itemHeight));
const visibleEnd = computed(() => visibleStart.value + visibleCount);
const visibleData = computed(() => {
return props.items.slice(visibleStart.value, visibleEnd.value);
});
// 滚动事件处理
const onScroll = () => {
if (container.value) {
scrollTop.value = container.value.scrollTop;
}
};
// 更新容器高度
const updateContainerHeight = () => {
if (container.value) {
containerHeight.value = container.value.clientHeight;
}
};
// 监听窗口大小变化
const onResize = () => {
updateContainerHeight();
};
// 生命周期钩子
onMounted(() => {
updateContainerHeight();
window.addEventListener('resize', onResize);
});
onBeforeUnmount(() => {
window.removeEventListener('resize', onResize);
});
</script>
<style scoped>
.virtual-list {
position: relative;
height: 100%;
overflow: auto;
}
.virtual-list-phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.virtual-list-content {
position: absolute;
left: 0;
right: 0;
top: 0;
z-index: 1;
}
.virtual-list-item {
display: flex;
align-items: center;
padding: 0 10px;
border-bottom: 1px solid #eee;
}
</style>
<template>
<div class="websocket-data-view">
<h2>WebSocket数据展示</h2>
<!-- 性能监控 -->
<div class="performance-info">
<span>数据总数: {{ totalDataCount }}</span>
<span>缓冲区大小: {{ bufferLength }}</span>
<span>更新频率: {{ updateRate }} Hz</span>
</div>
<!-- 数据列表 -->
<div class="data-list-container">
<VirtualList
:items="displayData"
:item-height="60"
:visible-count="15"
>
<template #default="{ item }">
<div class="data-item">
<span class="data-id">ID: {{ item.id }}</span>
<span class="data-value">值: {{ item.value }}</span>
<span class="data-time">{{ formatTime(item.timestamp) }}</span>
</div>
</template>
</VirtualList>
</div>
<!-- 控制面板 -->
<div class="control-panel">
<button @click="toggleConnection">
{{ isConnected ? '断开连接' : '连接WebSocket' }}
</button>
<button @click="clearData">清空数据</button>
<div class="config-controls">
<label>
批处理大小:
<input
type="number"
v-model.number="batchSize"
min="1"
max="200"
@change="updateConfig"
/>
</label>
<label>
更新间隔(ms):
<input
type="number"
v-model.number="updateInterval"
min="10"
max="1000"
@change="updateConfig"
/>
</label>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onBeforeUnmount } from 'vue';
import { WebSocketHandler } from '@/utils/websocketHandler';
import VirtualList from '@/components/VirtualList.vue';
// 响应式数据
const rawData = ref<any[]>([]);
const isConnected = ref(false);
const batchSize = ref(50);
const updateInterval = ref(100);
const totalDataCount = ref(0);
// WebSocket处理器实例
let wsHandler: WebSocketHandler | null = null;
// 计算属性
const displayData = computed(() => {
// 只显示最新的10000条数据,避免内存问题
return [...rawData.value].reverse().slice(0, 10000);
});
const bufferLength = computed(() => {
return wsHandler ? wsHandler['dataBuffer'].length : 0;
});
const updateRate = computed(() => {
return Math.round(1000 / updateInterval.value);
});
// 方法
const formatTime = (timestamp: number) => {
const date = new Date(timestamp);
return date.toLocaleTimeString();
};
const updateConfig = () => {
if (wsHandler) {
// 重新配置WebSocket处理器
initializeWebSocket();
}
};
const initializeWebSocket = () => {
// 清理旧的连接
if (wsHandler) {
wsHandler.close();
}
// 创建新的WebSocket处理器
wsHandler = new WebSocketHandler(batchSize.value, updateInterval.value);
// 设置数据处理回调
wsHandler.setOnDataCallback((dataBatch) => {
// 将新数据添加到列表开头(最新数据在前)
rawData.value.unshift(...dataBatch);
totalDataCount.value += dataBatch.length;
// 限制总数据量,避免内存溢出
if (rawData.value.length > 15000) {
rawData.value = rawData.value.slice(0, 10000);
}
});
isConnected.value = true;
};
const toggleConnection = () => {
if (isConnected.value) {
// 断开连接
if (wsHandler) {
wsHandler.close();
wsHandler = null;
}
isConnected.value = false;
} else {
// 建立连接
initializeWebSocket();
// 这里需要替换为实际的WebSocket服务器地址
wsHandler?.connect('ws://localhost:8080/websocket');
}
};
const clearData = () => {
rawData.value = [];
totalDataCount.value = 0;
};
// 生命周期钩子
onMounted(() => {
// 初始化WebSocket连接
initializeWebSocket();
});
onBeforeUnmount(() => {
// 清理WebSocket连接
if (wsHandler) {
wsHandler.close();
}
});
</script>
<style scoped>
.websocket-data-view {
height: 100%;
display: flex;
flex-direction: column;
padding: 20px;
}
.performance-info {
display: flex;
gap: 20px;
margin-bottom: 20px;
padding: 10px;
background-color: #f5f5f5;
border-radius: 4px;
}
.data-list-container {
flex: 1;
border: 1px solid #ddd;
border-radius: 4px;
overflow: hidden;
}
.data-item {
display: flex;
justify-content: space-between;
align-items: center;
width: 100%;
}
.data-id {
font-weight: bold;
color: #333;
}
.data-value {
flex: 1;
text-align: center;
}
.data-time {
color: #999;
font-size: 0.9em;
}
.control-panel {
margin-top: 20px;
display: flex;
gap: 10px;
align-items: center;
flex-wrap: wrap;
}
.control-panel button {
padding: 8px 16px;
background-color: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.control-panel button:hover {
background-color: #0056b3;
}
.config-controls {
display: flex;
gap: 15px;
align-items: center;
}
.config-controls label {
display: flex;
flex-direction: column;
font-size: 0.9em;
}
.config-controls input {
margin-top: 4px;
padding: 4px;
border: 1px solid #ddd;
border-radius: 4px;
}
</style>
这些修改实现了以下优化来解决WebSocket高频数据推送导致的前端卡顿问题:
WebSocket数据批处理 (
websocketHandler.ts
):- 将高频到达的数据缓存到缓冲区
- 按设定的批次大小和时间间隔批量处理数据
- 限制缓冲区大小避免内存溢出
虚拟滚动列表 (
VirtualList.vue
):- 只渲染可见区域的数据项
- 通过计算滚动位置动态显示数据
- 大幅减少DOM节点数量,提高渲染性能
性能监控和控制 (
WebSocketDataView.vue
):- 实时显示数据量和处理状态
- 提供可调节的批处理大小和更新频率
- 限制内存中保存的数据总量
通过这些优化,即使WebSocket以很高的频率推送数据,前端也能保持流畅的用户体验。