性能优化实战:内存优化、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能回收
}
避坑指南:我曾经在切换摄像头时,只调了stop()没置null,结果旧流一直占着内存。用户切了5次摄像头,内存涨了300M。记住:stop()只是停止传输,不是释放内存。

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存储连接状态,避免强引用
小技巧:用Chrome的Memory面板拍快照,对比两次快照看Detached DOM Tree。如果MediaStream或RTCPeerConnection的实例数只增不减,那就是泄漏了。

二、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 (软件编码)
核心原则:能用硬件绝不用软件。硬件编码的CPU占用通常是软件的1/5到1/3。我在项目中遇到过,同一台手机,软编码CPU 60%,硬编码只有12%。

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,
};
避坑指南:我曾经为了画质开了B帧,结果延迟从100ms飙到400ms。后来发现,实时通信场景下,B帧带来的画质提升远不如延迟降低带来的体验提升。所以,实时通信一律关B帧。

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,但实际延迟不稳定
}

四、整体优化框架

我把这三个维度的优化串起来,画了一张图。你看完就明白怎么联动了:

WebRTC性能优化三维框架 内存优化 • MediaStream及时释放 • 帧缓冲池复用 • SDP/ICE候选清理 • WeakMap弱引用 • GC压力降低80% CPU控制 • 动态码率调整 • 硬件编码优先 • 智能丢帧策略 • 分辨率降级 • CPU占用降低60% 延迟优化 • ICE P2P优先 • 关闭B帧编码 • rAF渲染同步 • 音频流优先TURN • 延迟降低至100ms 三者联动:内存释放 → CPU降低 → 延迟减少 → 用户体验提升

你看,这三个维度不是孤立的。内存释放了,GC频率降低,CPU占用自然下降。CPU降了,编码延迟也小了。延迟小了,用户感觉流畅,就不会频繁重连——又反过来减少内存分配。这是个正向循环。

总结一下我的经验:性能优化没有银弹。你得先做profiling,找到瓶颈在哪。我习惯用Chrome的Performance面板录一段30秒的会议,看火焰图。哪个函数占CPU多,哪个对象内存泄漏,一目了然。然后对症下药,别一上来就全盘改。

嗯,今天就聊到这儿。这些技巧都是我一个个坑踩出来的。你拿去用,有问题随时交流。

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