28、屏幕共享性能优化:Web Worker处理编码、Canvas优化、内存泄漏排查

屏幕共享这个功能,做出来容易,做好却很难。

我记得第一次给客户演示屏幕共享时,画面卡得像幻灯片。客户问了一句:「你们这个产品是Beta版吧?」我当时那个尴尬啊……

后来我花了整整两周时间,专门做性能优化。今天就把这些经验分享给你。

性能瓶颈到底在哪?

屏幕共享的性能问题,说白了就三个地方:

  • 编码太慢 —— 主线程被编码任务占满,UI直接卡死
  • Canvas绘制太重 —— 每一帧都要重绘,CPU扛不住
  • 内存只增不减 —— 跑几分钟后浏览器直接崩溃

你想想看,如果这三个问题同时出现,用户体验会怎样?

核心思路:把编码扔给Worker、优化Canvas绘制路径、用工具排查内存泄漏。三者缺一不可。

屏幕共享性能优化三大方向 Web Worker编码 分离编码到独立线程 Canvas优化 减少重绘、复用画布 内存泄漏排查 工具定位、及时释放 postMessage 传递帧数据 避免主线程阻塞 requestAnimationFrame + 离屏Canvas 减少CPU/GPU开销 Chrome DevTools Memory面板 定位未释放的引用 三者联动 → 流畅的屏幕共享体验

Web Worker处理编码:把重活扔到后台

屏幕共享时,浏览器需要不断抓取屏幕画面,然后编码成视频流。这个编码过程非常消耗CPU。

如果放在主线程做,UI渲染就会被阻塞。用户会看到画面卡顿、按钮点不动。

我的做法:创建一个专门的Worker线程来处理编码。主线程只负责抓取画面和展示结果。

来看代码实现:

// 主线程代码
const encoderWorker = new Worker('encoder-worker.js');

// 从屏幕共享流中获取帧
const track = stream.getVideoTracks()[0];
const imageCapture = new ImageCapture(track);

function captureAndSend() {
  imageCapture.grabFrame().then(bitmap => {
    // 把图像数据传给Worker
    const imageData = bitmapToImageData(bitmap);
    encoderWorker.postMessage({
      type: 'encode',
      data: imageData,
      timestamp: performance.now()
    }, [imageData.data.buffer]); // 使用Transferable对象
    bitmap.close();
  });
}

// 接收编码后的数据
encoderWorker.onmessage = (e) => {
  const { encodedData, timestamp } = e.data;
  // 发送到WebRTC peer connection
  sendToPeer(encodedData);
};

Worker内部的代码:

// encoder-worker.js
self.onmessage = async (e) => {
  const { type, data, timestamp } = e.data;
  
  if (type === 'encode') {
    // 使用OffscreenCanvas进行编码
    const canvas = new OffscreenCanvas(data.width, data.height);
    const ctx = canvas.getContext('2d');
    ctx.putImageData(data, 0, 0);
    
    const blob = await canvas.convertToBlob({
      type: 'image/webp',
      quality: 0.8
    });
    
    self.postMessage({
      encodedData: blob,
      timestamp: timestamp
    });
  }
};

注意:使用 Transferable 对象传递数据可以避免内存拷贝。我见过有人没加这个,内存直接翻倍。

Canvas优化:别让每一帧都重新绘制

很多人做屏幕共享时,每抓取一帧就重新创建Canvas。这其实非常浪费。

我习惯用「离屏Canvas + 增量更新」的策略。

具体怎么做?

  • 复用Canvas实例 —— 不要每次new Canvas,创建一次反复用
  • 使用OffscreenCanvas —— 可以在Worker中操作,不占用主线程
  • 控制帧率 —— 屏幕共享不需要60fps,15-30fps就够
// 优化后的Canvas处理
const offscreen = new OffscreenCanvas(1920, 1080);
const ctx = offscreen.getContext('2d');

let lastFrameTime = 0;
const FRAME_INTERVAL = 1000 / 24; // 24fps

function processFrame(bitmap, now) {
  if (now - lastFrameTime < FRAME_INTERVAL) {
    // 帧率控制:不到时间就跳过
    bitmap.close();
    return;
  }
  lastFrameTime = now;
  
  // 直接绘制到离屏Canvas
  ctx.drawImage(bitmap, 0, 0);
  bitmap.close();
  
  // 只绘制变化区域(如果有)
  // 这里可以用差分算法判断哪些区域变了
}

经验之谈:我曾经遇到一个项目,屏幕共享时CPU占用率高达80%。后来发现是每帧都创建了新的Canvas对象。改成复用后,CPU直接降到30%。

内存泄漏排查:别让浏览器悄悄崩溃

屏幕共享跑久了,内存会不断增长。如果不排查,最终浏览器会崩溃。

我排查内存泄漏的步骤是这样的:

  1. 打开Chrome DevTools的Memory面板
  2. 录制堆快照 —— 先拍一张,操作一段时间后再拍一张
  3. 对比两张快照 —— 看哪些对象没有被释放
  4. 重点检查 —— MediaStream、VideoTrack、Canvas、Blob这些对象

常见的泄漏点:

泄漏对象 原因 解决方法
MediaStream 没有调用 getTracks().forEach(t => t.stop()) 停止共享时主动释放
ImageBitmap 没有调用 bitmap.close() 用完立即关闭
Blob URL 没有调用 URL.revokeObjectURL() 创建后记得释放
事件监听器 没有移除监听 使用 removeEventListener

我曾经踩过的坑:有一次排查了三天,发现是Worker线程没有terminate。每次重新开始屏幕共享就new一个Worker,旧的Worker还在后台跑着,内存只增不减。

正确的做法:

let encoderWorker = null;

function startScreenShare() {
  // 如果已有Worker,先终止
  if (encoderWorker) {
    encoderWorker.terminate();
    encoderWorker = null;
  }
  
  encoderWorker = new Worker('encoder-worker.js');
  // ... 其他初始化代码
}

function stopScreenShare() {
  if (encoderWorker) {
    encoderWorker.terminate();
    encoderWorker = null;
  }
  
  // 释放所有track
  stream.getTracks().forEach(t => t.stop());
}

综合优化效果

把这三招都用上后,效果很明显:

  • CPU占用率从70%降到25%
  • 内存增长曲线从「直线上升」变成「平稳波动」
  • 用户反馈:「画面流畅多了」

嗯,其实性能优化没有银弹。但Web Worker、Canvas复用、内存排查这三板斧,能解决90%的屏幕共享性能问题。

剩下的10%,就得靠具体场景具体分析了。比如有些用户的电脑配置特别低,那还得降帧率、降分辨率。

最后一个小建议:上线前一定要做压力测试。我习惯用多台不同配置的机器跑30分钟以上,观察内存和CPU的变化曲线。


公众号:蓝海资料掘金营,微信deep3321