核心指标

168 阅读11分钟

在线指标

  • 首字节时间:responseStart-navigatorStart(redirect、app cache、DNS、TCP、request)
  • 可交互时间、解析时间:domInteractive-navigatorStart
  • 完全加载时间
  • 白屏时间:用户打开网站开始,到浏览器内有内容的时间
  • 首屏时间:用户打开网站开始,到浏览器首屏内容渲染完成的时间
  • js错误率 onError等

离线指标

  • lighthouse性能评分
  • 交互响应指标RAIL

性能优化内容

  • 框架优化
    • CodeCache:JS引擎换成V8,V8在完成代码编译后,会以字节码的形式输出CodeCache,并可将其序列化到磁盘上。在下次编译时,可以利用codeCache,大大缩短代码的解析和编译时间,以优化页面加载时间。
      • 如何利用:
        • 缩小JS文件,可以使用webpack压缩
        • 代码拆分:将大型的 JavaScript 文件拆分为多个小模块,可以提高缓存命中率,并且在需要时按需加载模块。使用 Webpack 等工具进行代码拆分。
        • 使用HTTP缓存:配置服务器的 HTTP 缓存头,确保浏览器能够缓存 JavaScript 文件。使用 Cache-Control 和 ETag 等 HTTP 头来控制缓存策略。
        • 避免内联脚本:内联脚本无法被浏览器缓存,每次页面加载时都需要重新解析和编译。将 JavaScript 代码放在外部文件中,以便浏览器可以缓存。
        • 使用Service Work:Service Workers 可以拦截网络请求并提供缓存机制,允许你缓存 JavaScript 文件,并在离线或网络不稳定时提供缓存的文件。
      • 监控和分析:lighthouse和performance
    • bundle懒加载:精简首屏资源,将非首屏资源懒加载,以加快首屏资源显示
  • 网络请求优化
    • 请求前置:请求前置顾名思义是指提前发送请求,减少页面等待接口数据时间。请求前置方案按网络请求时机,我们可分两种实现策略:
      • 页面中转&Native壳:在目标页面中转或其Native 壳中进行网络请求。方案优点是,提前了请求时机且不会造成无效请求。局限是,能够提前的时间有限,如果使用该方案很难做到“直出”效果
      • 上游页面:该方案也可称为请求数据预取。网络请求无需等待跳转,在不影响上游页面加载的情况下,在上游页面尽早地发起网络请求,跳转时只需读取本地缓存即可。方案优点是,几乎完全消除了网络请求占用的时间,页面加载从 "页面打开 -> Loading 页面 -> 内容展示" 变成 "页面打开 -> 内容展示",达到“直出”效果,对原本网络请求耗时长的页面,优化效果尤其明显。但是,也存在一定局限性,主要有两点:
        • 由于参数变化或用户最终没有进入下个页面导致大量的无效请求,增加接口 QPS(不同页面增加的 QPS 可能不同,以酒店列表为例 QPS 增加 55% 左右);
        • 对数据时效性有一定影响,需要注意设置缓存失效时间。目前,在请求参数不变化的情况下,酒店列表页的网络数据缓存失效时间设置为30s。
    • 请求精简:网络请求时,如果返回大量的冗余数据,对网络传输和客户端解析都会造成严重的负担。为了给用户更加顺畅的滑动感受,我们在首屏12个Cell加载完成后,会自动进行下一页的数据请求,尽量避免用户滑动中的等待。如果可以做极致优化,不用过多考虑业务需求,那渲染的个数刚好覆盖首屏为宜,理想的方案是,可以根据屏幕高度动态设置首屏渲染个数,避免小屏手机过多渲染或大屏手机个数过少导致底部留白。
  • 视图渲染优化
    • 分步渲染:首先对模块定义出优先级,重要的模块先渲染,次要的模块后渲染。控制分步渲染就是 React 中的条件渲染,即 if (某优先级模块全部渲染完成) then {下个优先级的模块开始渲染}。不同的页面区分优先级的依据不一样,而且随着代码迭代,入侵到render里的条件判断会导致代码可读性和可维护性降低,当需要重新排布优先级时成本增加。

    • 图片优化

      • 宽高限定:尽量避免请求原图,按需替换宽高后再请求,同时,一般针对图片会增加.webp后缀,以请求webp格式的图片,加快图片展示速度。
      • 按需加载:不在屏幕内的图片延后加载,例如常见的Banner,如果组件是用的Flatlist,可考虑设置合适的windowSize属性,避免渲染可视区外的元素。
      • 提前加载:如果能提前拿到图片的url,可以使用Image.prefetch方法按照展示的宽高预加载图片,当图片真正render时会读取到缓存。例如住宿列表的缩略图通常也是详情页的头图,在点击跳转时先预加载头图,可加快图片展示速度。
      export function imagePrefetch(imgUrl) {
          Image.prefetch(imgUrl)
              .then(_result => {})
              .catch(_e => {});
      }
      
      
  • 加载视觉优化
    • 白屏优化:将住宿前置页核心区 MTFlexBox 布局描述文件打入 Bundle 中,页面启动时加载布局文件并渲染,等 MRN 页面渲染时再隐藏 MTFlexBox。类似现在的骨架屏执行流程,但是对比骨架屏,用户可以更快地看到核心区内容并且能够进行简单的交互。
    • 跳变优化:跳变会影响用户使用体验,频繁的跳动会给用户极差的观感。跳变是指,已完成布局的视图,在加载过程中位置再次发生变动,跳变主要是因为模块数据返回无序而导致的部分模块被延后渲染,我们除了渲染部分提到的分步渲染方案,还可以对数据侧进行优化,主要有以下两种方案:
      • 接口合并:一个页面一般需要请求多个接口才能完成整个页面的加载。在接口并行请求中,每个api返回数据的时间不可控会造成模块跳动,如果等待所有api返回数据可能会造成整体接口变慢。根据api端到端耗时和api服务端响应耗时分析,耗时主要集中在网络传输、网络连接上。所以,我们和后台团队协作,将服务端多个api合并为一个api,避免网络请求不稳定而带来的整体页面加载变慢、模块跳变问题。
      • 页面带参:- 将上个页面的信息(下个页面也需要)带入下个页面,也能加快下个页面的信息展示,同时,这也能避免该部分模块的跳变。如Poi的基本信息是稳定的,列表请求到的Poi信息带入到对应Poi详情页,Poi详情页的基本信息相关的模块就可以直接展示,进而减少抖动跳变,同时,加快信息展示。

