小程序体验优化实践

背景

小程序作为我司的战略级业务小程序,致力于为千万商户提供强大的微商城系统和完整的移动电商解决方案!随着越来越多的商户接入,以及长时间的业务开发之后,我们的关注点逐渐从功能堆叠转移到用户的使用体验上。用户体验直接影响到我们小程序的留存率、转化率等,因此我们有迫切的性能体验优化需求。

小程序体验评分

小程序体验评分是微信开发者工具内置的一项功能,会在小程序运行过程中实时检查,分析出一些可能导致体验不好的地方,并且定位出哪里有问题,以及给出一些优化建议。

本文将结合小程序的评分功能,以智慧零售小程序首页作为优化对象进行一次体验优化实践,目标就是拿到一个100分的小程序。

零售小程序首页现状

零售小程序的首页是通过店铺装修(可视化搭建)构建出来的,整个页面的结构,形态,页面元素,组件等等完全由商户自主搭建。千店千面的首页在我们运行小程序体验评分时,不同的页面评分结果都不尽相同,但是通过我们对多个页面的评分结果收集,目前的主要问题有:

  1. 存在图片太大而有效显示区域较小
  2. 存在未绑定在wxml上的变量
  3. 发起太多的图片请求
  4. 使用了过大的 WXML 节点数目
  5. setData太过频繁

首页评分现状

首页评分现状

优化方案

1. 存在图片太大而有效显示区域较小


问题:加载的图片大小与DOM显示的图片大小不一致(比如:我们图片显示的宽高是50 x 50,而加载的图片宽高则为350 x 350),过大的图片会增加下载时间和内存消耗。

优化方案:以 实际显示宽高乘积 * (设备像素比 ^ 2) 大小加载。CDN服务商基本上都支持通过参数获取不同大小、格式的图片(如:腾讯云七牛云),我们可以通过封装一个公共方法或组件根据页面元素的实际展示大小裁剪图片。如:

  1. Component({
  2. ready() {
  3. this.createSelectorQuery()
  4. .select(".J-image")
  5. .boundingClientRect(res => {
  6. this.setData({
  7. src: `${this.data.src}?imageMogr2/thumbnail/${res.width}x`
  8. })
  9. }).exec();
  10. }
  11. })

当然上面的组件也存在一定的问题,就是 boundingClientRect 获取元素的尺寸会触发重排,同样对页面的性能有较大的影响,因此建议手动传入裁剪尺寸。

  1. Component({
  2. properties : {
  3. thumbWidth: {
  4. type: Number,
  5. value: 0,
  6. },
  7. thumbHeight: {
  8. type: Number,
  9. value: 0,
  10. },
  11. },
  12. ready() {
  13. if (this.data.thumbWidth || this.data.thumbHeight ) {
  14. this.setData({
  15. src: `${this.data.src}?imageMogr2/thumbnail/$this.data.thumbWidth}x{this.data.thumbHeight}`
  16. })
  17. } else {
  18. this.createSelectorQuery()
  19. .select(".J-image")
  20. .boundingClientRect(res => {
  21. this.setData({
  22. src: `${this.data.src}?imageMogr2/thumbnail/${res.width}x`
  23. })
  24. }).exec();
  25. }
  26. }
  27. })

除了关心图片的尺寸之外,我们更应该需要关注的是图片格式,不同的格式对图片的显示、压缩以及图片体积有着极大的差别,这里放一张图给大家做一个简单对比。

从图上简单对比来说,WebP 对于其他图片格式有着无可比拟的优势,因此建议默认都加载WebP格式图片。对于移动端来说,Android原生支持WebP根本不需要考虑兼容问题,而 iOS 平台上小程序在基础库 2.9.0 也内置了WebP支持,而对不支持webp的情况可以做相应的降级处理,可以根据下图选择对应的图片格式:

图片格式的转换我们也可以通过CDN服务商提供的服务直接转换:

  1. // 以腾讯云为例,直接在URL后拼接一个 /format/[对应格式]
  2. const src = `${this.data.src}?imageMogr2/thumbnail/$this.data.thumbWidth}x{this.data.thumbHeight}/format/webp`;

2. 存在未绑定在wxml上的变量


问题: this.data 上存在冗余字段,即不与UI渲染关联的字段。由于小程序是逻辑和渲染分离的双线程设计,两个线程的通信会经由Native做中转通信。因此我们需要尽可能的减少 setData 的次数的同时,减少两个线程之间通信的数据量(data上的字段尽可能少、小)。

优化方案: 这里优化方式主要有两种:

  1. 将字段挂载在组件实例上,如: this.xxx = “test”;
  2. 使用小程序的纯数据字段;
  1. Component({
  2. options: {
  3. pureDataPattern: /^_/, // 指定所有 _ 开头的数据字段为纯数据字段
  4. },
  5. data: {
  6. a: true, // 普通数据字段
  7. _b: true, // 纯数据字段
  8. },
  9. // 挂载在实例上的字段
  10. staticData: {},
  11. methods: {
  12. myMethod() {
  13. this.data._b; // 纯数据字段可以在 this.data 中获取
  14. this.setData({
  15. c: true, // 普通数据字段
  16. _d: true, // 纯数据字段
  17. });
  18. },
  19. },
  20. });

