文 / 腾讯 龚麒

0.前言

react native给了我们使用javascript开发原生app的能力,在使用react native完成兴趣部落安卓端发现tab改造后,我们开始对由react native实现的界面进行持续优化。目标只有一个,在享受react native带来的新特性的同时,在体验上无限逼近原生实现。作为一名前端开发,本文会从前端角度,探索react native首屏渲染最佳实践。

1.首屏耗时计算方法

1.1我们关注的耗时

优化首屏渲染耗时,需要先定义首屏耗时的衡量方法。将react native集成至原生app中时,可以将首屏耗时定义为如下

首屏耗时=react native上下文初始化耗时+首屏视图渲染耗时

其中,react native上下文初始化耗时为一个固定开销,通过将初始化过程提前至app启动后异步进行,在安卓端,这一耗时已经可以降低到约70ms。本文关注的是首屏视图渲染耗时,文中优化探索是在安卓端react native结合版app中进行,但其思路和方法,可以复用至iOS端。

1.2渲染耗时衡量方法

关注首屏视图渲染耗时,需要理解react框架视图渲染流程,相应的需要了解其生命周期方法。下图是一张react组件完整的声明周期图,从图中可以看出,上方虚线框内为生命周期第一阶段,这个阶段完成初始化,并第一次渲染组件。左下角虚线框为组件运行和交互阶段,这里可能会再次渲染组件。右下角虚线框为第三阶段,组件这一阶段卸载消亡。

对应上述生命周期方法,我们在初始阶段首先渲染loading视图,并开始拉取数据。获取数据后,通过改变状态(state),触发视图的再次渲染,在屏幕绘制出视图。

结合上述分析,我们将首屏视图渲染耗时定义为如下

首屏视图渲染耗时=compontDidUpdate结束时间 – compontWillMount开始时间

2.开启渲染优化探索之路

2.1输出当前渲染耗时

既然已经明确了视图渲染耗时的计算方法,那么就可以打时间点日志输出这一耗时看看。这里查看耗时有两种方法。

使用调试模式,通过console.log,在chrome调试工具中输出时间差值

封装native层日志方法封装给react native层调用,将日志输出到app日志文件中。

推荐使用第二种方法,采用这种方法,可以以release模式运行app,输出结果与我们普通的安装运行app表现一致。

这时我们的耗时输出是怎样的呢?查看日志得知,在wifi环境下,荣耀4X手机上,渲染耗时为约700ms,加上react native上下文初始化时间约70ms耗时,整个界面的耗时需要约770ms。

那么原生app的实现中,发现tab渲染耗时是多少呢?通过打点发现,安卓端原生app实现中,发现tab从onCreateViewEx开始至onResume结束,耗时约100ms。

2.2来吧,加上缓存

看到上述数据时,我的内心是有点崩溃的,说好的高性能呢?但是我并不慌,根据我不多的前端开发经验,我知道有一个大招还没有用上,那就是使用缓存数据。

react native为我们提供了AsyncStorage模块,AsyncStorage是一个简单的、异步的、持久化的Key-Value存储系统,它对于App来说是全局性的。相对于之前我们使用LocalStorage存储数据,在react native端,我们可以使用AsyncStorage作为数据存储解决方案。

在使用缓存之前,我们的视图渲染流程如下所示

上述流程中,每次渲染界面视图前,都需要等待网络请求。这里我们将请求数据缓存起来,渲染界面视图时,首先使用缓存数据渲染视图,同时发起网络请求,数据返回时再以新的数据渲染一遍。使用缓存之后的渲染流程如下图所示

上述流程中,当有缓存数据时,我们会快速拿到缓存数据渲染出界面视图。然后在网络请求返回数据时,再次触发render方法,以新数据再次渲染视图。

这里为了提升性能,避免多余的触发render方法,可以在shouldComponentUpdate方法中判断cache data和response data差异,仅当两份数据不一致时才再次触发render方法。同时,得益于react 框架的虚拟dom特性,在网络请求的数据返回再次触发render方法后,react native会计算dom diff并以此为依据来判定视图更新范围。

此时通过日志数据可以看到,在有缓存场景下,我们获取缓存时间耗时约在40ms,此时我们的渲染耗时下降至400ms。

2.3接管轮播图

加入缓存优化后,我们将渲染耗时降低至400ms,但是仍与原生实现中耗时有较大差距。此时的优化进入了深水区,经过梳理界面视图,可以将视图划分为上部轮播图、中间部落列表和下部热门帖子三个模块,逐一优化。