另一个方向

  • 加载阶段优化
    • 框架加载优化
      • 普通预加载:提前加载bundle到JS引擎
      • 深度预加载:深度预加载指的是,在业务预加载的基础上,增加了JS执行的操作。由于JSC的机制,等到进入页面时再次执行JS的方法会提速,减少运行业务JS的时间。
      • 引擎保活:以bundleName为key进行复用,相同bundleName的包会使用同一个引擎,复用时页面加载时间会大幅度缩短
    • 包体积优化,包大小检测工具:webpack-bundle-analyzer
      • tree-shaking-patch:引入依赖包时可能将整个 npm 包导入,而不是只导入需要的文件。 开发新的 Webpack Loader,对特定包进行 Import 入口修改,识别 Import 并分析真正引入的模块。
      • 分平台打包代码:process.env.platform 可以实现端的按需引入。
    • 网络请求优化
      • 页面带参
      • 请求前置
      • 请求优先级:优先展示首屏模块的请求
      • 页面跳变优化:页面的跳变是因为页面UI元素多次渲染,且造成了其位置或大小变化而产生的。而优化思路就是避免这些变化:
        • 业务需求层面:从需求上避免首屏UI元素对应的数据多次变化,进而实现稳定的UI元素;
        • 技术优化层面:UI渲染资源有限,避免非首屏UI元素的渲染抢占了首屏UI元素的渲染资源,进而加快首屏的渲染。
          对应的优化方案也可顺势得出:
        • 业务需求层面:在PM和UI需求评审时,提出建议,规避首屏UI对应多接口的问题。或者采用上文中提到的网络请求优化-页面带参的方式,来避免此问题的发生;
        • 技术优化层面:优先渲染首屏的UI元素,在其渲染完成后,再开始非首屏UI元素的渲染。将页面UI元素分为优先渲染模块和非优先渲染模块,通过状态的变化来控制后者是否开始渲染。

