28、屏幕共享性能优化:Web Worker处理编码、Canvas优化、内存泄漏排查
屏幕共享这个功能,做出来容易,做好却很难。
我记得第一次给客户演示屏幕共享时,画面卡得像幻灯片。客户问了一句:「你们这个产品是Beta版吧?」我当时那个尴尬啊……
后来我花了整整两周时间,专门做性能优化。今天就把这些经验分享给你。
性能瓶颈到底在哪?
屏幕共享的性能问题,说白了就三个地方:
- 编码太慢 —— 主线程被编码任务占满,UI直接卡死
- Canvas绘制太重 —— 每一帧都要重绘,CPU扛不住
- 内存只增不减 —— 跑几分钟后浏览器直接崩溃
你想想看,如果这三个问题同时出现,用户体验会怎样?
核心思路:把编码扔给Worker、优化Canvas绘制路径、用工具排查内存泄漏。三者缺一不可。
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%。
内存泄漏排查:别让浏览器悄悄崩溃
屏幕共享跑久了,内存会不断增长。如果不排查,最终浏览器会崩溃。
我排查内存泄漏的步骤是这样的:
- 打开Chrome DevTools的Memory面板
- 录制堆快照 —— 先拍一张,操作一段时间后再拍一张
- 对比两张快照 —— 看哪些对象没有被释放
- 重点检查 —— 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