在最初的版本中,我们是这么实现轮播组件的:在请求的返回结果中,取出轮播图集合,依次生成全部图片视图,并添加至轮播图容器视图中。最后监听容器视图的对应事件,设置当前显示的图片视图。

如下图所示,当结果数据中共有五张轮播图时,会有五个图片视图被添加至轮播图容器中。初始时仅首张图片视图可见,容器内滑动事件发生时,图片视图可见状态随之改变。

从上述过程可以看出一个明显的问题:对首屏来讲,轮播图容器内,非可见状态的其余图片视图的渲染是没有意义的。

既然已经找到了问题,优化是思路也就很自然:在从请求结果中获取轮播图集合后,不是生成全部图片视图,而是仅生成一份,将这一个图片视图添加至容器内,并为他设置当前图片的url地址。当容器内滑动事件发生时,不再是切换不同视图的可见状态,而是复用这一个图片视图,切换视图图片的url地址,其流程如下图所示。

采用这一方案,完全去除了非可见状态的图片视图的渲染耗时,同时,通过复用视图,也降低了对内存的占用。这一方案在理论上将渲染耗时和内存占用做到了最优,然而在实际运用中,会有一个体验的问题:当滑动发生时,才加载下一张图片并刷新图片视图,会导致容器内滑动事件的卡顿,使得滑动有阻滞感。因此我们最终采用了一种折衷方案:当轮播图集合超过三张时,在容器内加入当前图片视图,同时提前加入当前图片的上一张和下一张图片视图。容器内滑动事件发生时,复用这三个视图,来设置这次事件对应的当前图片、上一张和下一站图片视图。

通过日志输出优化前后的耗时数据,采用这一方案时,在渲染耗时上降低了约30ms,此时我们的首屏渲染耗时下降到了约370ms。

2.4为列表加特效

在发现tab界面中部,是子类别的部落列表。子类别数量通常是两个,每个子类别下一般有七至八个列表项,每个列表项由部落图标和部落名称组成。

最初的版本中,我们从请求结果数据中获取子类别下部落集合,以ScrollView为容器,依次创建列表项视图并添加至容器中。这样,当所有列表项渲染完成,该子类的部落列表才渲染完成,最初的实现示意如下所示。

通过上述过程可以发现,在部落列表渲染过程中,等待非可视区域的列表项的图片下载再渲染该视图,对于首屏是没有意义的。

结合对轮播图组件的优化经验,我们尝试延迟加载列表项视图。理想的状态是在首屏仅渲染可见的几个列表项,非可见区域的列表项,延迟渲染,流程如下所示

很快我们遇到了问题,在轮播图组件中,首屏仅显示第一张图片。但是在横向部落列表中,首屏应该渲染几个列表项呢?答案是我们无法提前确定,首屏可见的列表项,只有在渲染时,根据当前列表项在横向列表中的相对位置,才能计算出该项是否可见。

继续思考,虽然我们不能预知在首屏应该几个列表项,但是利用react native 提供的API方法,我们可以获取当前视图相对于容器的坐标位置。

static measureLayout(relativeToNativeNode: number, 
onSuccess: MeasureLayoutOnSuccessCallback, onFail: () => void)

利用measureLayout方法,在视图完成布局,触发其onLayout事件回调后,可以通过视图的measureLayout方法,传入容器节点,从而获得当前视图在容器中的相对位置。利用这一特性,结合我们的视图特点——列表项视图的尺寸是确定的,可以在初始时,将所有列表项视图渲染为这一特定尺寸的空视图,待所有空视图渲染完成后,获取视图在容器中相对位置,仅将可视区域内的列表项用真实视图重新渲染。同时监听列表容器的滚动事件,在滚动事件回调中,计算视图在容器中的相对位置,当视图进入了可视区域时,再用真实视图替换空视图,上述流程如下所示

通过上述方案,我们可以实现列表项的延迟渲染特性,通过日志输出耗时时间,可以发现通过这一优化,渲染优化又可降低约30ms。

上述方案中,屏幕在首次渲染空白视图和再次渲染首屏可见列表项视图之间,有短暂闪动的感觉,为解决这一体验问题,可使用占位图方案替代空视图方案,即首先都用静态资源占位图来渲染列表项icon图,并延迟加载非可见列表项icon图,其方案如下所示

