前端性能优化经验总结

1,112 阅读5分钟

昨天面试,面试官让讲一下前端性能优化,脑子转不过来,随便讲了下,今天总结下自己性能优化相关的经验。

减少 DOM 操作

2012 年的时候基于 jQuery 开发单页应用,功能是设备的资源管理,切换页面的时候加载 html 片段然后向后端获取数据,请求成功后使用 DOM 接口将数据添加到表格中进行显示。

做的时候每一条数据单独 append,项目演示的时候列表类页面平均需要 3 秒左右才能显示出来,第一次去了解前端性能优化。

查资料了解到主要的性能瓶颈在 DOM 操作(选择元素、添加元素)上,优化方向主要有两点:

  1. 缓存选择元素的结果,不要每次重新选择;
  2. 数据获取完成后将所有的数据合并一起添加到表格中。 优化后性能提升明显,平均打开速度降低到了 300 毫秒以下。

减少 CSS 的重绘和回流

这个优化思路主要是谷歌性能优化的建议,讲重绘和回流的博客也比较多,实施起来主要就是涉及变化的样式尽量使用不会导致重绘和回流的 CSS 样式,这个性能提升多少我自己并没有进行验证和测试,只是按照最佳实践进行开发。

在 Reactjs-player 代码中有所提现:

  1. 播放器工具栏隐藏的时候使用了 translate 平移的方式代替 top
 style={{ transform: hiding ? 'translate(0, 48px)' : 'translate(0, 0)' }}
  1. 缓存进度条长度 使用 transform 代替 width
  transform: scaleX(0);

developers.google.com/speed/docs/…

React 性能优化

根据我的经验,React 性能优化主要就是减少 render 次数。

使用类组件的时候可以使用 Pure 组件提升性能,在某些情况使用 shouldComponentUpdate 减少组件更新。

在 React 16.8 版本以后,我的项目代码都切换到了函数式组件,性能优化的方式主要有以下几点:

  1. 使用 React.memo 包装组件,React.memo 的第二个参数类似与类组件的 shouldComponentUpdate,在需要的时候可以忽略 props 的变化不更新组件,挂载在 Router 上的组件如果不这样使用切换路由会导致组件更新,我的项目中都是显式的写出来,Reactjs-player 中也是这样实现的,代码地址,;
export default React.memo( 
  ReactjsPlayer,
  (p, n) =>
    p.kernel === n.kernel &&
    p.live === n.live &&
    p.config === n.config &&
    p.onKernelError === n.onKernelError &&
    p.src === n.src &&
    p.type === n.type &&
    p.controls === n.controls &&
    p.className === n.className &&
    p.videoProps === n.videoProps &&
    p.playerProps === n.playerProps &&
    p.children === n.children,
);
  1. Redux 的 useSelector 也是类似处理,可以直接使用 redux 提供的 shallowEqual 函数;

  2. 通过 ref 缓存状态,尽量减少 useEffect 和 useCallback 的依赖,Reactjs-player 的代码实现

  3. 使用 useReducer 代替 useState,合并多个状态更新操作,同时使用节流降低状态变化的次数,Reactjs-player 的代码实现

白板性能优化

白板的功能大致时老师上传 PPT 到服务器,后端解析成多个图片后提供给前端使用,网页将 PPT 显示在 Canvas 中,老师可以在 PPT 上面标注,同时将老师电脑摄像头的画面叠在最上层,然后将老师这边的操作通过 captureStream 导出成视频之后使用 WebRTC 和学生进行互动。

在开发完成后,如果使用画笔工具的时候一直拖动不松开,会导致 CPU 消耗一直上涨,开发的电脑上 CPU 利用率会升到 90% 以上。

优化的手段有以下几点:

  1. 将白板分层,减少不必要的绘制;
  2. 老师每次绘制结束后将 Canvas 导出一个图片缓存下来,下次绘制 canvas 的时候只需要绘制这个截图和当前操作的轨迹;
  3. 绘制的时候做节流处理,每次鼠标 move 事件只记录轨迹点,超过绘制间隔才触发绘制;
  4. 未实施:使用离屏 Canvas 和 Web Worker,进一步优化性能。

白板在使用了上面的前三步优化后,CPU 利用率可以稳定在 50% 以下,后续没有去实施第四步了。

视频会议中的性能优化

之前的 Janus 信令都是并行处理,前端只要收到 WebSocket 消息就会自动去连接等操作,同一个会议中已加入的人超过 10 个以后,后面的人加入会议需要同时建十几个连接,在安卓手机上经常出现 ICE 失败的情况。

根据问题现象分析可能是性能问题导致,优化的方式如下:

  1. 底层 Janus 信令处理只上报状态,由上层主动调用接口去建立连接;
  2. 使用任务队列限制并发,同时只去建立一个连接;
  3. 减少页面中视频显示的个数,这个主要是业务上优化;
  4. 对于页面上没有显示视频的流,直接收音频,控制服务器不要发送视频流。

经过上述的优化后,ICE 失败的问题没有再出现了。

导播页面性能优化

使用 Electron 开发了一个类似 vmix 的程序,软件界面如下图所示。

mixer.jpg

左下角两个摄像头预览的分辨率是 1080p,上面的输出和预览使用的 canvas 绘制 video 实现的,因为使用的硬件性能比较差,推流到后端的视频帧率只能到 15 左右。

最开始的想法是使用 MediaStream 的 applyConstraints 接口动态调整摄像头的分辨率,帧率可以提升到 20 帧左右,但是有些摄像头切换分辨率会花屏 2-3 秒,体验差不能采用。

最后分析 CPU 的消耗在渲染上,解决方法是左下角的摄像头预览不直接显示 video,而是将 video 绘制到低分辨率的 canvas 进行预览,性能提升很大,输出帧率可以稳定在 28 左右,满足了业务需求。