为了达到接近原生的体验,更好地支撑业务的快速迭代,京东门香团队在启动、交互、能耗、追踪点等方面进行了整体的优化提升。
一、引言
【1.1 京东LBS服务介绍】
京东LBS目前已支持仓配、药品应急配送、天选、小时POP等多个业务,具备多终端能力,一套代码可以在京东APP、健康APP、微信小程序中运行,一定程度上研发效率的提升可以更快速地支撑业务迭代。
随着业务需求的激增,以及各类 AB 场景的上线测试,交互的复杂度也随之提升,因此对门的整体交互体验、小程序的加载速度、列表的滚动性能、业务数据层面都有了更高的要求。因此,作为前端研发团队,我们也迎来了一些新的挑战。
【1.2 挑战】
基于以上业务和用户体验诉求,亟待解决以下问题:
1.2.1 页面加载速度慢:
中高端机型实测从点击到存储渲染数据约为1.2s[,小米11青春版];
1.2.2门细则滑动性能差:
1)与美团外卖、京东到家原生APP相比,我们的货架层级无法支持手势滑动;
2)列表滚动在多屏时卡住,商品列表加载不流畅;
① 监控到曝光跟踪点后,DOM元素的查找方法耗时较大,阻塞UI线程,导致滑动时丢帧
②与美团外卖相比,我们的榜单没有联动加载功能,给用户体验不好
③ 列表中分类与名片数据联动逻辑复杂,维护困难
④ 列表内存占用高,未释放
⑤ 二级列表偶尔闪烁
1.2.3 小购物车展开时卡住:
当添加超过20件商品到购物车时,在中高端机型上滑动有明显卡顿,购物车打开和渲染内容较慢,机型上首次渲染大概需要2秒,添加购物车动画卡顿;
1.2.4门的详细数据不准确:
暴露点报告时序与数据产品要求不一致
为了更好更快地支撑业务需求,解决上述问题刻不容缓,经过四轮性能优化迭代,我们取得了一定的优化成果。
2. 实践结果
本节首先对整体结果进行概述,然后对页面启动和交互结果进行详细说明。
本节首先对整体结果进行概述,然后对页面启动和交互结果进行阐述。【2.1 结果概述】
在确定了以上四类问题之后,我们对问题进行了整体分析,确定了需要优化的方面:启动时间、交互体验、能耗优化、追踪点。整体优化完成上线后,我们验证了以下效果:
① 整体门加载速度提升约50%;
在同样的网络环境下,同一模型优化前后数据对比如下:
②小程序健康评分在90分左右;
③小程序整体崩溃率降低10%左右;
④埋点可用率95%以上;
【2.2 启动结果】
① 同一5G环境,同一门店,同一机型,优化前后效果对比:左侧为优化前;右侧为优化后。
② 在相同5G环境、相同门店、相同设备型号下,对优化版与原生版进行对比,左侧为原生京东到家APP,右侧为优化版京东门香小程序。
【2.3 相互作用结果】
左边是京东到家原生APP,右边是门细节效果。
从上面的结果我们可以看出在中高端车型上,开门速度可以达到秒级的效果。接下来我们将详细介绍门性能优化的分析过程和解决方案。
3. 启动时间
本节首先对整体结果进行概述,然后对页面启动和交互结果进行详细说明。
下图是小程序的启动流程:
影响创业的四个关键因素:
1、引擎预热时间:引擎初始化、小程序框架注入都需要时间;
2.小程序信息接口时间耗费:获取小程序权限、基本配置信息、版本等信息需要花费相对固定的时间;
3.小程序加载时间:随业务包大小不同而变化,包括读取业务代码并注入小程序执行环境、初始化原生UI的时间;
4、服务初始化时间:根据服务初始化逻辑和首屏网口消耗的时间不同而不同;
【3.1 小程序信息接口异步功能】
问题:启动小程序会同步获取小程序信息接口,小程序打开过程耗时较长。
目的:减少小程序信息界面请求的时间。
行动:
1、通过提前缓存小程序信息同步接口信息;
2、打开小程序时优先使用缓存信息,同步接口异步处理;
结果:接口改为平行后,平均数据减少了约。
【3.2 小程序按需加载功能】
问题:随着业务的增长,小程序二进制包大小达到6MB+(解压后),JS注入执行成为耗时的主要原因。
目标:减少启动时服务注入所需的代码量,从而减少注入时间。
行动:
1、编译转化:将内容注入渲染层和逻辑层只保留业务公共框架,将页面的JS代码剥离到相应页面;
2、启动改造:启动时仅注入公共框架和当前页面相关的代码,减少额外内容的注入;
3.改变包裹大小【具体操作请参考3.3节详细包裹缩减】
结果:优化前:6MB+数据,优化后:注入2.7MB数据,使用小米11手机测试启动性能,未使用按需加载功能启动时间约为:,使用按需加载功能启动时间约为:,性能明显提升。
【3.3 门细节包括减重】
问题:入口包的大小会直接影响小程序的加载时间
目标:将包大小从 1.2M 减小到 0.6M 以下。
行动:
1、分析包大小的组成,检查不合理的模块,去除冗余、无用的模块;
使用打包工具查看封装内容,分析出封装体积较大的模块为和模块。
:除Taro之外的公共依赖项;
:项目中业务代码的公共逻辑;
taro:Taro 相关依赖;
:运行时入口;
2.优化各模块
① 下线无用的业务模块;
② 优化模块。很明显,编译进来了一些不合理的包。比如小程序的请求一般用的是jd.,理论上是不需要的。这时候我们可以打开-lock.json文件,看看这个模块依赖了哪些包。比如:@这个包应该是微信域中的地址组件。京东小程序用的是APP原生组件,所以可以用.env.端到端的区分方法剔除。
重复以上步骤,检查pako lego,去除无用的依赖项,优化后的结果从减少到。
③优化模块,包括:
.jxss:经过检查发现我们的样式文件中用到了很多图标,这会增加包体积,这里可以使用雪碧图或者网络图片来代替它们。
.js: 优化已有的代码,减少重复的代码,这里我们可以使用eos来扫描代码,合并重复的代码块。
3、制定封装瘦身标准,长期控制封装尺寸增量;
①预防为主
在开发初期,要注重优化代码和资源文件,避免不必要的代码和文件冗余,避免重新发明轮子。
资源引入规范:使用第三方功能时,需要将必要功能拆开单独引用,媒体资源必须通过CDN方式引入,不允许直接放在项目本地;
多终端区分标准:使用.env.适当去除代码,不引入无用的模块;
优化构建工具:使用或开发构建工具来优化最终包的大小,例如、和其他工具;
②防治结合:
定期检查并优化小程序大小,删除无用的代码和资源,更新第三方库为较小版本,优化图片、视频资源;
定期审查并优化项目依赖关系,删除不必要或重复的库和框架;
在代码评审过程中,检查新代码对包大小的影响,避免重复的轮子入库;
③综合治理:
建立反馈机制,当包裹体积超标时及时通知相关人员;
借助EOS扫描,关注代码重复率,若重复率较高,需及时制定整改方案;
结果:经过上述处理后,包大小从1.2M减小到了0.49M。
【3.4 减少应用启动时间:(首屏渲染、界面预请求)】
问题:由于我们的服务是基于LBS的,需要在首页界面请求完成后才能进行页面的整体渲染,因此首页主界面的请求耗时也是我们耗时较长的原因之一,另外界面和地址获取的串行逻辑也拉长了整体的数据链路。
目标:将中端和高端机型的服务加载时间缩短至 100% 以下。
行动:
① 主界面推进请求 -> 预热主界面
一般来说,小程序首屏交互时间会受到代码包注入执行时间的影响,代码执行完成后才能开始业务网络请求,请求数据返回后才能开始真正的内容渲染。为了解决这一痛点,京东小程序通过三线程架构实现一项特殊能力,在小程序启动过程中注入轻量级的代码片段,提前初始化数据、网络请求等非UI功能,当小程序主环境就绪后,直接将数据下发给逻辑层,实现小程序业务预热能力。
小程序实例化后,通过cilp文件发起预热请求,并监控请求结果的功能;对首页的接口请求,由我们的小程序进行解析,并进行数据请求。小程序实例化后,当首页实例化完成,并且首页数据可以请求时,如果预热接口已经返回,则直接使用预热结果,从而减少主接口请求的时间消耗。
② 将地址逻辑放在前面->运行实例小程序时获取地址信息
效果:将串行逻辑改成并行之后,整体从业务启动到渲染内容出现时长可以在100%左右。
③ 全局渲染优化
主要原因:
①跳转协议、地址信息、主界面数据均以状态值形式存在,粒度较细,造成大区域多次全屏重绘。
② 页面渲染时,层数较多,细粒度状态触发频繁,导致最终整体 DOM 的渲染时间较晚,白屏等待时间较长。
分析及解决方法:
① 状态分类与合并,区分状态和变量
② 地板分批渲染及局部渲染
实现方法:
重新设计了页面数据监听方案,采用全局状态管理的方式,减少渲染和重绘的次数。
最终效果如下:
上图左侧为优化前的渲染方式,使用状态绑定组件并进行改变。
上图右侧是优化后分类合并后的数据源,通过改变值,一次性触发数据更新,并分批渲染。
新的方式将状态变成值,并且通过观察者模式赋予组件订阅变量更新的能力,虽然状态绑定使用起来更方便,但是缺少了变量值的管理,因此可以弥补这个缺失的能力。
使用建议:
1.使用状态的地方,必须是 JSX 中使用的值(状态变化触发组件刷新),不要在逻辑中使用的变量中使用状态值;
2、可按照独立的业务模块设置粒度,减少频繁的无效设置值,并在对象变更时提前进行数据比对;
3.当组件只需要改变状态,不需要依赖状态时:推荐写法:=();

