性能优化实战:内存优化、CPU占用控制、延迟优化技巧
做WebRTC开发,性能优化是绕不开的坎。我见过太多项目,功能都跑通了,一上量就崩——内存飙到2G,CPU跑到100%,延迟直接卡成PPT。说白了,WebRTC的优化不是锦上添花,是生死存亡。
今天我就把压箱底的经验掏出来。内存、CPU、延迟,这三个维度我们一个一个啃。
一、内存优化:别让浏览器吃掉你的手机
WebRTC的内存泄漏,我踩过最大的坑。有一次线上会议,用户反馈手机发烫、卡死。一查,内存占用从200M涨到1.2G,半小时就崩了。后来定位到是MediaStream对象没释放。
1.1 及时释放MediaStream
很多同学拿到流就忘了关。你想想看,每个流都带着视频轨道、音频轨道,不释放就是内存黑洞。
// 错误示范:只停不关
function stopStream(stream) {
stream.getTracks().forEach(track => track.stop());
// 流对象还在内存里!
}
// 正确做法:彻底释放
function releaseStream(stream) {
if (!stream) return;
stream.getTracks().forEach(track => {
track.stop();
track = null; // 断开引用
});
stream = null; // 让GC能回收
}
1.2 视频帧缓冲池复用
视频处理时,每一帧都new一个Uint8Array?那内存分配会频繁到让你怀疑人生。我习惯用对象池模式:
class FrameBufferPool {
constructor(size = 10) {
this.pool = [];
this.maxSize = size;
}
acquire() {
return this.pool.pop() || new Uint8Array(1920 * 1080 * 4);
}
release(buffer) {
if (this.pool.length < this.maxSize) {
buffer.fill(0); // 清空复用
this.pool.push(buffer);
}
}
}
这样做的好处很明显:内存分配次数减少80%以上,GC压力也小很多。
1.3 小心SDP和ICE候选的内存占用
你可能不知道,一个复杂的SDP字符串能到几十KB。如果频繁重协商,这些字符串会堆积。我建议:
- 每次协商后,把旧的SDP对象置null
- ICE候选列表不要无限累积,超过50个就清理旧的
- 使用WeakMap存储连接状态,避免强引用
二、CPU占用控制:让手机不再发烫
CPU优化,说白了就是算力分配。WebRTC里最吃CPU的是编码和解码。1080p 30fps的视频,软编码能吃掉一个核的全部算力。
2.1 动态码率与分辨率调整
我做过一个实验:在移动端,720p 15fps和1080p 30fps,CPU占用差了3倍。但用户感知到的画质差异其实没那么大。所以,动态调整是关键:
// 基于CPU使用率动态降级
function adjustEncoding(peerConnection, cpuUsage) {
const sender = peerConnection.getSenders()[0];
const params = sender.getParameters();
if (cpuUsage > 0.8) {
// CPU超过80%,降分辨率
params.encodings[0].scaleResolutionDownBy = 2.0;
params.encodings[0].maxBitrate = 500000; // 500kbps
} else if (cpuUsage > 0.5) {
params.encodings[0].scaleResolutionDownBy = 1.5;
params.encodings[0].maxBitrate = 1000000;
}
sender.setParameters(params);
}
2.2 硬件编码优先
嗯,这里要注意:不是所有浏览器都支持硬件编码。我建议做特性检测:
| 平台 | 硬件编码支持 | 推荐编码器 |
|---|---|---|
| Chrome (桌面) | H264, VP8, VP9 | H264 (硬件加速) |
| Chrome (Android) | H264, VP8 | H264 (硬件加速) |
| Safari (iOS) | H264 | H264 (硬件加速) |
| Firefox | VP8, VP9 | VP9 (软件编码) |
2.3 帧率控制与丢帧策略
CPU扛不住的时候,降帧率比降分辨率更有效。为什么?因为编码器每帧都要做运动估计,帧率减半,计算量直接减半。
// 智能丢帧策略
class FrameDropper {
constructor(targetFps = 30) {
this.targetFps = targetFps;
this.frameInterval = 1000 / targetFps;
this.lastFrameTime = 0;
}
shouldDrop(currentTime) {
if (currentTime - this.lastFrameTime < this.frameInterval) {
return true; // 丢帧
}
this.lastFrameTime = currentTime;
return false;
}
}
三、延迟优化:从端到端的每一毫秒
延迟是WebRTC最敏感的性能指标。我做过一个测试:延迟超过300ms,用户就开始觉得对话不自然。超过500ms,基本没法用。
3.1 网络延迟:ICE与TURN的取舍
ICE的P2P直连延迟最低,但NAT穿透失败率不低。TURN中继延迟高,但稳定。我的经验是:
- 优先尝试P2P,设置超时5秒
- P2P失败再切TURN,但只切音频流
- 视频流如果必须走TURN,降低分辨率到360p
3.2 编码延迟:从配置入手
编码延迟主要来自B帧和参考帧。B帧需要参考前后帧,延迟最大。我建议:
// 低延迟编码配置
const lowLatencyConfig = {
'googCpuOveruseDetection': true,
'googCpuOveruseThreshold': 85,
'googHighStartUpBitrate': false,
'googPayloadPadding': false,
'googScreencastMinBitrate': 100,
// 关键:关闭B帧
'googNoiseReduction': false,
'googDenoising': false,
};
3.3 渲染延迟:requestAnimationFrame vs setTimeout
渲染视频帧时,用requestAnimationFrame比setTimeout好得多。为什么?因为rAF和浏览器的渲染循环同步,不会出现帧错位。
// 推荐:使用rAF渲染
function renderVideo(videoElement) {
function frame() {
// 这里处理视频帧
videoElement.currentTime = performance.now() / 1000;
requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
// 不推荐:setTimeout会有额外延迟
function badRender(videoElement) {
setInterval(() => {
videoElement.currentTime = Date.now() / 1000;
}, 33); // 约30fps,但实际延迟不稳定
}
四、整体优化框架
我把这三个维度的优化串起来,画了一张图。你看完就明白怎么联动了:
你看,这三个维度不是孤立的。内存释放了,GC频率降低,CPU占用自然下降。CPU降了,编码延迟也小了。延迟小了,用户感觉流畅,就不会频繁重连——又反过来减少内存分配。这是个正向循环。
嗯,今天就聊到这儿。这些技巧都是我一个个坑踩出来的。你拿去用,有问题随时交流。