小程序性能优化实践总结

项目简述 & 问题

先简单介绍一下项目,就是一个比较常规的点餐小程序。
界面如图:


1.png

左边是分类菜单,右边是长列表,有多个分类的商品,单个分类滚动完后可以继续滚动切换到下一个分类,同时左边的分类菜单选中态会跟着切换到当前商品列表显示的分类。

考虑到更好的用户体验,以及参考了美团等点餐小程序,这个商品列表的数据是一次性返回的。目前遇到的问题就是,当商品数量比较多时,首次渲染时间很长,而且页面会卡顿。

优化方向

逻辑优化

小声bb:其实就是原来代码(由于历史原因)写得太烂了……OTL

先放个图👇


2.png

小声bb:连小程序都看不下去了,要警告了o(╯□╰)o

微信开发者工具都有警告了,而且提示里面也有定位到具体代码的位置,所以关键就是这个 setData !!!

我们可以先看看官方对于小程序性能以及 setData 优化的一些建议。(https://developers.weixin.qq.com/miniprogram/dev/framework/performance/tips.html

具体实践:

1. setData 不能一次性传太多数据,如果列表太长,可以分开渲染【比如转化为二维数组,每次循环渲染一个数组】。

v1:简单粗暴版

// 每次渲染一个分类
// 假设goodsList是一个二维数组
goodsList.forEach((item, index) => {
    this.setData({
        [`goodsList[${index}]`]: item
    })
})

像上面这样写会有一个问题,页面首屏渲染是快了,但是点击页面操作(比如加购按钮等),页面会卡住,等一下才有反应,操作反馈延迟严重。
其实这是因为,这个循环是把单次 setData 数量减少了,但是却变成了循环多次 setData,我们看着首屏显示好了,但是其实其他分类(其他数组)还在渲染,线程还是忙碌状态,JS 线程一直在编译执行渲染,点击事件不能及时传递到逻辑层,逻辑层亦无法及时将操作处理结果及时传递到视图层。

v2:定时器hack版

既然js线程忙着渲染,那我们可以强制让它先停下来。于是有了v2的定时器hack版。

// 每次渲染一个分类
let len = data.goodsList ? data.goodsList.length : 0;
let idx = 0
let timer = setInterval(() => {
    if (idx < len) {
        that.setData({
            [`goodsList[${idx}]`]: data.goodsList[idx]
        });
        idx++
    } else {
        clearInterval(timer)
    }
}, 15);

现在首屏渲染速度问题解决了,点击按钮延迟响应问题也解决了。就是代码有点hack,逼死强迫症o(╯□╰)o

v3:大杀器——虚拟列表

虚拟列表简单说原理就是只渲染当前显示屏幕区域以及前n屏和后n屏的数据,用一个单独的字段保存当前需要显示的数组(就是当前一屏+前n屏+后n屏),每次列表滚动的时候重新计算需要显示的数据,更新这个字段,页面就会相应更新了。这样就能保证页面上的元素节点数量不会太多,就可以支持大量数据的长列表需求。
更详细的原理和实现各位同学们可以自己搜一下,此处不展开。

小程序官方也有开源的虚拟列表组件:recycle-view

2. setData 可以支持颗粒更新,指定到具体的属性。

比如加购等操作,需要更新商品右上角的小数字,可以这样写:

this.setData({
    [`goodsList[${categoryIndex}][${goodsIndex}].num`]: goodsItem.num
})

3. 跟页面无关的数据不要保存在 data,不要用 setData 更新,因为 setData 会触发页面渲染。

eg:

Page({
    data: {
        ...
    },
    // 跟页面渲染无关的数据
    state: {
        hasLogin: false,
    },
    ...
})

// 更新的时候直接赋值就行
this.state.hasLogin = true

PS:或者甚至不需要挂载到 page 对象下,直接用普通变量保存。

4. 图片大小优化

在长列表中图片大小如果不加限制,大量的大图会占用很多内存,有可能导致iOS客户端内存占用上升,从而触发系统回收小程序页面。除了内存问题外,大图片也会造成页面切换的卡顿。

解决办法就是根据当前显示的图片区域大小,取尺寸刚好合适(2倍-3倍图)的图片。

建议图片用CDN,一般CDN服务厂商提供图片服务的都会提供裁剪图片的接口,然后接口只返回原图链接,前端根据需要传参数裁剪图片。前端具体做法可以写公共的图片处理方法,或者自己封装图片组件。

附常用图片CDN服务商图片裁剪API文档:

5. 减少不必要的数据请求

比如在该点餐页面进入时需要获取定位,然后根据定位获取最近的门店,前面两个接口都需要请求(具体可以根据业务需求),而最后如果获取到的距离最近的门店跟上次一样,则不需要重新获取店铺详情和商品数据。

体验优化

1. 合并短时间内的多个loading提示

还是该点餐页面流程,像上文说过的,进入页面时需要获取定位接口,等定位接口返回结果了再拿定位取值去获取距离最近的店铺,最后才是请求店铺和商品数据。
这三个接口是串行的。此时如果我们每个接口都弹出一个loading提示,就会出现loading显示一会儿,消失,又显示一会儿,又消失……这样的现象,这样的体验是不太好的。
建议可以通过封装请求,并且在请求里统一处理loading,来合并短时间内多次发起请求的多个loading。

eg:

let showLoadingTimer = null;
let showRequestLoading = false; // 标记是否正在显示loading

/**
 * 封装request
 * @param {*} {showLoading:是否需要显示loading, options:request参数,如url,data等} 
 */
function request({showLoading = true, ...options}) {
    // 显示request loading
    handleShowLoading(showLoading)

    wx.request({
        ...
        complete() {
            // 关闭request loading
            handleShowLoading(false)
        }
    })
}

/**
 * 封装request loading
 * 短时间内如果调用多次showLoading,会合并在一起显示,而不是每个都闪现一下
 * @param showLoading
 */
function handleShowLoading(showLoading) {
    if (showLoading) {
        // 显示loading
        clearTimeout(showLoadingTimer);
        if (!showRequestLoading) {
            showRequestLoading = true;
            wx.showNavigationBarLoading();
            wx.showLoading({ title: "加载中", mask: true })
        }
    } else {
        // 200ms后关闭loading
        showLoadingTimer = setTimeout(() => {
            showRequestLoading = false;
            wx.hideNavigationBarLoading();
            wx.hideLoading()
        }, 200)
    }
}

2. 整个页面初次加载时可以使用页面loading动画或骨架屏,优化加载中体验。

3. 静默获取、更新数据

比如这个点餐页每次 onShow 都会调用定位接口和获取最近门店接口,但是不显示loading,用户就没有感知,体验比较好。

接口优化

需要关注接口的粒度控制。
因为有时候合并接口,前端可以减少一次请求,体验更好;但有时候如果接口的数据太多,响应太慢,就可以考虑是否某部分数据可以后置获取,让主要的页面内容先渲染出来,根据这个设计来拆分接口。

比如项目中的点餐页面,原来购物车数据和商品规格弹窗显示的详情数据都是在获取店铺商品接口一次性返回的,而这个接口本来由于设计需要一次返回所有商品,就会造成数据量太大,而且后端需要查询的表也更多。于是把获取购物车,和商品详情接口都拆分为单独的接口,获取店铺商品接口的响应时间就减少了,页面也能更快显示出来。

总结

其实上面提到的逻辑优化和接口优化很多都是细节,并不是太高深的技术,我们平时迭代的时候就可以注意。而体验方面的优化则需要前端同学在前端技术以外更多关注用户体验和设计方面的知识啦,而且这也是一个有追求的前端应该具备的技能……←_←
所以嘛……技术路漫漫,大家共勉吧

参考文章

https://developers.weixin.qq.com/miniprogram/dev/framework/performance/tips.html

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,377评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,390评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,967评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,344评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,441评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,492评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,497评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,274评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,732评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,008评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,184评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,837评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,520评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,156评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,407评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,056评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,074评论 2 352