组件中的纯数据字段不会被应用到 WXML 上:

  1. <view wx:if="{{a}}"> 这行会被展示 </view>
  2. <view wx:if="{{_b}}"> 这行不会被展示 </view>

3. 发起太多的图片请求


问题: 同域名耗时超过 100ms 的图片请求并发数超过 6 个。

优化方案:

  1. 对于Icon类的图片,建议能使用 iconfront 代替的话就使用iconfont,如果iconfont不能满足要求的话,最好使用雪碧图
  2. 其他则建议使用小程序 Image 组件支持通过配置 lazy-load 参数来实现懒加载。不过这里有一个坑就是:在轮播组件或者横向滚动的组件中,小程序图片组件的懒加载并不能处理这种情况,所以针对这种特定组件如果需要做懒加载的话需要我们去自行适配。

4. 使用了过大的 WXML 节点数目

问题:页面的WXML节点超过1000个,节点深度超过30层,子节点数超过60个。控制页面的节点数,节点深度的意义在于过多的节点数,过深的节点层数会增加内存消耗的同时,页面重排的时间也会加长,影响用户体验。

优化方案: 对于一个通过 LowCode 搭建的页面来说,1000个WXML节点数简直不要太容易达到。像商品类的组件,我们允许商家在单个组件可配置的商品数达 500 个,商家多配制几个商品类型的组件我们的页面节点就直接奔4、5千去了。

目前通过下面几个优化尽可能减少页面节点数量:

  1. 分页加载。对整个页面组件数据进行分页,组件内的数据也进行分页加载。
  2. 在组件层面进行数据懒加载,初始化加载时先加载骨架屏,组件进入屏幕下一屏时加载数据。
  3. 对于长列表组件,如:商品组件,当商品元素滑出可视区域外时,隐藏商品元素,或者使用虚拟滚动。
  1. // 组件延迟数据
  2. Component({
  3. ready() {
  4. this.intersectionObserver = this.createIntersectionObserver();
  5. this.intersectionObserver
  6. .relativeToViewport({ bottom: 300 })
  7. .observe('.J-design-module', res => {
  8. if (res.intersectionRatio > 0) {
  9. // 加载组件数据
  10. this.featchData();
  11. }
  12. })
  13. }
  14. })

5. setData 调用太过频繁

问题: 根据微信的评分标准,我们在一秒时间内调用 setData 超过20次。

优化方案: 我们在对代码进行分析时发现,很多组件中都通过 setData 来处理组件的样式,如:

  1. Component({
  2. ready() {
  3. this.setData({
  4. 'componentStyle.containerStyle': [
  5. styleFormat.backgroundColorStyle(
  6. moduleConfig.style.backgroundColor.rgb,
  7. ),
  8. ].join(';'),
  9. 'componentStyle.labelStyle': [
  10. styleFormat.heightStyle(moduleConfig.style.height),
  11. styleFormat.lineHeightStyle(moduleConfig.style.height),
  12. styleFormat.fontStyle(
  13. moduleConfig.style.labelFontSize,
  14. moduleConfig.style.labelColor.rgb,
  15. moduleConfig.style.labelAlign,
  16. ),
  17. ].join(';'),
  18. 'componentStyle.descStyle': [
  19. styleFormat.fontStyle(
  20. moduleConfig.style.descFontSize,
  21. moduleConfig.style.descColor.rgb,
  22. ),
  23. ].join(';'),
  24. 'componentStyle.moreStyle': [
  25. styleFormat.heightStyle(moduleConfig.style.height),
  26. styleFormat.lineHeightStyle(moduleConfig.style.height),
  27. ].join(';'),
  28. });
  29. }
  30. })

在wxml中使用

  1. <view class="title-container" style="{{componentStyle.containerStyle}}">
  2. <view class="title-label">
  3. <view class="more" style="{{componentStyle.moreStyle}}" >
  4. <view class="link-style link-style-1" >
  5. 查看更多
  6. </view>
  7. </view>
  8. <view class="title" style="{{componentStyle.labelStyle}}">{{moduleConfig.style.label}}</view>
  9. </view>
  10. <view class="title-desc" style="{{componentStyle.descStyle}}">{{moduleConfig.style.desc}}</view>
  11. </view>