实现效果如下,首次渲染时均用占位图,首屏区域内的列表项icon视图依次下载并重新渲染。在采用这样实现后,解决了空视图带来的屏幕闪动问题,快速的完成初次渲染,实现了icon图片的延迟加载,渲染耗时稍有增加,相比空视图方案耗时增加约10ms,此时我们的首屏渲染耗时下降到了约350ms。

2.5热门帖模块的归宿

发现tab视图下部还有一个模块,如下所示,是几条热门帖子的展示区域。对于首屏内容来说,这一模块整个都属于非可视区域,因此我们需要设法将这一块的耗时从首屏耗时中拿掉。

有了上面部落列表的优化经验,我们已经知道,在布局事件完成后,可以获取视图在容器内的相对位置,对于滚动视图如ScrollView,我们可以监听他的滚动事件,来判断视图距离可视区域的位置以决定是否渲染该视图,为了优化体验,我们再给视图的渲染设置一个提前阈值aheadDistance,当视图距离可视区域还有aheadDistance单位时,提前渲染该视图,这一方法如下图所示

采用这一方案后,已经可以将热门帖子模块的渲染耗时从首屏耗时中去掉了,此时通过日志输出耗时分析,首屏渲染耗时下降到了约270ms。

那么这块的优化工作完成了吗?没有,在优化做完,回顾体验时,我们发现了这里仍存在体验问题。这一体验问题与发现tab的视图结构有关。在发现tab中,热门帖子模块,距可视区域底部的距离不远。因为本身距离可视区域底部很接近,所以ahead Distance的设置并没有起到预想的效果,导致发现tab界面向下滑动时,因为实时去渲染热门帖子模块,使得滑动不流畅,有阻滞感。

为了解决这一体验问题,继续对热门帖子模块做了定制的优化,最终对热门帖子模块做到了异步渲染。在首屏渲染时,还是将热门帖子渲染为空视图,然后将ahead Distance调大,使得空视图落在ahead Distance区域内,接着使用setTimeout技巧,异步的去计算该空视图在容器中位置,并将热门帖子真实视图渲染出来。

通过上述方案,将延迟渲染替换为异步渲染,在不增加渲染耗时的基础上,同时解决了滑动不流畅的体验问题,最终将首屏渲染耗时定格在约270ms。

3.总结

react native框架给了我们新的能力,使得我们可以用javascript开发原生app。当我们走入react native应用的深水区,开始对他进行各方面细致的优化时,我们在原来web端积累的最佳实践依然有效,诸如缓存的使用、元素复用、延迟加载、异步加载等方法依然会起到很好的效果。

与此同时,对渲染耗时的优化也要兼顾使用的体验,我们应该追求的,不仅仅是快。而是在快的基础上,也有很好的使用体验。

优化探索到最后,我们将首屏渲染耗时定格在约270ms,加上react native初始化耗时约70ms,我们的首屏总耗时仍需约340ms,相比原生实现仍然存在差距。原生实现的渲染速度也是经过了层层的优化,而我们对react native最佳实践的探索也没有结束,欢迎大家指导、探讨。

想了解更多干货,欢迎关注腾讯优测微信号:腾讯优测(wxutest)

腾讯优测:

腾讯优测是备受客户信赖的移动云测试平台,为应用、游戏,H5混合应用的研发团队提供产品质量检测与问题解决服务。不仅在线上平台提供「云手机远程操控与调试」、「自动化全面兼容性测试」、「APK源码级缺陷性分析」等多种质量检测工具,更为VIP客户配备专家团队提供定制化综合测试解决方案。真机实验室配备上千款手机,覆盖市面98%主流机型,7*24小时在线运行,覆盖亿级用户。

