# 小程序优化指南

## 性能预估

1. 预估性能问题
2. 监控性能问题
3. 使用工具排查问题
4. 实施性能解决方案

我们可以用微信开发者工具自带的评分 ![开发者评分](https://tva1.sinaimg.cn/large/006tNbRwgy1g9ziv7pxgsj313c0u077h.jpg)

这是它的评分点： ![评分点](https://tva1.sinaimg.cn/large/006tNbRwgy1ga21bv81h2j30pr0pmmxn.jpg)

微信官方也给出了优化方案:

### 性能

1. 首屏时间 首屏时间是指用户从打开小程序看到第一屏主要内容的时间，首屏时间太长会导致用户长时间看到的都是白屏，影响使用体验。

   优化首屏时间，可以分为以下几种情况：

   首屏渲染的内容较多，需要集合多份数据进行渲染。这种情况需要开发者把内容分优先级，把优先级高的内容做优先展示，缩短白屏时间； 首屏内容依赖的数据从服务端请求的时间太长。开发者需要从服务端侧具体分析服务端数据返回的时间长的原因； 一次性渲染数据太大或依赖的计算过于复杂。减少渲染的数据量、优化渲染相关数据的算法可以解决这类问题。 得分条件：首屏时间不超过 5 秒
2. 渲染时间 渲染时间指的是首次渲染或因数据变化带来的页面结构变化的渲染花费的时间。

   渲染界面的耗时过长会让用户觉得卡顿，体验较差，出现这一情况时，需要校验下是否同时渲染的区域太大（例如列表过长），或渲染依赖的计算是否过于复杂。

   得分条件：渲染时间不超过 500ms
3. 脚本执行时间 脚本执行时间是指JS脚本在一次同步执行中消耗的时间，比如生命周期回调、事件处理函数的同步执行时间。

   执行脚本的耗时过长会让用户觉得卡顿，体验较差，出现这一情况时，需要确认并优化脚本的逻辑

   得分条件：一个执行周期内脚本运行时间不超过 1 秒
4. setData调用频率 setData接口的调用涉及逻辑层与渲染层间的线程通信，通信过于频繁可能导致处理队列阻塞，界面渲染不及时而导致卡顿，应避免无用的频繁调用。

   得分条件：每秒调用setData的次数不超过 20 次
5. setData数据大小 由于小程序运行逻辑线程与渲染线程之上，setData的调用会把数据从逻辑层传到渲染层，数据太大会增加通信时间。

   得分条件：setData的数据在JSON.stringify后不超过 256KB
6. 避免setData数据冗余 setData操作会引起框架处理一些渲染界面相关的工作，一个未绑定的变量意味着与界面渲染无关，传入setData会造成不必要的性能消耗。

得分条件：setData传入的所有数据都在模板渲染中有相关依赖

...

### 提高体验

1. 开启惯性 惯性滚动会使滚动比较顺畅，在安卓下默认有惯性滚动，而在 iOS 下需要额外设置-webkit-overflow-scrolling: touch的样式；

   得分条件：wxss中带有overflow: scroll的元素，在 iOS 下需要设置-webkit-overflow-scrolling: touch样式
2. 避免使用:active伪类来实现点击态 使用 css :active伪类来实现点击态，很容易触发，并且滚动或滑动时点击态不会消失，体验较差。建议使用小程序内置组件的 'hover-class' 属性来实现

   得分条件：不使用:active伪类，并使用hover-class替换:active
3. 保持图片大小比例 图片若没有按原图宽高比例显示，可能导致图片歪曲，不美观，甚至导致用户识别困难。可根据情况设置 image 组件的 mode 属性，以保持原图宽高比。

   得分条件：显示的高/宽与原图的高/宽不超过 15%
4. 可点击元素的响应区域

....

### 最佳体验

1. 避免JS异常 出现 JavaScript 异常可能导致程序的交互无法进行下去，我们应当追求零异常，保证程序的高鲁棒性和高可用性。

   得分条件：不出现任何JS异常
2. 避免网络请求异常 请求失败可能导致程序的交互无法进行下去，应当保证所有请求都能成功。

   得分条件：所有网络请求都正常返回
3. 不使用废弃接口 使用即将废弃或已废弃接口，可能导致小程序运行不正常。一般而言，接口不会立即去掉，但保险起见，建议不要使用，避免后续小程序突然运行异常。

   得分条件：不使用任何文档中提示废弃的接口
4. 使用HTTPS 使用HTTPS，可以让你的小程序更加安全，而HTTP是明文传输的，存在可能被篡改内容的风险

   得分条件：所有网络请求都使用HTTPS

....

我们打开一个小程序时： 下载小程序代码包、加载小程序代码包、初始化小程序首页

## 常见优化方法

1. 从资源包入手，图片的压缩。 我们根据设备的不同特性去选择最优的压缩率，例如 jpg、webp，375\*大图 小图,iconfont

* tinypng\[<https://tinypng.com/>]
* yasuotu\[<https://yasuotu.com/>]
* pngcrush\[<https://pngcrush.com/>]

上面放几个比较常用的链接： 2. 利用GPU 将 translate 等消耗gpu的图片变换替代改变宽、高等计算位置 3. 分包大小 受限于小程序的主包不能超过2M,所以我们只能将活动页放置于分包中，下面是我的分包设置。

```js
project
|────src
    | Thanks
        | pages
        | components
        | styles
        └ utils
    | pages
    | utils
    | styles
    | components
    | app.js
    └ APP.vue

└───project.config.json
```

app.js的配置如下

```json
config:{
  pages:[
    "pages/home/index", // 首页
  ],
  subPackages: [
    {
      root: "Thanks",
      pages: [
        "pages/home/index",// 首页
      ],
      independent: true
    }
  ],
}

```

4. 分包预加载

我们可以通过分包预加载的方案，来解决这个问题。打开首页，加载完主包后，可以静默加载其他分包，通过配置 preloadRule 即可实现分包预加载。

```js
"preloadRule":{
  "pages/index":{
    "network": "all",
    "packages": ["xxxx"]
  }
}

```

5. 按需引入代码和资源

在开发中， 我们可能引入一些新的库文件去使用其具体的方法等，这时候如果用到按需引入会更优。

```js
import dayjs from 'dayjs' // load on demand
import 'dayjs/locale/zh-cn' // 按需加载
```

自定义方法可以采用

```js
//index.js
export function promisify(fn) {

}

export default {
  promisify
}
//request.js
import { promisify } from "@/utils/index";
```

6. global 替代 vuex

wxStorage 在配合上 global 取代 vuex,没有必要为了用 vuex 而用 vuex，小程序自带的 global 就够了。

```js
    // 调用
   const { user } = getApp().globalData;
   // 存储
    getApp().globalData.user = user;
```

缓存是将一些常用的或者上一次请求回来的数据做保存，如 banner，用户信息等。

```js
  function setWxStorage(key, data) {
    wx.setStorage({
      key,
      data
    });
  }
  function getWxStorage(key) {
    try {
      const value = wx.getStorageSync(key)
      if (value) {
        return value
        // Do something with return value
      }
    } catch (e) {
      console.log('e: ', e);
      // Do something when catch error
    }
  }
```

6. 请求的封装

```js
function get(url) {
  this.then = function (resolve, reject) {
    if (url == '/tips') {
      // 避免重复弹出窗口，不做数据缓存
    } else if (url == '/user') {
      // 未授权用户，不做数据缓存
      let user = getWxStorage(url);
      if (user && user.aliasName){
        value && typeof resolve === 'function' && resolve.call(this, user)
      }
    } else {
      let value = getWxStorage(url)
      value && typeof resolve === 'function' && resolve.call(this, value)
    }
    request(url).then(res => {
      typeof resolve === 'function' && resolve.call(this, res)
      setWxStorage(url, res)
    }).catch(err => {
      typeof reject === 'function' && reject.call(this, err)
      throw err
    })
  }
  return this;
}

```

7. iphone 刘海屏上下适配

获取屏幕高度

```js

template
      <scroll-view
        scroll-y
        :style="`height: ${scrolHeight}px`"
        @scrolltolower="scrolltolower"
      >
    </scroll-view>
js
    getScroll() {
      const query = wx.createSelectorQuery();
      const res = query
        .select(".bar")
        .boundingClientRect()
        .exec(
          function(res) {
            let barHeight = res[0].height;
            let systemInfo = wx.getSystemInfoSync();
            this.scrolHeight = systemInfo.windowHeight - barHeight;
          }.bind(this)
        );
    },

```

获取tabbar高度

```js
    const model = wx.getSystemInfoSync().model;
    if(model.match("iPhone X")){
      this.isIpx = true;
    }

// 动态类

.fix-iphonex-icon {
  height: 142rpx;
  padding:32rpx 86rpx 80rpx;
}
```

自定义导航栏高度

```js
      let res = wx.getSystemInfoSync();
      // 导航栏总高度 & 占位块高度
      // {
      //       'iPhone': 64,
      //       'iPhoneX': 88,
      //       'Android': 68,
      //       'samsung': 72
      // }
      let isiOS = res.system.indexOf("iOS") > -1;
      let totalBar;
      if (!isiOS) {
        const model = res.model;
        if (model.match("samsung")) {
          totalBar = 34;
        } else {
          totalBar = 36;
        }
      } else {
        const model = res.model;
        if (model.match("iPhone X")) {
          totalBar = 44;
        } else {
          totalBar = 32;
        }
      }

      // 时间、信号等工具栏的高度
      let toolBar = res.statusBarHeight;
      this.tool_height = res.statusBarHeight;
      // 页面title栏的高度
      this.title_height = totalBar * 2 - toolBar;
    },


```

8. 巧用 mixins

我们可以将常用的全局方法，设置为mixins

```js
const shareMix = {
  onShareAppMessage(res) {
    let { title, imageUrl, path, user } = getApp().globalData;
    path = user._id ? `${path}?refer=${user._id}` : path;
    return {
      title,
      imageUrl,
      path
    };
  }
}
export default shareMix;

```

9. 热启动与冷启动

* 冷启动：如果用户首次打开，或小程序销毁后被用户再次打开，此时小程序需要重新加载启动，即冷启动。
* 热启动：如果用户已经打开过某小程序，然后在一定时间内再次打开该小程序，此时小程序并未被销毁，只是从后台状态进入前台状态，这个过程就是热启动

  通常，只有当小程序进入后台一定时间，或者系统资源占用过高，才会被销毁。 我们可以在必要时使用 wx.onMemoryWarning 监听内存告警事件，进行必要的内存清理。 小程序退出时，页面可能被销毁，函数 onSaveExitState 会被调用。

```js
  onLoad: function() {
    var prevExitState = this.exitState // 尝试获得上一次退出前 onSaveExitState 保存的数据
    if (prevExitState !== undefined) { // 如果是根据 restartStrategy 配置进行的冷启动，就可以获取到
      prevExitState.myDataField === 'myData'
    }
  },
  onSaveExitState: function() {
    var exitState = { myDataField: 'myData' } // 需要保存的数据
    return {
      data: exitState,
      expireTimeStamp: Date.now() + 24 * 60 * 60 * 1000 // 超时时刻
    }
  }


```

10. 页面的性能优化

建议一个页面使用少于1000个WXML的节点，节点树深度少于30层，子节点数不大于60个 赋值前可以对数据进行一次过滤，去掉不必要的条目和属性。

冗余数据放在 vue 中都会影响性能，单独的一个js文件存放冗余数据，再 export 出来。

```js
    let arr=[];
    let total = res.data.length;
    //一次插入5条
    let once = 5;
    //每条记录的索引
    let index = 0;
    let arr2 =[];
      //循环加载数据
    function loop(curTotal,curIndex){
      let oncePage = res.data.length /once
      let pageCount = Math.min(curTotal , once);
      if(curTotal <= 0){
          return false;
      }
      arr2 = res.data.concat(res.data);
      setTimeout(()=>{
          for(let i = 0; i < pageCount; i++){
              arr.push(arr2[i])
              that.showDialogs = arr;
          }
          loop(curTotal - pageCount,curIndex + pageCount)
      },1000 / 60)
    }
    loop(total,index);
```

11. swiper 优化

有时候我们需要图片进行轮播展示，如果我们采用 swiper 组件，每个 swiper 组件平均展示 10 张图片， 那么我们展示 3 个card的话， 节点将有 30 个，这在一些安卓低端机的开销依然很大，极易造成卡顿。 ![swiper](https://tva1.sinaimg.cn/large/006tNbRwgy1ga8t3mcbz4j30bk0ahq3j.jpg)

```js
  // template
  <swiper
    class="swiper"
    :indicator-dots=" banners.length > 1 "
    @change="changeSwiper">
    <block
      v-for="(item,index) in banners"
      :key="item">
      <swiper-item>
        <image
          v-if="index-currentIndex < 2 && index -currentIndex > -2 "
          :src="(index-currentIndex < 2 && index -currentIndex > -2 )? item.imgUrl : '' "
          class="img"
          @click="toBanner(item, index)" />
      </swiper-item>
    </block>
  </swiper>

  //js
  changeSwiper(event){
    const  {current, source} =   event.detail;
    this.currentIndex = current;
  },

```

12. 不要重复setdata

小程序分为逻辑层和渲染层，而我们每次逻辑层改变了，要借由 Native 进行。小程序的渲染层和逻辑层由两个线程管理：渲染层的界面使用了 WebView 进行渲染；逻辑层采用 JsCore 线程运行 JS 脚本。一个小程序存在多个界面，所以渲染层存在多个 WebView 线程，这两个线程的通信会经由微信客户端 Native 做中转，逻辑层发送网络请求也经由 Native 转发，

![渲染、逻辑](https://tva1.sinaimg.cn/large/006tNbRwgy1ga8zu9vfrqj30zm0qcabs.jpg)

所以我们不要重复 setdata ，以及减少数据的传输量。我们的数据传输实际是一次 Javascript 脚本过程，当数据量过大时会增加脚本的编译执行时间，占用 WebView JS 线程。去除不必要的事件绑定（ WXML 中的 bind 和 catch ），从而减少通信的数据量和次数。

小程序推出了一个 wxs 层，能够在视图层进行逻辑运算，感兴趣的看一下。 ![wxs](https://tva1.sinaimg.cn/large/006tNbRwgy1ga8zfy6tkcj314k0oggnc.jpg)

13. externals

涉及到react构建这些走 cdn 减少构建大小.

```js
export default {
  externals: {
    'react': 'window.React',
    'react-dom': 'window.ReactDOM',
    '@antv/f2': 'window.F2',
  },
  scripts: [
   'https://gw.alipayobjects.com/os/lib/react/16.13.1/umd/react.production.min.js',
   'https://gw.alipayobjects.com/os/lib/react-dom/16.13.1/umd/react-dom.production.min.js',
   'https://gw.alipayobjects.com/os/antv/assets/f2/3.4.2/f2.min.js',
  ],
}
```

14. 选用可替代的依赖

例如 Moment 这个时间处理库, 则可以替换成 dayjs, api 和用法都和 Moment 一致, 但是包大小却非常小.我们就可以替换掉.

15. 调整 splitChunks 策略

```js
export default {
 chainWebpack(config:any) {
    config.merge({
      optimization: {
        splitChunks: {
          chunks: 'all',
          automaticNameDelimiter: '.',
          name: true,
          minSize: 30000,
          minChunks: 1,
          cacheGroups: {
            vendors: {
              name: 'vendors',
              chunks: 'all',
              test: /[\\/]node_modules[\\/]/,
              priority: -12,
            },
          },
        },
      },
    });
  }
}
```

增加文件加载顺序声明 chunks ，因为我们增加了一个 js 文件，这是我们就要告诉项目，应该先加载哪个文件，如果你有增加其他的拆分文件，记得也要同步添加这个配置。

```javascript
export default {
  chunks: ['vendors', 'umi'],
}

```

16. 原生属性

MPVue 写的 input/textarea 在输入的时候就会出现光标闪烁/内容回滚的异常（原因就不赘述了），所以使用小程序原生语法重写了 input/textarea 组件，主要目的就是让组件的输入能够脱离 MPVue 的更新，直接组件内部走小程序的 setData ，因为当时 MPVue 还没有对数据更新做 diff 操作，目前据说做了一层 diff （实际效果大家自己去测评吧） 产品中的文章详情需要 Markdown 渲染，因为文章内容相对来说都是数据量比较大的，使用 MPVue 去对这部分数据进行解析的话性能会相对来说较差。因此我们也使用了原生去实现了一个 Markdown 渲染的组件。

## 参考文献

\[1]<https://developers.weixin.qq.com/miniprogram/dev/framework/audits/scoring.html>