image.png

  • 运行阶段优化
    • 运行发烫优化
      • 无意义的重复渲染;重复渲染是指组件的Props,State进行深比较没有改变,但依然触发了Render,这个Render是完全没有意义的,可以优化掉的。为此我们可以使用RN debugger提供的React面板显示组件重复渲染次数来发现有问题的组件。
        • 无意义的setState
        • 不经意的props改变。在使用Redux时,如果在mapStateToProps里订阅了一个比较复杂的数据结构,那么该数据结构任意字段发生了改变,即使我们并不关心该字段,也会触发组件的Render,但可能并不会注意到。
      • 频繁且耗时的JS方法调用。使用RN Debugger提供的JS方法监控面板,可以查看JS方法实时的调用情况,比如调用次数和平均耗时等。
    • 动画卡顿优化。业务开发中我们经常会遇到需要使用动画的场景,原生的JS动画需要JS侧和Native侧的频繁通信,我们可以从这一点着手进行动画的优化。
      • 使用useNativeDriver优化Animated动画:大部分工作都是在JS线程中执行的。如果JS线程被阻塞了,就会发生掉帧。同时,每一帧都会通过JS to Native Bridge更新Native View,也会有一定的性能损耗。因此,我们可以使用useNativeDriver把相关工作全放在Native侧去完成,利用原生UI线程执行,不会因为JS线程阻塞,影响动画渲染执行。
    • UI调优
      • React.Fragment:组件返回多个元素时,使用 React.Fragment 包裹可以无需向 DOM 添加额外的节点。也可以使用短语法 <> </>,与其他元素使用方式一样但不支持 Key 或属性。
      • Text合并:对于多种不同样式的文本节点,不需要一个样式创建一个 Text,可以在 Text 中内嵌 Text,即在 Text 节点下挂载不同的虚拟 Text 节点标示不同的样式
      • 组件指定key:处于并列关系的多个组件,为各个组件指定 Key 属性使 React 能够区分同类元素的不同个体,当其中有组件移除或添加时可以避免其他组件不必要的刷新。列表可以通过 keyExtractor 函数为列表 Item 指定 Key,功能类似。
      • requestAnimationFrame提升点击响应:有些时候,如果我们有一项操作与点击事件所带来的透明度改变或者高亮效果发生在同一帧中,那么有可能在onPress函数结束之前我们都看不到这些效果。比如在onPress执行了一个setState的操作,这个操作需要大量计算工作并且导致了掉帧。对此的一个解决方案是将onPress处理函数中的操作封装到requestAnimationFrame中。

解决方案

  • 避免无意义的重复渲染
    • 使用PureComponent:PureComponent可以对组件的Props和State进行浅比较,若有变化才进行渲染操作。
    • 善用shouldComponentUpdate
    • 保持组件的Props是同一个对象。问题代码如下,每次父组件Render时,scrollPercent都会生成一个新的对象,导致TitleBarContainer/TitleBarComponent重复Render。

image.png
解决方案:差值scrollPercent存储在State中,它的值会随着滚动而变化,但是始终是同一个对象,不会造成重复Render。

image.png

  • 优化React中的条件渲染 优化前:
render () {
        if (!this.props.isLogin) {
            return (
                <View>
                    <LoginComponent />
                    <AComponent />
                    <BComponent />
                </View>
            )
        }else {
            return (
                <View>
                    <AComponent />
                    <BComponent />
                </View>
            )
        }
    }

优化后:

    render () {
        return (
            <View>
                {!this.props.isLogin && <LoginComponent /> }
                <AComponent />
                <BComponent />
            </View>
        ) 
    }
  • 保持render中的条件渲染。Render函数应当是个纯函数,不应修改应用的状态,比如调用setState等,否则可能会使得应用陷入无限递归调用。
  • 使用唯一键迭代。创建React组件时,我们可以给其设置Key,Key被React用来区分组件的唯一标识。每个Key对应一个组件,相同的Key React认为是同一个组件,这样后续相同的Key对应组件都不会被创建。