开发者社区 > 博文 > 最佳实践 | 京东小程序-LBS业务场景的性能提升
分享
  • 打开微信扫码分享

  • 点击前往QQ分享

  • 点击前往微博分享

  • 点击复制链接

最佳实践 | 京东小程序-LBS业务场景的性能提升

  • 18****
  • 2023-12-13
  • IP归属:北京
  • 11440浏览

    作者:姜微、邓树海、章文顺、王冰洋

    一、前言

    1.1 京东LBS门详业务介绍

    京东LBS门详目前已经支持了仓网、药急送、天选、小时达POP多种业务,并且具备了多端的能力,一套代码可以在京东app、健康app、微信小程序中运行,一定程度上研发效率的提升能够更加快速的支持业务迭代。

    随着业务需求猛增、各种AB场景线上测试,交互复杂度提升,所以对门详的整体交互体验,小程序加载速度、列表的滚动性能以及业务数据层面都有更高的要求,因此作为前端研发团队,我们也迎来了一些新的挑战。

    1.2 面临的挑战

    基于以上的业务与用户体验的要求,门详急需解决的问题如下:

    1.2.1 页面加载速度缓慢:在中高端机型,实测从点击到店头渲染数据在1.2s左右【iphone12,小米11青春版】;

    1.2.2 门详列表滑动性能差:

    1)和美团外卖、京东到家App原生相比,我们货架层面不能支持手势滑动;

    2)列表滑动多屏卡死,商品列表加载不流畅;

    ① 曝光埋点监听后,DOM元素的查找方式耗时,阻塞UI线程,导致滑动丢帧

    ② 和美团外卖进行比较,我们的列表没有联动加载的功能,给用户不好的体验

    ③ 列表中的分类及商卡数据联动逻辑复杂,不易维护

    ④ 列表内存占用高,不释放

    ⑤ 二级列表偶现闪动问题

    1.2.3 门详小购物车展开卡顿:加车超过20个品,在中高端机型,出现滑动的明显卡顿问题,且打开购物车缓慢,渲染内容缓慢,在iphone12机型上首次渲染需要2s左右;加车动画卡顿;

    1.2.4 门详埋点数据不准确:曝光埋点的上报时机和数据产品要求口径不一致

    为更好,更快速的支撑业务诉求,上述的问题解决也迫在眉睫。经过四期的性能优化迭代,我们取得了一定的优化成果。

    二、实践结果

    该部分首先重点介绍一下总结果概览,然后详细阐述一下页面启动和交互结果。

    2.1 结果概览

    当我们识别到上述的四类问题之后,进行了整体的问题的分析,识别出的需要高优优化的环节分别是:启动耗时、交互体验、能耗优化以及埋点;整体优化完成上线后,我们验证的结果如下:

    ① 门详加载速度整体提升了约50%

    同一网络环境下,同款机型优化前后的数据对比如下:

    ② 小程序健康度评分在90左右;

    ③ 整体的小程序崩失率减少了10%左右;

    ④ 埋点可用率在95%以上;

    2.2 启动结果

    ① 同5G环境,同门店,机型是iphone14pro,优化前后的效果对比:左侧是优化前右侧是优化后

    ② 同5G环境,同门店,机型是iphone14pro,优化后和原生做个对比:左侧是京东到家app原生右侧是优化后的京东门详小程序

    2.3 交互结果

    左侧是京东到家原生app,右侧是门详的效果

    从上述结果可以看出,在中高端机型上,门详的打开速度达到了秒开的效果。接下来详细介绍门详性能优化的分析历程以及解决方案。

    三、启动耗时

    下图是小程序的启动过程:

    影响启动的四个关键因素:

    1.引擎预热耗时:引擎初始化与小程序框架注入耗时;

    2.小程序信息接口耗时:获取小程序权限、基本配置信息、版本等信息,耗时相对固定;

    3.小程序加载耗时:跟业务包大小而变化,包含读取业务代码注入到小程序执行环境以及原生UI初始化耗时;

    4.业务初始化耗时:根据业务初始化逻辑与首屏网络接口耗时而变化;

    3.1 小程序信息接口异步化功能

    问题:启动小程序会同步获取小程序信息接口,小程序打开过程存在耗时。

    目标:减少小程序信息接口请求耗时。

    行动:

    1.通过将小程序信息同步接口信息提前缓存;

    2.打开小程序优先使用缓存信息,将同步接口异步化处理;

    结果:接口改为并行后,平均数据减少158ms左右。

    3.2 小程序按需加载功能

    问题:随着业务增长,小程序二进制包达到6MB+(包解压之后),JS注入执行已经成为耗时主要原因。

    目标:降低启动时业务注入时的代码量,从而降低注入耗时。

    行动:

    1.编译改造:将渲染层、逻辑层注入内容只保留业务公共框架的部分,页面的JS代码剥离到对应页面中;

    2.启动改造:启动时只注入公共框架与当前页面相关的代码,减少注入额外内容;

    3.改变包大小【具体操作可见3.3部分门详包瘦身】

    结果:优化前:6MB+ 数据,优化后:注入2.7MB数据, 使用小米11手机测试启动性能,未使用按需加载功能的启动时间为:1200ms左右,使用按需加载功能的启动时间是:800ms左右,性能显著提升。

    3.3  门详包瘦身

    问题:门详包的大小会直接影响小程序的加载时长

    目标:降低包大小,从Release代码压缩包1.2M降低到0.6M以下

    行动:

    1.分析包体积的构成,检查不合理的模块,移除冗余无用的模块;

    通过打包工具,查看包中内容,分析得到包体积较大的模块是 vendors 和 common 模块。

    vendors: node_modules 除 Taro 外的公共依赖;

    common: 项目中业务代码公共逻辑;

    taro: node_modules 中 Taro 相关依赖;

    runtime: Webpack 运行时入口;

    2.逐个模块优化

    ① 下线掉无用业务模块;

    ② 优化 vendors 模块,明显可以看到一些不合理的包被编译进来了,比如 Axios,小程序的请求一般用jd.request,理论上用不到Axios,此时我们可以打开 package-lock.json文件看下,是哪个包依赖了这个模块。例如:@plato这个包应该是在微信域的地址组件,在京东小程序里使用APP原生组件,所以可以采用端区分的方式process.env.TARO_ENV进行剔除。

    重复上述步骤, 检查 pako lego jsencrypt inversify ,将无用的依赖剔除,优化后的结果,由783KB下降到318KB

    ③ 优化 common 模块,包括:

    common.jxss:经检查,我们的样式文件中有不少icon使用的是base64,会增加包体积,此处我们可以采用雪碧图方案或者网络图进行替换。

    common.js:对存量代码进行优化,减少重复代码。此处我们可以借助eos进行代码扫描,检查到重复代码块进行代码合并。

    3.制定包瘦身规范,长期控制包大小增量;

    ① 预防为主

    在开发初期就要注重代码和资源文件的优化,避免不必要的代码和文件冗余,禁止重复造轮子;

    资源引入规范:使用到三方功能,需要将必要的功能拆解后独立引用,媒体资源引入,需以CDN方式,不允许直接放入项目本地;

    多端区分规范:合理使用process.env.TARO_ENV​ 进行代码剔除,不要引入无用的模块;

    优化构建工具:使用或开发构建工具来优化最终包的体积,如webpack 、terser 等工具;

    ② 防治结合:

    定期对小程序进行体积检查和优化,移除无用代码和资源,更新第三方库到更小体积的版本,以及优化图片和视频资源;

    定期审查和优化项目依赖,移除不必要或重复的库和框架;

    在代码review审查过程中检查新增代码对包体积的影响,避免重复的轮子入库;

    ③ 综合治理:

    建立反馈机制,当包体积超标时,能够及时通知相关人员;

    借助eos扫描,关注代码重复率,重复率高则需要及时制定整改方案;

    结果:经过上述过程后,包大小从1.2M降低到0.49M。

    3.4 应用启动耗时减少:(首屏渲染 、 接口预请求)

    问题:由于我们是基于LBS的门详业务,首页接口请求完成后,才能进行页面的整体渲染,所以首页主接口请求的耗时时长也成为我们loading时间长的原因之一;另外接口和地址获取串行的逻辑也加长了整体的数据链路;

    目标:业务加载时长在中高端机型降低到500ms以内

    行动:

    ① 将主接口的请求进行提前 -> 主接口预热

    一般来说小程序首屏的可交互时间都会受代码包注入执行时间影响,在代码执行完毕后才能开始业务网络请求,请求数据返回后才能开始真正的内容渲染,为了解决这一痛点,京东小程序Clips是一种通过三线程架构实现的特色能力,可以在小程序启动过程中注入一个轻量化代码片段,提前做初始化数据,网络请求等非UI功能,当小程序主环境准备好后,可直接将数据送给逻辑层,实现小程序业务预热能力。

    通过cilp文件发起预热请求的功能,以及在小程序实例化后对该请求结果的监听;达到首页的接口请求在我们小程序解析的同时,进行了数据的请求,实例化小程序后,在首页实例完成,准备请求首页数据时,如果预热接口已经返回,则直接用预热的结果,从而达到减少主接口请求的耗时。

    ② 将地址逻辑前置 -> 实例小程序的同时,获取地址信息

    结果:串行逻辑修改成并行之后,业务从开始loading 到出现渲染内容,整体时长在iphone12上可以达到350ms左右。

    ③ 全局渲染优化

    主要原因:

    ① 跳转协议、地址信息、主接口数据都是以Recoil状态值的方式存在,且颗粒度细碎,大面积多次造成全屏重绘。

    ② 页面渲染时,楼层多,细粒度状态触发频繁,导致最终整体DOM呈现时间晚,白屏等待时间久。

    分析解决:

    ① 状态归类合并,区别状态和变量

    ② 分批渲染楼层,局部渲染

    实现方式:

    采用全局Context状态管理的方式,重新改造了页面数据监听方案;从而达到减少多次渲染重绘的效果;

    最终实现效果如下图:

    上图中左侧是优化前,使用Recoil状态绑定组件,改变即变更的渲染方式。

    上图中右侧是优化后,使用React Context归类合并后的数据源,通过改变Context值,一次性触发数据更新,并分批渲染。

    新的Context将Recoil的状态改成值的方式,同时通过观察者模式给予组件订阅变量更新的能力,尽管Recoil的状态绑定更方便使用,但是其缺乏变量值的管理,因此可以采用Context弥补这一缺失的能力。

    Recoil用法的建议:

    1、使用Recoil状态的地方,一定是JSX中用到的值(状态变更则触发组件刷新),不要在逻辑中使用的变量也使用状态值;

    2、Recoil的颗粒度可以根据独立的业务模块设定,减少频繁无效的设置值,对象变更提前做好数据比对;

    3、组件只需要改变状态,不需要依赖状态时:推荐写法:const setName = useSetRecoilState(nameAtom);

    3.5 整体启动过程对比示意图如下:

    四、交互体验

    4.1 门详整体滑动优化

    以下是美团外卖、京东到家app门详和门详小程序的滑动效果对比分析:

    通过分析得出以下3个关键交互体验问题:

    ① 手势体验问题;

    ② 商品列表列表留白,分类切换不流畅、二级列表偶现闪动的问题;

    ③ 内存占用高;

    4.1.1 整个货架手势的支持

    前期我们做了支持手势的调研:

    方案对比
    movable-view 方案
    jds 动态设置样式方案
    scroll-view嵌套
    调研结果拖动效果卡部分功能暂时不支持,无法获取自定义组件实例在内层容器到顶部,内层切换滚动属性时,会顿一下

    根据调研结果综合评估:我们采用了scroll-view嵌套的方案ScrollView嵌套场景下,我们要进行手势的交互,以及跟手的处理,所以我们面临了一些挑战:

    ① 现存代码使用了大量的touch事件拦截,手势重构成本高;

    ② 页面存在多层吸顶和吸底动画交互效果,需要有合理的替代方案;

    原有支持自动吸顶,我们通过动态设置外层scrollTop替换原动画方案

    ③ 多层scroll-view嵌套,带来了多处手势冲突问题;

    在Taro3中小程序逻辑层实现了一套事件系统,包括事件触发和事件冒泡。在小程序模板中绑定的事件都是以bind的形式。

    一般情况下,这套在逻辑层实现的小程序事件系统是可以正常工作的,事件回调能正确触发、冒泡、停止冒泡。小程序原生模板中绑定的catchtouchmove事件除了可以阻止回调函数冒泡触发外,还能阻止视图的滚动穿透,但是Taro的事件系统是做不到的。

    Taro为我们提供的解决方案:可以为View组件增加catchMove属性:

    // 这个 View 组件会绑定 catchtouchmove 事件而不是 bindtouchmove
    <View catchMove={catchMove}></View>

    基于以上情况,我们采用动态控制catchmove的方式,来实现是否向上进行事件冒泡通知外层ScrollView。

    根据用户手势上推和下滑,控制内层ScrollView的scrollY是否可以滚动。


    我们在Taro组件中,动态设置catchMove来控制是否向上冒泡,但是在一些场景发现一个问题,已经完成渲染的dom树在切换catchMove时,DOM元素在视图内不可见。后经过与Taro团队沟通,在设置catchMove 时,Taro会使用不同的静态模版 ,为了解决该问题我们选择将catchMove 绑定在原生小程序组件上,采用原生和Taro混用解决这个问题。

    封装小程序原生组件:
    <view id="container"  catchtouchmove="{{catchMove}}">
      <slot></slot>
    </view>
    原生组件嵌套Taro组件:
    <native-container catchMove={catchMove}>
      嵌套Taro组件
    </native-container>

    经过我们不断的调优,各种机型的测试,最后优化前视频和优化后对比如下:

    4.1.2 列表流畅度优化

    中低端机型测试:在门详商品列表中浏览xxx个品后,页面出现卡屏卡死现象;

    所以针对上述问题,我们进行了列表的流畅度优化:

    曝光元素查找方式优化

    实践发现曝光createIntersectionObserver回调获取元素的方式严重影响页面的滑动流畅度。以下给出两种写法:

    const categroyNodeObserver = createIntersectionObserver(Taro.getCurrentInstance().page, { observeAll: true })
    categroyNodeObserver.observe('#app >>> .className', result => {
        // 1、通过class获取列表再遍历查找对应元素
        const vnodes = document.getElementsByClassName('className') as unknown as HTMLCollectionOf<Element>[];
        const target = vnodes.find(vnode => vnode.uid == result.id)
    
        // 2、通过id直接获取元素
        const target = document.getElementById(result.id);
    })

    通过对比:第2种获取元素的耗时在个位数毫秒级,第一种耗时高达上百毫秒。

    数据结构优化 - 树结构分类扁平化处理

    商品货架分类是一个嵌套树结构,最多有三级,如下图扁平化前黄色区域所示。

    在代码逻辑中,多处需要获取末级分类进行逻辑处理,比如:① 点击1级分类,需要找到其下的第一个3级分类进行接口请求;② 点击2级分类,需要找到其下的第一个3级分类进行接口请求;以上两种情况还有例外:如果没有3级分类,就会变成使用2级分类进行接口请求。如下图所示:

    未扁平化的数据结构向后查找下一个分类最大时间复杂度O(n);数据结构扁平化后,将嵌套结构改成单层链式结构,如上图扁平化后绿色区域,后续可以直接在新的数据结构上处理,向后查找下一个分类最大时间复杂度O(3)。

    扁平化递归算法:

    普通递归,随着分类数据的增加,递归调用栈很多变量开辟了内存空间未被释放,所以要用到尾递归。

    尾递归是一种特殊的递归,它的特点是在函数的最后一步调用自身,而不是在调用后还有其他操作。尾递归可以有效地避免栈溢出的风险,因为它不需要保存每次调用的上下文,只需要保留一个栈帧即可。尾递归也可以提高递归的性能,因为它减少了函数调用的开销。

    4.1.3 支持列表补足功能

    现有设计方案中,多分类商品无联动加载逻辑,滑动时有顿挫感,不流畅。

    因此,我们在分类扁平化的基础上,将多分类商品进行连续请求处理,占满屏幕,如下图右侧效果。

    列表优化过程:

    ① 新的加载流程如下:

    商品列表滑动过程中,分类的联动选中,我们的做法是:在上述流程图中的组装数据环节,在分类末尾位置增加标记位,一条1px的线,然后利用createIntersectionObserver,监听该元素上推消失和下拉露出的时机,联动选中对应的1,2,3 级分类。

    ② 列表分页加载方式选择

    触底加载时机试验对比:

    序号
    触底监听方式
    稳定性
    说明
    1
    Scrollview触底函数
    onScrollToLower
    不稳定
    lowerThreshold设置较大时,稍微上拉速度快一些,频繁触发scrolltolower事件,导致列表滑动+频繁渲染,列表抖动。
    lowerThreshold设置为0时,偶发不回掉onScrollToLower
    2
    利用createIntersectionObserver监听底部Loading露出
    稳定
    触底及时回调

    监听Loading的露出来作为下一页的请求回调时机,如下图:

    ③ 列表分页渲染层级优化

    现状:采用递归的方式 + CustomWrapper 进行渲染列表。有效解决每次setData都是全数据耗时的情况。同时引入一些弊端,其中主要的问题是,头部排序条的position: sticky,在多分类联动商卡页数较多的时候,层级嵌套多层后,sticky的元素会被推出可视区域外。

    为了实现setData时,Taro可以增量给小程序传递数据,我们回到React底层diff原理,考虑diff可以通过key值优化,于是在Render Row每行元素上增加位置key值,结果:Taro即可进行自动diff,更新单页数据,这样DOM结构由原先的嵌套结构变成一层。解决了上述sticky的问题,也提升渲染性能。

    加载10页,每页setData大约消耗时间:

    4.1.4 列表内存占用过高

    通过Chrome://inspect性能检测工具,做了内存快照比对发现主要原因有两点:

    ① Taro侧: 经过内存快照对比工具,发现Taro侧有两个问题,一是清空列表前后 Taro 生成的 react fiber 树(th对象)一直持有缓存数据,二是每次setData的数据没有释放掉,导致内存持续增长。

    ② 小程序引擎侧:在自定义组件更新时,diff前后virtualTree计算出需要更新的节点,对于 jd:if 和 jd:for 标记的节点没有识别并清理掉。

    经过和引擎侧和Taro侧多次沟通联调,最终解决上述两个问题。Taro侧问题是升级到3.5.1解决,引擎侧是商城App 12.2.4 版本修复。

    4.2 门详二级页面/购物车卡片/门详弹窗优化

    随着业务增多,门详二级页面的需求量也在不断迭代,需要支撑业务进行更多品,更多功能的展露,例如加价购、搭配购买等等;所以我们针对各种二级页面和弹窗卡片也进行了性能上的体验优化;从中主要解决了以下几类问题:

    ① 列表页面的整体列表抖动问题的修复;

    ② 二级页面的渲染时序优化,组件插入方式变更;

    ③ 小车卡片上大量数据加载的扁平化处理,分页加载功能的支持;

    ④ 弹窗功能的整体统一;

    二级页面的详细优化后续我们会继续发文。

    五、能耗优化

    实测:门详商品列表滑动50页左右,手机明显发烫,耗电严重,所以高能耗也是我们需要高优解决的问题。

    具体分析步骤如下:

    ① 手机静止不动,手机发热、发烫;

    ② 列表不断滚动,手机发热、发烫;

    5.1 静止状态下的消耗

    ① 布局与渲染:说明有dom内容一直在变。如倒计时、banner轮播.

    ② 媒体与动画:说明有css在一直执行动画。如呼吸动画、loading.

    ③ javascript:如倒计时事件.

    基于以上,我们针对站内的倒计时,页面的Banner轮播 做了隐藏停止的处理;动画效果的css样式由gif图进行了替换;最后达成效果如下:

    能耗的结果:

    静止情况:能耗由高降为低。

    在滑动情况下:我们做了滚动防抖和图片大小优化,滑动瞬时能耗降低了20%,在滑动70页后开始发热,在dom节点数量以及节点深度上还有优化的空间,后续我们会继续分享。

    我们总结了能耗优化的一些方向,并放到了我们的开发规范中:

    影响能耗的类型 要求
    css动画类 animation xxx 1s infinite; 用完移除,不要在后台一直执行动画
    setInterval 倒计时结合requestanimation优化
    swiper组件类视口外停止轮播 加长轮播间隔时间
    image组件类根据视口图片大小加载适当尺寸的图片
    scroll-view 滚动类避免节点过多 避免节点层级过深
    onScroll回调回调事件防抖 避免复杂逻辑
    元素监听类监听及时移除 监听回调事件避免做复杂逻辑


    六、埋点治理

    我们在9月份最后一个版本上线了全站的埋点方案的整改。其中包含底层埋点(PV CLICK EP)埋点参数的逻辑优化,以及所有业务上报埋点的机制与时机处理。整改之前,埋点存在部分问题:

    ① 曝光埋点的上报时机有误,不可用;

    ② PV埋点和点击埋点缺少规范,业务添加埋点不规范,成本高;

    ③ 曝光请求未聚合,导致频繁发起埋点请求;

    整改之后,我们输出了门详的整体埋点开发规范;重新整理了所有曝光埋点的上报时机与整体方案;目前门详埋点可用率达到95%以上;为业务数据提供了更加准确的方向。

    七、总结与展望

    以上是我们近期优化的阶段性成果,不仅得到了业务和产品同学的认可,而且线上用户反馈门详加载慢、卡顿的问题占比降低了约15%。

    用户体验提升是需要长期坚持,技术同学需要具备一定的工匠精神,持续探索,我们在规划中的事情还有:

    ① 能耗层面的继续探索与优化;

    虚拟列表的支持:目前门店列表支持多分类的加载联动,且在同一个分类下进行了组件复用与回收;下一步,我们将继续进行跨分类的组件复用与回收,将门详列表的回收渲染机制形成统一性的解决方案。

    嵌套层级过深导致的性能消耗:进一步减少门详组件嵌套的问题,从内到外进行整体组件嵌套层级的优化,减缓手机发热发烫的问题。

    ② 优化方案同步微信域;

    ③ 共建能力的支持;

    提供门详组件的共建能力,将门详组件与业务进行解耦。

    ④ 小程序对H5的转化;

    ⑤ 近原生体验的提升:视频能力的支持;

    ⑥ 小程序侧手势的支持;

    期待我们在高效支撑业务迭代的同时,可以给用户带来更极致的体验!!


    要特别感谢以下团队和部门:

    京喜与新业务研发部-京东小程序研发团队,感谢你们在性能优化方面给予的大力支持。

    Taro团队,你们的支持对我们至关重要。

    LBS业务产品组-小时购前台产品团队,你们的努力让我们的产品更加完善。

    同城研发部-质量提升团队,你们的专业测试保证了我们服务的高质量。

    同城研发部-到家平台研发组,感谢你们在整理工作中的协助。

    你们的帮助对我们的成功至关重要,再次表示衷心的感谢!



    文章数
    1
    阅读量
    286

    作者其他文章