You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于React和Socket.io的协同绘图应用卡顿问题排查

协同绘图应用实时绘图卡顿问题排查与解决

问题背景

基于React + Socket.io开发的实时协同绘图应用,单用户操作时画布流畅无卡顿,但多用户协作时,其他用户屏幕会出现绘图卡顿现象。当前实现通过持续发送光标位置同步绘图指令,暂未找到卡顿根源。

现有核心代码

前端鼠标交互处理代码

const handleMouseEvent = (e) => {
var x, y
if(e.type === 'touchmove'){
  x = e.touches[0].clientX;
  y = e.touches[0].clientY;
}else{
  x = e.clientX;
  y = e.clientY;
}
if(mouseDown){
  socket.emit('mouse:draw', {code, x, y, color})
  ctx.lineTo(x, y)
  ctx.strokeStyle = color
  ctx.stroke()
}
if(socket) socket.emit("mouse:move", {code, x, y, memberName: location.state.username})
}

前端接收绘图指令代码

socket.on("mouse:ondraw", (data)=>{
  ctx.lineTo(data.x, data.y)
  console.log(data.color)
  ctx.strokeStyle = data.color
  ctx.stroke()
})

socket.on("mouse:onDown", (data)=>{
  ctx.moveTo(data.x, data.y)
  ctx.beginPath()
})

服务端转发绘图指令代码

module.exports = (io)=>{

const MouseDraw = function(data){
    const socket = this
    const code = data.code
    const x = data.x
    const y = data.y
    const color = data.color
    socket.to(code).emit("mouse:ondraw", {x, y, color})
}
return{
    MouseDraw
}
}

卡顿核心原因

  • 高频事件发送导致网络拥堵:鼠标/触摸移动事件触发频率极高(每秒可达数十至上百次),当前代码每次事件都发送mouse:move,绘图时还额外发送mouse:draw,大量数据包堆积导致网络延迟、丢包,接收端无法及时处理。
  • 接收端绘制逻辑低效:每次收到mouse:ondraw就执行stroke(),频繁的画布重绘造成性能开销过大;同时缺少路径连贯性处理,容易出现绘制断连感。
  • Socket.io传输策略未优化:未对高频事件做节流、合并处理,也未启用压缩或volatile传输模式,进一步加剧网络负担。

针对性优化方案

1. 对高频事件做节流处理

通过节流函数限制事件发送频率(比如每16ms,对应60帧/秒),减少网络传输量:

// 节流工具函数
const throttle = (func, delay = 16) => {
  let lastCall = 0;
  return (...args) => {
    const now = Date.now();
    if (now - lastCall >= delay) {
      lastCall = now;
      func(...args);
    }
  };
};

// 用节流包装事件处理函数
const throttledHandleMouseEvent = throttle((e) => {
  let x, y;
  if(e.type === 'touchmove'){
    x = e.touches[0].clientX;
    y = e.touches[0].clientY;
  }else{
    x = e.clientX;
    y = e.clientY;
  }
  if(mouseDown){
    socket.emit('mouse:draw', {code, x, y, color})
    ctx.lineTo(x, y)
    ctx.strokeStyle = color
    ctx.stroke()
  }
  if(socket) socket.emit("mouse:move", {code, x, y, memberName: location.state.username})
});

// 绑定事件时使用节流后的函数
canvas.addEventListener('mousemove', throttledHandleMouseEvent);
canvas.addEventListener('touchmove', throttledHandleMouseEvent);

2. 优化接收端绘制逻辑

缓存其他用户的路径数据,批量绘制减少重绘次数,同时保证路径连贯性:

// 初始化其他用户路径缓存
const remotePaths = new Map(); // key: 用户名, value: {points: [], color: ''}

socket.on("mouse:onDown", (data) => {
  // 新路径开始时初始化缓存
  remotePaths.set(data.memberName, {
    points: [[data.x, data.y]],
    color: data.color
  });
});

socket.on("mouse:ondraw", (data) => {
  const path = remotePaths.get(data.memberName);
  if (!path) return;
  path.points.push([data.x, data.y]);
  
  // 每收集3个点批量绘制
  if (path.points.length >= 3) {
    ctx.beginPath();
    ctx.strokeStyle = path.color;
    ctx.moveTo(path.points[0][0], path.points[0][1]);
    for (let i = 1; i < path.points.length; i++) {
      ctx.lineTo(path.points[i][0], path.points[i][1]);
    }
    ctx.stroke();
    // 保留最后一个点作为下一段路径起点
    path.points = [path.points[path.points.length - 1]];
  }
});

注意:需在mouse:onDown事件中传递用户名和颜色,服务端转发时同步携带这些信息

3. 优化Socket.io传输配置

  • 启用数据包压缩:服务端初始化时开启压缩,减少传输体积
const io = require('socket.io')(server, {
  perMessageDeflate: {
    threshold: 1024 // 大于1KB的数据包启用压缩
  }
});
  • 对光标移动事件使用volatile发送:允许丢失不重要的中间帧,不影响最终体验
if(socket) socket.volatile.emit("mouse:move", {code, x, y, memberName: location.state.username})

4. 服务端批量转发优化

短时间内缓存绘图指令,批量转发减少Socket.io通信次数:

// 服务端缓存绘图指令
const drawBuffer = new Map(); // key: 房间码, value: {queue: [], timer: null}

const MouseDraw = function(data){
  const socket = this;
  const { code, x, y, color, memberName } = data;
  
  if (!drawBuffer.has(code)) {
    drawBuffer.set(code, { queue: [], timer: null });
  }
  const buffer = drawBuffer.get(code);
  buffer.queue.push({ x, y, color, memberName });
  
  // 每20ms批量转发一次
  if (!buffer.timer) {
    buffer.timer = setTimeout(() => {
      socket.to(code).emit("mouse:batchDraw", buffer.queue);
      buffer.queue = [];
      buffer.timer = null;
    }, 20);
  }
};

前端对应处理批量绘制事件:

socket.on("mouse:batchDraw", (batchData) => {
  batchData.forEach(data => {
    const path = remotePaths.get(data.memberName);
    if (!path) return;
    path.points.push([data.x, data.y]);
  });
  // 统一绘制所有缓存路径
  remotePaths.forEach((path) => {
    if (path.points.length < 2) return;
    ctx.beginPath();
    ctx.strokeStyle = path.color;
    ctx.moveTo(path.points[0][0], path.points[0][1]);
    for (let i = 1; i < path.points.length; i++) {
      ctx.lineTo(path.points[i][0], path.points[i][1]);
    }
    ctx.stroke();
    path.points = [path.points[path.points.length - 1]];
  });
});

内容的提问来源于stack exchange,提问作者Prerit Agrawal

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 14:58:29