注册
web

你还别不信,我帮同事优化代码,速度快了1000倍以上!!

背景


我们公司有个小程序的产品,里面有个功能是根据用户输入的商品信息,如 干葱3斤,沙姜1斤,番茄2斤...,然后传给后端接口就能解析到符合规格的数据,这样用户就不用一个个录入,只需要一次性输入大量 sku 信息文本,即可快速下单。


故事发生在这周三早上,我同事急匆匆地找到我,说识别商品很慢。


一开始,我以为是后端的接口慢(因为之前这个识别一直在做优化),那这个实际上前端大多无能为力,因为确实识别了大量的商品(具体是 124 个),且输入文本千奇百怪,比如豆腐一块,那我要理解为是一块豆腐,还是豆腐一块钱?但他跟我说,虽然接口耗时 2.8s,但是还得等待接近 5s 的时间才渲染商品列表,且经常出现创建完 124 个商品的订单,开发工具就报内存不足。


image.png



这个是网上找工具模拟的,因为企业微信截图水印去除太麻烦了。。。不过对话是真实的对话~



我一开始还以为,难道是渲染长列表没做性能优化?然而经过一顿排查,排除了是渲染的锅,罪魁祸首是请求完接口后,对商品信息的处理导致了卡顿,过程大致如下:


  /** 发起请求 */
async request() {
/** 这里接口耗时2.8s */
const data = await ParseDetails()
onst { order_detail_items, sku_map, price_map } = data;

/** 耗时出现在这里 长达5s+,随着识别商品数量呈线性增加 */
this.list = order_detail_items.map(
(item, i) => new DataController({ ...item, sku_map, price_map })
);
}

2023-03-01 21.34.05.gif



每次生成一个 DataController 实例大概耗时 30+ ~ 50ms



定位到耗时的大致位置,那就好办了,接下来,实际上就只需要看看为何创建 DataController 那么耗时就好了。


这里我也写了个类似的demo,点击可见具体的代码细节~




本来想通过码上掘金写 demo 的,但发现好像不太支持,所以还是在 codesandbox 上写,然后在码上掘金通过 iframe 插入,如果预览不出来,可能是 codesandbox 抽风了



image.png



尾缀为 1 的为优化后的代码



了解下 demo


代码结构


整个项目采用的技术栈是 react + mobx(一个响应式的数据管理库)



  • batch_input: 点击识别后会调用 batchInput 的 request 进行解析,解析完成后会处理商品列表信息
  • data_controller: 商品信息二次处理后的结构,request 后得到的 order_detail_items 会遍历生成一个个 DataController 实例,用于渲染商品列表
  • mock_data: 随便造了一点数据,124 项,屏蔽了真实项目的具体字段,结构为 { order_detail_items, sku_map, price_map }

其中 batch_input1、data_controller1 是优化后的代码


为何要有两个 map


每次请求接口解析后会返回一个数据结构:



  • order_detail_items: 返回列表的每一项,结果为 OrderDetailItem[]
  • sku_map: sku 即商品的结构,可通过 order_detail_item 上的 sku_id 映射 到对应的 sku,结构为 Record<string, Sku>,即 Sku_Map
  • price_map: 对应的报价信息,可通过 order_detail_item 上的 sku_id 映射 到对应的 price,结构为 Record<string, Price>,即 Price_Map

以上相关信息放到对应的 map 上是为了让一个 order_detail_item 不挂太多的数据,只通过对应的 id 去 map 对应的其他信息,比如我想拿到对应的 sku 信息,那么只需要:


const sku = sku_map[order_detail_item.sku_id]

而不是通过:


const sku = order_detail_item.sku

拿到,以达到更好的扩展性。


一起看看问题出在哪


现在我们定位到了问题大致是出现在创建 DataController 实例的时候,那么我们看具体的构造函数:


image.png


image.png


我们看到每次遍历都把 order_detail_item 和两个 map 都传给 DataController 类,然后 DataController 将得到的 detail 全部赋值到 this 上,之后通过makeAutoObservable实现响应式。


看到这里的读者,我想大部分都知道问题出现在哪了,就是原封不动地把所有传过来的参数都加到 this 上去,那么每次创建一个实例,都会挂载两个大对象的 map,导致 new 每个实例耗时 30 ~ 50ms,如果是 100+个,那就是 3 ~ 5s 了,这是多么的恐怖。


还有一个点,实际上 DataController 声明的入参类型是OrderDetailItem,是不包括 Sku_Map 和 Price_Map,但是上面的代码却都不顾 ts 报错传过去,这也是导致代码可能出现问题的原因