探索react native首屏渲染最佳实践的更多相关文章

  1. 腾讯优测优分享 | 探索react native首屏渲染最佳实践

    腾讯优测是专业的移动云测试平台,旗下的优分享不定时提供大量移动研发及测试相关的干货~ 此文主要与以下内容相关,希望对大家有帮助. react native给了我们使用javascript开发原生app ...

  2. React服务器渲染最佳实践

    源码地址:https://github.com/skyFi/dva-starter React服务器渲染最佳实践 dva-starter 完美使用 dva react react-router,最好用 ...

  3. 基于React Native的58 APP开发实践

    React Native在iOS界早就炒的火热了,随着2015年底Android端推出后,一套代码能运行于双平台上,真正拥有了Hybrid框架的所有优势.再加上Native的优秀性能,让越来越多的公司 ...

  4. 基于React Native的移动平台研发实践分享

    转载:http://blog.csdn.net/haozhenming/article/details/72772787 本文目录: 一.React Native 已经成为了移动前端技术的趋势 二.基 ...

  5. React Native 红屏之Could not connect to development server.

    React Native 是目前最火的开发框架,其他不说了,上Bug. 按照  React Native iOS环境搭建 高级版 在mac上  搭建 React Native  环境,运行 项目 若出 ...

  6. 【react native】rn踩坑实践——从输入框“们”开始

    因为团队技术栈变更为react native,所以开始写起了rn的代码,虽然rn与react份数同源,但是由于有很多native有关的交互和变动,实际使用还是碰到蛮多问题的,于是便有了这个系列,本来第 ...

  7. 首屏渲染时间获取 performance.now()

    Performance — 前端性能监控利器   最近在写一个监控脚本,终于有机会接触到了这一块,整理后写下了本文.Performance是一个做前端性能监控离不开的API,最好在页面完全加载完成之后 ...

  8. 如何使用webpack优化首屏渲染时间

    其实说到性能优化,他的范围太广了,今天我们就只聊一聊通过webpack配置减少http请求数量这个点吧. 简单说下工作中遇到的问题吧,我们做的一个项目中首页用了十多张图片,每张图片都是一个静态资源,所 ...

  9. vue 首屏渲染优化 -- 这个不错

    这篇文章分享了从遇到前端业务性能问题,到分析.解决并且梳理出通用的Vue 2.x 组件级懒加载解决方案(Vue Lazy Component )的过程. 初始加载资源过多 问题起源于我们的一个页面,下 ...

随机推荐

  1. jquery 字数限制

    $("#TextArea1").keydown(function(){ 10 var curLength=$("#TextArea1").val().lengt ...

  2. 河流 tyvj1506

    题目大意: 给出一棵n个节点的有根树,一开始 树根 是一个控制点,现在要增加m个控制点,使得总费用最少. 给出每个节点的父节点以及到父节点的距离,还有这个节点的权值, 一个节点的费用 即它的权值 乘以 ...

  3. [开发笔记]-“在引用COM组件时,出现了无法嵌入互操作类型。。。”的错误

    这两天在做一个需要将wps文档转换成word文档的程序,在调用wps的com组件时项目编译是没有问题的,但当运行的时候却弹出了下面的错误提示: 从网上百度一番后,找到了正确的解决方法. 先从Com组件 ...

  4. Linux-C程序的存储空间布局

    正文段 指的是由CPU执行的机器代码,通常,正文段是可以共享的,执行的程序在存储器中只有一个副本.通常也是只读的,防止程序本身被修改. 初始化数据段 数据段,被明确赋值的变量,比如全局变量 非初始化数 ...

  5. zoj 3228 覆盖及非覆盖串的多次匹配

    题目题意: 给定多个小串,在一个长串中寻找这些串的匹配次数,有些统计的是可覆盖的,有些统计的是非覆盖的 先可以简单理解一下,建立ac自动机后,当前节点包含的字符串必然被把它作为fail指针的节点包含, ...

  6. <转>2015-7-14面试题

    由于一些原因,最近打算换一份工作,主要目标是大型的互联网公司.在经历了上周三天小公司试水后,昨天终于开始正式的面试之旅了(其实接到面试通知的就几家公司

  7. 【python】dict。字典

    特点:以空间换取时间,使用HASH算法通过key算出了value的内存地址,建立索引,拿到key后查找速度快,但内存浪费多 因为是用key值算的内存地址,所以key为不可变变量 (set,和dict类 ...

  8. hdu 1069

    //Accepted 264 KB 0 ms //每种block只有三种方法,且每种放法至多放一次 //规定三条边的顺序后 //把所有的block按x递增排序,x相同则按y递增排序 //然后dp // ...

  9. hadoop环境搭建遇到问题集锦

    1  在hadoop的bin目录下, 运行hadoop version命令,提示“hadoop:没有此命令” 解决办法: ./hadoop version或者$HADOOP_HOME/bin放在PAT ...

  10. UIControl的使用

    在开发当中,可能很多时候都需要做个点赞的需求,如果用按钮实现,按钮作为一个系统复合控件,外部是一个 View-->UIControl的容器, 内部包含了UILabel和UIImage,以及一些排 ...