WebSocket数据推送导致前端卡顿的问题

发布于:2025-09-04 ⋅ 阅读:(13) ⋅ 点赞:(0)

WebSocket数据推送导致前端卡顿的问题,下面是如何处理高频WebSocket数据,包括数据批处理和节流等技术。

以下是关键的优化实现:

  1. 数据批处理以减少DOM更新频率
  2. 节流机制限制更新速率
  3. 大数据集的虚拟滚动
  4. 高效的数据结构管理
/**
 * 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高频数据推送导致的前端卡顿问题:

  1. WebSocket数据批处理 (websocketHandler.ts):

    • 将高频到达的数据缓存到缓冲区
    • 按设定的批次大小和时间间隔批量处理数据
    • 限制缓冲区大小避免内存溢出
  2. 虚拟滚动列表 (VirtualList.vue):

    • 只渲染可见区域的数据项
    • 通过计算滚动位置动态显示数据
    • 大幅减少DOM节点数量,提高渲染性能
  3. 性能监控和控制 (WebSocketDataView.vue):

    • 实时显示数据量和处理状态
    • 提供可调节的批处理大小和更新频率
    • 限制内存中保存的数据总量

通过这些优化,即使WebSocket以很高的频率推送数据,前端也能保持流畅的用户体验。


网站公告

今日签到

点亮在社区的每一天
去签到