image.png


多说一嘴


然而实际上定位问题没有那么快,因为首先实际的 DataController 很大,且 constructor 里面的代码也有点多,还有我后来也没有负责这个项目,对代码不是特别的熟悉。


而上面的 demo 实际上是经过极简处理过的,实际的代码如下:


image.png



将近 250 行



image.png



单单一个 constructor 就 50+行了



一起看看如何优化吧


我们现在找到原因了,没必要每个示例都挂载那么多数据,特别是两个大对象 map,那我们如何优化呢?


大家可以想一想怎么做?


我的方案是,DataController 上面声明个静态属性 maps,用来映射每次请求后得到的 sku_map 和 price_map,见data_controller1


image.png


然后每次请求之前生成一个 parseId,用来映射每次请求返回的数据,demo 里面是用Date.now()简单模拟,将生成的两个 map 存放到静态属性 maps 上,然后 parseId 作为第二个参数传给每个实例,见 batch_input1


image.png


那么 每个实例的get sku, get mapPrice(真实项目中实际上很多,这里简化了不少) 中就可以替换为该静态 map 了,通过 parseId 映射到对应的 sku 和 price


我们看看优化后的效果:


2023-03-01 21.36.58.gif


现在生成 list 大概花费了 4 ~ 6ms 左右,比起之前动辄需要 5 ~ 6s,足足快了 1000 多倍!!!


c5826fd4a758463390413a173ee0899d.gif


先别急


等等,我们上次说了是因为把太多数据都放到实例上,特别是两个大 map,才导致生成实例太过于耗时,那真的是这样吗?
大家可以看看 demo 的第三个 tab,相比第一个 tab 只是注释了这行代码:


image.png


让我们看看结果咋样


2023-03-01 21.37.22.gif


可以看到生成 list 只是耗费了 1+ms,比优化后的代码还少了 3+ms 左右,那么,真正的根源是就肯定是makeAutoObservable这个函数了


makeAutoObservable 做了什么


我们上面说到,mobx 是个响应式的数据管理库,其将数据都转换为 Observable,无论数据多么深层,这个我们可以 log 下实例看看


image.png


会发现 map 上每个属性都变成一个个的 proxy(因为这里我们用了 mobx6),那如果我两个 map 都很大且很深层的话,遍历处理每个属性加起来后就很耗费时间,导致每次生成一个实例都耗费了将近 50ms!!


所以,我们上面所说的在this 上挂载了太多的数据只是直接原因,但根本原因在于 makeAutoObservable,当然,正是这两者的结合,才导致了代码如此的耗时。


总结


我们一开始以为是渲染太多数据导致页面卡顿,然而实际上是生成每个 DataController 实例导致的耗时。


我们开始分析原因,发现是因为每个实例挂了太多的数据,所以优化方案是将两个大对象 map 放到类的静态属性 maps 上,通过 parseId 来映射到对应的数据,从而将速度优化了 1000+倍。


然后我们继续深入,发现实例挂载太多数据只是表面的原因,根本原因还是在于 mobx 的 makeAutoObservable 对数据的每个属性都转换为 proxy 结构,让其变成响应式,而对象数据又很大,导致太过于耗时。


还有一点要注意的就是,原先的代码忽略了 ts 的类型限制,因为 sku_map、price_map 实际上不在入参的限制范围内(实际代码还不只多传了这两个 map),所以确保 ts 类型的正确性,也非常有利于规避潜在的 bug。


同时,如何写好每个 mobx store 也是我们应该深入思考的,多利用好 private、static,get 等等属性和方法,哪些应该放到实例上去,哪些应该放到静态属性上,哪些是 public、哪些是 static 的,都应该考虑好。


最后


当我优化代码后,就马上跟同事吹嘘:


image.png


看看,这是人说的话吗!!


但是,我突然想到:诶,这不是每次产品、测试、UI 说这里太慢、这里少 1px、这里交互有问题的时候,我不也是说:有啥问题?又不是不能跑吗?


image.png


但嘴上是这样说着,然而实际上私下却偷偷看为何会这样(不可能,绝对不可能,我的代码天下无敌),正所谓,嘴上说着不要,心里却很诚实。


QQ20230225-205345-HD.gif


好了,今天的故事就分享到这里,各位看官大大觉得可以的话,还请给个赞,谢谢~


作者:暴走老七
来源:juejin.cn/post/7204100122887536700

0 个评论

要回复文章请先登录注册