类似这种样式上的处理,我们建议通过 WXS,在WXS中处理提高性能的同时在 iOS 设备上还能获得更好的运行效率。

  1. / / style-format.wxs
  2. function bgColor(color) {
  3. var _color = formatColor(color);
  4. return _color ? 'background-color:'.concat(_color) : '';
  5. }
  6. function fontColor(color) {
  7. var _color = formatColor(color);
  8. return _color ? 'color:'.concat(_color) : '';
  9. }
  10. module.exports = {
  11. bgColor: bgColor,
  12. fontColor: fontColor,
  13. };
  1. <wxs src="./style-format.wxs" module="SF" />
  2. <view
  3. class="title-container"
  4. style="{{SF.bgColor(styleConfig.backgroundColor.rgb)}}"
  5. >
  6. <text style="{{SF.fontColor(styleConfig.descColor.rgb)}}">标题</text>
  7. </view>

骨架屏方案

在上面的优化方案中,我们多次提到了【骨架屏】,所谓的骨架屏就是页面/组件的一个空白版本,通常会在页面/组件完全渲染之前,通过一些灰色的区块大致勾勒出轮廓,待数据加载完成后,再替换成真实的内容。那么小程序的骨架屏应该怎么做呢?

一、 引入静态文件

引入静态文件应该是最容易想到的骨架屏解决方案。静态文件形式的骨架屏,一般是根据组件/页面的未加载形式或UI给的未加载样式手动编写。

如果骨架屏页面较多,需要使用的场景多,可以考虑通用的骨架屏内容抽象成一个普通的组件提高复用率。

当然微信开发者工具也提供了生成骨架屏的功能:

静态文件形式的骨架屏会形成冗余代码并且增加小程序包的体积,同时,当设计的界面发生变化时,对于引入静态文件的方式也是十分不友好的,设计界面的改变导致相应的骨架屏实现也要随之修改!

二、动态生成

动态生成骨架屏是指当用户进入界面的时候,我们使用微信提供的一些API的去获取当前页面一些指定区块的信息(如坐标、尺寸等),然后在相应的坐标处绘制与指定区块相同大小的灰色区域来实现骨架屏效果。这里推荐我司一个同学开发的组件:miniprogram-skeleton

动态生成骨架屏有一个比较明显的缺点是,需要页面上元素定宽定高,否则就无法生成对应的骨架屏。

三、组件内置

组件内置是指将骨架屏集成在各个组件中,由组件自己管理。当页面处于加载状态时,给需要显示骨架屏的组件传入特定值来标识当前组件需要展示骨架屏状态,然后在组件内部添加相应的class,从而通过样式文件再给特定的class添加骨架屏效果。

举个简单例子:

  1. Component({
  2. data: {
  3. loading: true,
  4. },
  5. someTimes() {
  6. this.setData({
  7. loading: false,
  8. })
  9. }
  10. })

wxml中根据 loading 添加样式

  1. <view class="{{ loading ? 'loading' : null}}">
  2. <text>骨架屏灰块</text>
  3. </view>

Less 文件

  1. .loading {
  2. text {
  3. background-color: #f2f2f2;
  4. color: #f2f2f2;
  5. }
  6. }

这种方案对组件开发来说会增加一些工作量,与此同时组件还需要根据特定标识来判断组件是否需要展示骨架屏,对组件也产生了一定的入侵性。

这个方案也是我们目前首页采用的方案,可视化搭建的首页决定了它页面的不确定性以及变化性,组件内置骨架屏更加适合当前场景。

优化结果

经过我们上面的一些优化,我们拿到了一个💯分小程序。

小程序性能评分可以从指标和实际数据上给我们的项目优化提供一些建议,本文涉及到的点并不全面希望能给大家一些参考。

写于 2021年08月22日小程序 3185

如非特别注明,文章皆为原创。

转载请注明出处: https://www.liayal.com/article/61223de61f09d31234eabf6e

记小栈小程序上线啦~搜索【记小栈】【点击扫码】体验

你不想说点啥么?
😀😃😄😁😆😅😂🤣☺️😊😇🙂🙃😉😌😍😘😗😙😚😋😜😝😛🤑🤗🤓😎🤡🤠😏😒😞😔😟😕🙁☹️😣😖😫😩😤😠😡😶😐😑😯😦😧😮😲😵😳😱😨😰😢😥🤤😭😓😪😴🙄🤔🤥😬🤐🤢🤧😷🤒🤕😈👿👹👺💩👻💀☠️👽👾🤖🎃😺😸😹😻😼😽🙀😿😾👐👐🏻👐🏼👐🏽👐🏾👐🏿🙌🙌🏻🙌🏼🙌🏽🙌🏾🙌🏿👏👏🏻👏🏼👏🏽👏🏾👏🏿🙏🙏🏻🙏🏼🙏🏽🙏🏾🙏🏿🤝👍👍🏻👍🏼👍🏽👍🏾👍🏿👎👎🏻👎🏼👎🏽👎🏾👎🏿👊👊🏻👊🏼👊🏽👊🏾👊🏿✊🏻✊🏼✊🏽✊🏾✊🏿

评论

someone07-18 2023
@dhlolo: 敢问贵司是?

Tencent😊

dhlolo12-06 2021

敢问贵司是?