前言
对于前端开发攻城狮们来说,性能优化是一个永恒的话题。随着前端需求复杂度的不断升高,在项目中想始终保持着良好的性能也逐渐成为了一个有挑战的事情。本次分享简述我们在 Rax 项目中常用的一些性能优化方式,并将从近期的一个实际业务需求出发,讲述我在 Rax C端应用场景下所遇到性能问题排查时的心路历程。
本次分享的优化内容,主要是指用户滑动屏幕浏览页面过程中所花费的单帧渲染耗时的降低比例。 单帧渲染耗时通过 chrome dev tools 插件以及手淘端app dev tools测得。一般来说浏览器运行帧率为 60hz,因此对于帧率要求非常高的场景如内容输入后的响应、动画等,需要尽可能的将单帧耗时控制在 16.6ms(1s / 60)以内,用户才能完全感知不到系统的卡顿。对于一般性能要求没那么高的应用场景,单帧耗时能达到33.2ms(2s / 60),也能达到一个相对流畅的用户体验。
实际业务现状
接下来我将从近期参与的一个实际业务需求出发,记录下我在这个项目中遇到性能问题时,发现问题、解决问题的心路历程。
从实际业务场景出发,我们发现不少页面在用户滑动浏览页面的过程中存在卡顿,一些情况下卡顿严重,非常影响用户体验,由于多数安卓机的渲染性能不如IOS系统,安卓端的性能问题会尤其明显
那么如何优化这个性能问题?
在 Rax当前的版本中,rax框架使用同步渲染模式(Synchronous Rendering),也就是说,在处理更新时会阻塞整个 UI 线程,直到所有更新完成才会重新渲染视图。这种方式虽然简单易懂,但是在处理大量或复杂数据时存在明显的性能问题,导致应用程序出现卡顿或者无响应等问题。
React18的并发模式(Concurrent Mode),它可以将渲染任务分解成多个小块,并通过 React 的调度器(Scheduler)来优化执行顺序和时间,从而降低了单次渲染所需的计算资源和时间。并发模式可以在渲染过程中及时响应用户的输入和请求,避免出现 UI 卡顿或者无响应的情况,从而提高了整个应用的用户体验。
目前Rax框架在性能优化上虽然不及react18,但仍然有非常好的应用渲染性能基础,站在了巨人的肩膀上,我们所能做的,便是在不添乱的前提下,帮助Rax更快、更高效的完成遍历渲染的过程,使更新链路尽可能的短的走完。
排查流程
通过安卓手淘扫码观察app dev tools的性能分析,每帧普遍在 700ms以上,最严重的长达 1847ms
观察发现最耗时的任务为一个叫appear的函数执行,appear函数出现在trackerLink组件中,是曝光方法,也就是说,每当一个标签需要进行曝光,那么当这个元素出现在浏览器视口,就会触发此appear方法,随着一次又一次的需求迭代,需要曝光的点越来越多,导致每一次元素重新出现在适口,都会有大量的appear方法调用,消耗了大量的性能,在安卓系统性能不够优秀的机型上,用户体验表现就会卡顿。
当我们滑动页面时,由于大量元素需要进行曝光操作,appear函数长时间执行,对于用户来讲,这段时间界面是卡死且无法交互的。
如果我们把这个例子中的appear函数类比成Rax的更新过程:即setState触发了一次更新,而这次更新耗时非常久,比如1800ms。那么在这1800ms的时间内界面是卡死的,用户无法进行交互,非常影响用户的使用体验。
优化思路
雪中送炭
擒贼先擒王,首先解决最影响性能的,怎么让appear方法的执行不影响主线程的渲染?
1.接入react18?
成本太高。pass
2.使用requestIdleCallback来控制appear方法的执行不影响主线程?
requestIdleCallback 会将任务安排在浏览器的空闲时期执行。在当前页面的生命周期内,也不能保证 requestIdleCallback 中的任务一定会执行。如果浏览器长时间忙于处理高优先级任务,低优先级的空闲回调可能会被推迟,直到有足够的空闲时间来执行它们。如果这种情况一直持续,那么这些任务可能会被无限期地推迟。pass
3.如果任务是关键性的,需要在特定时间内一定要执行,那么使用 setTimeout可能是更好的选择。setTimeout这个API提供了更可靠的方式来计划任务的执行,即使它们没有 requestIdleCallback 提供的执行时机那样灵活。
setTimeout将同步任务变为异步任务,当需要trackertLink组件需要曝光时,将曝光函数异步执行,不阻塞影响主线程渲染,提高用户体验性。
改动TrackerLink组件:
// needAsyncSending控制埋点曝光异步进行,不阻塞主线程
if (needAsyncSending) {
const sendExpTimer = setTimeout(() => {
recordExp();
clearTimeout(sendExpTimer);
}, 0)
} else {
recordExp();
}
优化后:
我们可以看到,当trakcerLink组件异步调用曝光方法时,每帧渲染时间大幅缩减,效果明显,每帧最糟糕的情况从1800ms降低至每帧最糟糕的情况200ms左右,
锦上添花
通过异步发送曝光埋点解决了最大头的性能问题,用户滑动页面时已经不会有明显的延时和滑不动,但是任然有部分顿挫感,由于无法使用react app dev tools工具观察组件的渲染情况,我们使用console.log来判断哪些组件一直在重新渲染,发现在滚动过程中,航班报价列表和报价卡片一直在重新渲染,
但是业务场景下,此种渲染是不必要的,当没有重新发起请求时,是不需要一直重新渲染ota报价卡片
避免 State 的不必要更新
一些业务场景下,不需要一直更新setState,找到不必要的setState,避免这些更新
防止父组件重新渲染重新声明方法导致子组件不必要的重新渲染
纯函数组件使用 Memo 包裹,纯函数组件的function类型的prop使用useCallback包裹
===========>>>>>
最终优化效果:
可以看到最终性能分析得到的数据没有明显卡顿的地方,很多帧也达到了16.7ms的理想状态
版权归原作者 万邺 所有, 如有侵权,请联系我们删除。