likes
comments
collection
share

React Native APP页面渲染监测系统介绍

作者站长头像
站长
· 阅读数 40

前言

从前端技术来说,一个上线的页面,除了完成其业务功能,页面本身的渲染性能也是需要格外关注的。黑湖智造作为一款工业互联网软件,其用户群体是制造业工厂内上班的工人。实际使用的设备要么是企业统一采购的手机、PDA、平板电脑,要么是工人自己使用的手机,总体而言,其设备性能处于中下游水平。从这个角度来看,这对我们APP的页面渲染性能提出了更高的要求,因为其容错率更低,一些常见的容易被忽视的渲染问题会在低端机型中被放大,从而导致更严重的线上问题。

目前市面上关于React Native页面渲染性能并没有最佳实践,没有成熟的工具可用,于是我们内部发起了一个自建APP页面渲染监测系统的项目,用于实时检测APP页面的渲染情况,并辅助后台统计工具加以分析和研判。

监测指标

传统的性能监控指标包括:Chrome 团队和 W3C 性能工作组推出了一组以用户为中心的性能指标,从用户角度更好地去评判页面性能。

这些指标主要包含:

FCP,First Contentful Paint 首次内容绘制:测量的是页面从开始加载到页面内容的任何部分在屏幕上完成渲染的时间,这个指标回答了一个用户问题,应用正在运行吗

LCP,Largest Contentful Paint 最大内容绘制:这个指标对应的关键用户问题是,内容是否有用,即页面是否已经呈现出对用户有用的内容。

TTI,Time to Interactive 可交互时间:用户可以和页面交互的时间,对应的用户关注点是可以使用吗

TBT,Total Blocking Time 总阻塞时间:TBT 和 TTI 是一对配套指标,用于衡量在页面可交互之前的阻塞程度。

CLS,Cumulative Layout Shift 累积布局偏移:累积布局偏移指标用于衡量页面视觉稳定性。单次布局偏移分数是影响分数(不稳定区域占可视区域的百分比)与距离分数(不稳定元素最大位移距离占比)的乘积。

综合上述指标,TTI是最值得关注的指标,因为用户直观的体验就是页面卡不卡,而页面卡不卡则直接由TTI决定。搜寻网上的方案,有一种方式(Measuring Render Time | React Native Performance)是从页面跳转开始计时,结束时间由用户指定,比如页面中的A view渲染成功后,即认为页面可交互了。 但是从实际业务角度出发,该方案并不可行,一方面是需要所有页面手动指定可交互时机,无法统一代码处理,另一方面可交互时机的判断并不准确,极容易引起误差。所以我们抛弃了类似方案,结合实际场景和经验,制定了如下两个监控指标:

页面初次渲染次数FPT(First Paint Times)和从路由跳转到用户操作介入前渲染总时间TBUI(Paint Time Before User Interactive)。

页面初次渲染次数FPT

统计页面初次渲染次数,是希望从渲染次数的角度来监控页面渲染情况。由于我们的项目大量使用react hooks,很多页面代码不规范,处理依赖不明确,导致无效刷新过多。设置渲染间隔阈值为1s,即两次渲染时间间隔1s即认为其属于稳定状态,第二次渲染由用户手动触发,非代码初次渲染触发;监控总范围时间为10s,我们认为所有页面的初次渲染时间一定在10s内完成,这是一个感性上的偏大的值,在10s内,页面每触发一次刷新,并且渲染间隔小于1s,渲染次数都会加一,如果间隔大于1s,即立刻停止计数,并将当前所计的渲染总次数作为FPT的值;同时,如果用户操作介入,比如输入组件接受到输入、开关组件切换、按钮点击、tab切换等,也立即停止计数,并在适当的时机进行上报。 综合测试下来,该计数方式存在一定的偏差,但是绝大多数情况下都可以满足实际需求,其统计结果总体符合实际场景。

除了数据上报后统计平台,在APP端开发环境下也给开发者进行适度提醒,如果渲染次数小于5次,会输出普通的 log;如果渲染次数大于5次,会进行 toast 提示;由于渲染次数可以进行适当优化,但因为该项指标并没有直接影响用户体验,所以选择进行 toast 弱提示。

React Native APP页面渲染监测系统介绍

用户操作介入前渲染总时间TBUI

渲染时间的统计方式:同FPT计算方式相同,从路由跳转开始计时,直到页面最后一次刷新前停止(如果页面在 1s 内没有刷新,就认为页面处于稳定状态,之后的刷新任务是用户操作带来的)。

