写点什么

【最佳实践】京东小程序 -LBS 业务场景的性能提升 | 京东云技术团队

  • 2023-12-20
    北京
  • 本文字数:7724 字

    阅读完需:约 25 分钟

【最佳实践】京东小程序-LBS业务场景的性能提升 | 京东云技术团队

一、前言

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 整个货架手势的支持

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




根据调研结果综合评估:我们采用了 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 级分类。



② 列表分页加载方式选择


触底加载时机试验对比



监听 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 节点数量以及节点深度上还有优化的空间,后续我们会继续分享。


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



六、埋点治理

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


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


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


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


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

七、总结与展望

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


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


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


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


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


② 优化方案同步微信域;


③ 共建能力的支持;


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


④ 小程序对 H5 的转化;


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


⑥ 小程序侧手势的支持;


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


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


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


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


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


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


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


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


作者:京东零售 姜微、邓树海、章文顺、王冰洋

来源:京东云开发者社区 转载请注明来源

发布于: 刚刚阅读数: 4
用户头像

拥抱技术,与开发者携手创造未来! 2018-11-20 加入

我们将持续为人工智能、大数据、云计算、物联网等相关领域的开发者,提供技术干货、行业技术内容、技术落地实践等文章内容。京东云开发者社区官方网站【https://developer.jdcloud.com/】,欢迎大家来玩

评论

发布
暂无评论
【最佳实践】京东小程序-LBS业务场景的性能提升 | 京东云技术团队_小程序_京东科技开发者_InfoQ写作社区