【3.5 整体启动流程对比示意图】
4. 互动体验
本节首先对整体结果进行概述,然后对页面启动和交互结果进行详细说明。
【4.1 门体整体推拉优化】
以下是美团外卖、京东到家APP门店详情、门店详情小程序滑动效果对比分析:
通过分析,我们发现交互体验存在以下三个关键问题:
① 手势体验问题;
② 商品列表空白,类目切换不流畅,二级列表偶尔闪烁;
③ 内存占用高;
4.1.1 支持全屏手势
前期我们对手势支持进行了研究:
根据调研结果综合评估:我们采用了View嵌套的方案,在嵌套的场景下需要进行手势交互以及后续的处理,因此面临一些挑战:
①现有代码大量使用事件拦截,手势重建成本较高;
②页面存在多层顶吸、底吸动画交互效果,需要合理的替代方案;
原来支持自动吸顶,我们通过动态设置外层动画方案替代了原来的动画方案
③ 多层视图嵌套导致多个手势冲突;
小程序逻辑层实现了事件系统,包括事件触发、事件冒泡等,小程序模板中绑定的事件都是bind形式。
总体来说,这个在逻辑层实现的小程序事件系统是可以正常工作的,事件回调可以正确触发、冒泡、停止冒泡。除了可以阻止回调函数冒泡之外,在小程序原生模板中绑定的事件还可以阻止视图滚动,而Taro的事件系统做不到这一点。
Taro 给我们提供的解决方案是给 View 组件添加属性:
// 这个 View 组件会绑定 catchtouchmove 事件而不是 bindtouchmove
基于上面的情况,我们通过动态控制来判断是否将事件向上传播到外层。
控制内层是否可以根据用户的上推和下滑手势进行滚动。
在 Taro 组件中我们通过动态设置来控制是否向上冒泡,但是我们发现在某些场景下存在一个问题,当切换已经渲染好的 DOM 树时,DOM 元素在视图中是不可见的。与 Taro 团队沟通后发现,Taro 在设置时会使用不同的静态模板。为了解决这个问题,我们选择将其绑定到原生的小程序组件上,混合原生和 Taro 来解决这个问题。
封装小程序原生组件:<view id="container" catchtouchmove="{{catchMove}}"> <slot>slot>view>
原生组件嵌套Taro组件: 嵌套Taro组件
经过我们对各个模型的不断优化和测试,优化前后的视频对比如下:
4.1.2 优化列表流畅度
中低端机型测试:在商品详细列表中浏览xxx商品后,页面卡住;
因此针对以上问题,我们对列表的流畅度进行了优化:
优化了查找暴露元素的方法
实践中发现,通过曝光回调来获取元素的方式严重影响了页面滑动的流畅度,以下是两种写法:
const categroyNodeObserver = createIntersectionObserver(Taro.getCurrentInstance().page, { observeAll: true })categroyNodeObserver.observe('#app >>> .className', result => { // 1、通过class获取列表再遍历查找对应元素 const vnodes = document.getElementsByClassName('className') as unknown as HTMLCollectionOf[]; 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的线,然后用它来监听元素上推消失,下拉出现的时机,并联动相应的1,2,3级类目。
② 列表分页加载方式选择
底部加载时间测试比较:
以显示器的曝光度作为下一页的请求回调时机,如下图:
③ 列表分页渲染层面优化
现状:使用递归方式+渲染列表。这样有效解决了每次渲染所有数据耗时的问题。同时也引入了一些弊端,最主要的问题是当多分类链接名片页面较多时,经过多层嵌套,头部排序栏的元素会被挤到可见区域之外。
为了实现 Taro 可以增量地将数据传递给小程序,我们回归底层 diff 原理,考虑到 diff 可以通过 key 值进行优化,所以我们给 Row 的每一行元素都添加了 键值。这样一来,Taro 就可以自动 diff 并更新单页数据,使得 DOM 结构由原来的嵌套结构变成了一层一层。这样就解决了上述问题,同时也提升了渲染性能。
加载10页,每页大约需要:
4.1.4 List内存使用率过高
利用性能测试工具,我们对比了内存快照,发现主要有以下两个原因:
①Taro端:使用内存快照对比工具后,我们发现Taro端存在两个问题,一是Taro在清除链表前后生成的树(th对象)中一直保存着缓存数据,二是这些数据并不是每次都释放,导致内存不断增长。
② 小程序引擎:自定义组件更新时,通过前后diff计算需要更新的节点,对标有jd:if、jd:for的节点不进行识别和清理。
经过引擎和Taro多次沟通和联调,以上两个问题最终得到解决,Taro端问题通过升级到3.5.1版本解决,引擎端问题通过商城App 12.2.4版本修复。
【4.2 优化门详情二级页/购物车卡片/门详情弹窗】
随着业务的增加,二级页面的需求也在不断迭代,需要支撑业务展示更多的商品、更多的功能,比如加购、组合购等。因此我们也对各类二级页面及弹窗卡片的性能体验进行了优化,主要解决以下几类问题:
① 修复列表页整体列表抖动问题;
② 优化二级页面渲染时序,改变组件插入方式;
③ 对购物车卡片上大量数据的加载进行扁平化,并支持分页加载功能;
④ 弹窗功能整体统一;
关于二级页面的详细优化我们还会继续发文章。
5. 能源消耗优化
本节首先对整体结果进行概述,然后对页面启动和交互结果进行详细说明。
本节首先对整体结果进行概述,然后对页面启动和交互结果进行详细说明。
实际测试:在翻阅了大约50页详细的产品清单之后,手机明显发烫,并且耗电量较大,所以能耗高也是我们需要解决的问题。
具体分析步骤如下:
① 手机静止不动,发热;
② 列表不断滚动,手机发热;
【5.1 静态消耗】
① 布局与渲染:DOM内容不断变化。例如倒计时、轮播等。
② 媒体和动画:指有 CSS 一直在执行动画。例如呼吸动画、
③:如倒计时活动。
基于以上,我们分别对站点上的倒计时和页面上的轮播进行了隐藏和停止;动画效果的CSS样式用gif图片代替;最终效果如下:
能耗结果:
静态:能耗由高到低下降。
滑动情况下:我们实现了滚动防抖和图片尺寸优化,滑动瞬间能耗降低20%。
70页之后开始发热,DOM节点数、节点深度还有优化空间,后续我们继续分享。
我们总结了一些能耗优化的方向,并放到了我们的开发规范中:
6.埋点管理
本节首先对整体结果进行概述,然后对页面启动和交互结果进行详细说明。
9月份,我们上线了全站埋点解决方案的最后一个版本,包括底层埋点(PV EP)埋点参数的逻辑优化,以及所有业务埋点上报机制和时序优化。在整改之前,埋点存在一些问题:
①曝光点报告时序错误,无法使用;
②PV跟踪、点击跟踪缺乏标准化,添加跟踪的业务不规范,导致成本过高;
③曝光请求未聚合,导致跟踪请求频繁;
整改后输出了整体门点追踪开发规范;重新梳理了各暴露门点的上报时序及总体方案;目前门点追踪可用率已达95%以上;为业务数据提供了更精准的方向。
七、总结与展望
本节首先对整体结果进行概述,然后对页面启动和交互结果进行详细说明。
以上是我们近期优化的中期成果,不仅得到了业务、产品同事的认可,也使线上用户反馈门户加载慢、卡顿的比例降低了约15%。
提升用户体验需要长期的坚持,技术同事需要有一定的工匠精神,不断探索,我们也在规划:
①持续探索和优化能源消耗;
支持虚拟列表:目前门店列表支持多类目联动加载,且同类目下组件复用、回收;下一步我们将继续实现跨类目的组件复用、回收,形成门店详情列表回收渲染机制统一的解决方案。
嵌套层数过深导致的性能消耗:进一步减少详细组件嵌套问题,由内而外优化整体组件嵌套层数,减少手机发热问题。
② 优化同步微信域名的解决方案;
③支持共建能力;
提供门细节组件共建能力,将门细节组件与业务解耦。
Mini计划转换为H5;
⑤改善近乎本地的经验:对视频功能的支持;
⑥支持微型编程侧手势;
我们希望我们可以为用户提供更极端的体验,同时有效地支持业务迭代!