除了数据上报后统计平台,在APP端开发环境下也给开发者进行适度提醒,如果渲染时间小于 1s,会输出普通的 log;如果渲染时间超过1.5s,会通过弹框进行提示(因为渲染时间直接影响了用户体验,所以建议开发者立刻优化);

React Native APP页面渲染监测系统介绍

使用方式

BlPage 接收参数: id 为配置的路由名称,name 可以为任意字符,说明页面功能即可,例如生产任务执行、维保任务执行等;Wrapper 为包裹页面使用的组件,默认为 View,可以传入 ScrollViewKeyboardAvoidView 等;

<BlPage id={Routes.xx} name="页面名称" Wrapper={View}>
    <View/>
</BlPage>

接入限制

我们无法保证所有的开发者都默认遵守规则,将BlPage作为业务页面的根组件,于是我们开发了一个eslint插件,用于限制所有页面必须遵照该规则,否则代码无法被正常提交。

如果页面名称以 Screen 结尾,但是最外层没有使用 BlPage 包裹的话,eslint 会给出错误提示:

React Native APP页面渲染监测系统介绍

统计展示

统计结果可在管理后台查看。统计平台可以区分手机操作系统、环境查看,也可以通过页面名称、版本号、租户id、用户id等信息进行筛选查看。

统计方式分为三种:

  1. 全部数据列表:

React Native APP页面渲染监测系统介绍

  1. 以页面为聚合,展示所有页面在单环境下的柱状图分布情况。

React Native APP页面渲染监测系统介绍

  1. 查看单一页面的数据在不同时间段的变化情况,以折线图方式呈现。

React Native APP页面渲染监测系统介绍

统计方式

在路由跳转的时候,记录一下页面跳转时间:

export const useNavigationStart = (handler?: EventListenerCallback<EventMapBase, 'state'>) => {
  const navigation = useNavigation();
  useEffect(() => {
    // state change 发生在进入页面之后,并不是路由开始之前
    const unsubscribe = navigation.addListener('state', e => {
      lastTime = Date.now();
      handler?.(e);
    });
    return () => {
      unsubscribe();
      lastTime = 0;
    };
  }, [handler, navigation]);
};

页面每次刷新,renderTimes 都会加1:

export function useRenderTimesInMs(ms: number) {
  const startTime = useRef(Date.now());
  const renderTimes = useRef(1);
  useEffect(() => {
    if (Date.now() - startTime.current < ms) {
      renderTimes.current += 1;
    }
  });
  return {
    renderTimes: renderTimes.current,
  };
}

如果页面 1s 内没有更新,就认为页面已经处于稳定状态,不会继续统计;

useEffect(() => {
    if (stable) {
      return;
    }
    debounceSetStable();
    const now = Date.now();
    requestAnimationFrame(() => {
      renderCost.current = now - getNavigationTime();
      logRenderTimes(
        `${name || 'AnonymousPage'} 页面在 ${limitMS / 1000}'s 内渲染 ${renderTimes} 次数`,
      );
      log(
        `${name || 'AnonymousPage'} 从进入页面到页面最终渲染间隔: ${renderCost.current / 1000}s`,
      );
      if (renderTimes > maxRenderTimes) {
        const errorStr = `${name || 'AnonymousPage'} 页面在 ${
          limitMS / 1000
        }'s 内 render ${renderTimes} 次,超过 ${maxRenderTimes} 次`;
        logError(errorStr);
      }
    });
  });

如果用户操作介入,也认为页面已经处于稳定状态,停止统计:

useEffect(() => {
  BlInput.innerFunction.onChangeText =
    BlButton.innerFunction.onPress =
    BlTabView.innerFunction.onIndexChange =
    BlSearchBar.innerFunction.onChangeText =
    BlSwitch.innerFunction.onValueChange =
    BlSelector.innerFunction.onChange =
      () => {
        if (stableRef.current) return;
        stableRef.current = true;
        debounceAddRecordFlush();
      };
}, [debounceAddRecordFlush]);

展望未来

1、目前监听的路由跳转,实际上统计出来的时间不是从跳转开始,而是路由状态已经发生改变(已经进入页面),如果路由在跳转的时候做了繁重的操作,这部分时间没能统计在内;

2、细化网络请求时间统计和页面渲染时间统计,多维度分层次展示页面渲染耗时分布。