性能优化与最佳实践

💡 核心结论

  1. setData是性能瓶颈,需控制频率和数据量

  2. 图片懒加载和预加载可显著提升体验

  3. 分包加载可突破2MB主包限制

  4. 长列表使用虚拟列表,避免一次性渲染大量节点

  5. 合理使用缓存,减少网络请求


1. setData优化

1.1 setData原理

逻辑层(JS)  →  序列化数据  →  WebView线程  →  重新渲染
         ↑                             ↓
         └─────────── 耗时过程 ─────────┘

问题

  • 数据传输有性能开销

  • 会阻塞用户交互

  • 频繁调用导致卡顿

1.2 优化策略

1. 只更新必要的数据

// ❌ 错误:更新整个对象
this.setData({
  user: {
    name: '张三',
    age: 25,
    address: '北京',
    avatar: 'https://...'
  }
})

// ✅ 正确:只更新变化的字段
this.setData({
  'user.name': '李四'
})

2. 合并多次setData

// ❌ 错误:多次调用
this.setData({ count: 1 })
this.setData({ name: '张三' })
this.setData({ age: 25 })

// ✅ 正确:合并调用
this.setData({
  count: 1,
  name: '张三',
  age: 25
})

3. 控制数据量

// ❌ 错误:传输大量数据
this.setData({
  list: hugeArray  // 1000个复杂对象
})

// ✅ 正确:分页加载
this.setData({
  list: hugeArray.slice(0, 20)
})

4. 防抖和节流

// utils/debounce.js
export function debounce(fn, delay = 500) {
  let timer = null
  return function(...args) {
    if (timer) clearTimeout(timer)
    timer = setTimeout(() => {
      fn.apply(this, args)
    }, delay)
  }
}

// 使用
Page({
  onInput: debounce(function(e) {
    this.setData({
      keyword: e.detail.value
    })
    this.search()
  }, 300)
})

5. 避免在后台态调用

Page({
  data: {
    isActive: true
  },
  
  onShow() {
    this.setData({ isActive: true })
  },
  
  onHide() {
    this.setData({ isActive: false })
  },
  
  updateData() {
    // 只在页面激活时更新
    if (this.data.isActive) {
      this.setData({ /* data */ })
    }
  }
})

2. 长列表优化

2.1 虚拟列表

// components/virtual-list/virtual-list.js
Component({
  properties: {
    list: {
      type: Array,
      value: []
    },
    itemHeight: {
      type: Number,
      value: 100
    }
  },
  
  data: {
    visibleData: [],
    startIndex: 0,
    endIndex: 0,
    scrollTop: 0
  },
  
  lifetimes: {
    attached() {
      this.updateVisibleData()
    }
  },
  
  methods: {
    onScroll(e) {
      const { scrollTop } = e.detail
      this.setData({ scrollTop })
      this.updateVisibleData()
    },
    
    updateVisibleData() {
      const { list, itemHeight, scrollTop } = this.data
      
      // 计算可见区域
      const viewportHeight = wx.getSystemInfoSync().windowHeight
      const startIndex = Math.floor(scrollTop / itemHeight)
      const endIndex = Math.ceil((scrollTop + viewportHeight) / itemHeight)
      
      // 添加缓冲区
      const bufferSize = 5
      const start = Math.max(0, startIndex - bufferSize)
      const end = Math.min(list.length, endIndex + bufferSize)
      
      this.setData({
        visibleData: list.slice(start, end),
        startIndex: start,
        endIndex: end
      })
    }
  }
})

2.2 分页加载

Page({
  data: {
    list: [],
    page: 1,
    hasMore: true,
    loading: false
  },
  
  async onLoad() {
    await this.loadData()
  },
  
  async onReachBottom() {
    if (!this.data.hasMore || this.data.loading) {
      return
    }
    
    this.setData({
      page: this.data.page + 1
    })
    
    await this.loadData()
  },
  
  async loadData() {
    this.setData({ loading: true })
    
    try {
      const res = await wx.request({
        url: 'https://api.example.com/list',
        data: {
          page: this.data.page,
          size: 20
        }
      })
      
      const newList = res.data.list
      
      this.setData({
        list: [...this.data.list, ...newList],
        hasMore: newList.length === 20
      })
    } finally {
      this.setData({ loading: false })
    }
  },
  
  // 下拉刷新
  async onPullDownRefresh() {
    this.setData({
      list: [],
      page: 1,
      hasMore: true
    })
    
    await this.loadData()
    wx.stopPullDownRefresh()
  }
})

3. 图片优化

3.1 图片懒加载

<!-- pages/list/list.wxml -->
<scroll-view scroll-y bindscroll="onScroll">
  <view wx:for="{{list}}" wx:key="id">
    <image 
      src="{{item.visible ? item.image : placeholderImage}}"
      lazy-load
      mode="aspectFill"
    ></image>
  </view>
</scroll-view>
Page({
  data: {
    list: [],
    placeholderImage: '/images/placeholder.png'
  },
  
  onScroll(e) {
    const { scrollTop } = e.detail
    const viewportHeight = wx.getSystemInfoSync().windowHeight
    
    // 计算哪些图片应该加载
    this.data.list.forEach((item, index) => {
      const itemTop = index * 200  // 假设每项高度200rpx
      const itemBottom = itemTop + 200
      
      if (itemTop < scrollTop + viewportHeight && itemBottom > scrollTop) {
        this.setData({
          [`list[${index}].visible`]: true
        })
      }
    })
  }
})

3.2 图片预加载

Page({
  preloadImages() {
    const images = [
      'https://example.com/image1.jpg',
      'https://example.com/image2.jpg',
      'https://example.com/image3.jpg'
    ]
    
    images.forEach(src => {
      wx.getImageInfo({ src })
    })
  }
})

3.3 图片压缩

Page({
  async compressImage(filePath) {
    const res = await wx.compressImage({
      src: filePath,
      quality: 80  // 压缩质量 0-100
    })
    
    return res.tempFilePath
  },
  
  async chooseAndCompress() {
    const chooseRes = await wx.chooseImage({
      count: 1,
      sizeType: ['compressed']  // 选择压缩图
    })
    
    const compressedPath = await this.compressImage(chooseRes.tempFilePaths[0])
    
    // 上传压缩后的图片
    this.uploadImage(compressedPath)
  }
})

4. 分包加载

4.1 分包配置

// app.json
{
  "pages": [
    "pages/index/index",
    "pages/logs/logs"
  ],
  "subPackages": [
    {
      "root": "packageA",
      "name": "商城",
      "pages": [
        "pages/products/products",
        "pages/detail/detail",
        "pages/cart/cart"
      ]
    },
    {
      "root": "packageB",
      "name": "个人中心",
      "pages": [
        "pages/profile/profile",
        "pages/orders/orders",
        "pages/settings/settings"
      ],
      "independent": true  // 独立分包
    }
  ],
  "preloadRule": {
    "pages/index/index": {
      "network": "all",
      "packages": ["packageA"]
    }
  }
}

4.2 分包跳转

// 跳转到分包页面
wx.navigateTo({
  url: '/packageA/pages/products/products'
})

// 预下载分包
wx.preloadSubpackage({
  name: 'packageA',
  success() {
    console.log('分包预下载成功')
  }
})

5. 缓存策略

5.1 数据缓存

// utils/cache.js
const CACHE_EXPIRE = 5 * 60 * 1000  // 5分钟

export const cache = {
  set(key, value, expire = CACHE_EXPIRE) {
    const data = {
      value,
      expire: Date.now() + expire
    }
    wx.setStorageSync(key, data)
  },
  
  get(key) {
    const data = wx.getStorageSync(key)
    if (!data) return null
    
    if (Date.now() > data.expire) {
      wx.removeStorageSync(key)
      return null
    }
    
    return data.value
  },
  
  remove(key) {
    wx.removeStorageSync(key)
  },
  
  clear() {
    wx.clearStorageSync()
  }
}

// 使用
import { cache } from '../../utils/cache.js'

Page({
  async loadData() {
    // 先从缓存读取
    let data = cache.get('products')
    
    if (!data) {
      // 缓存不存在,请求接口
      const res = await wx.request({
        url: 'https://api.example.com/products'
      })
      
      data = res.data
      
      // 存入缓存
      cache.set('products', data)
    }
    
    this.setData({ products: data })
  }
})

5.2 图片缓存

// 小程序会自动缓存图片
// 使用相同的URL会从缓存读取

// 清除图片缓存
wx.removeImageCache({
  success() {
    console.log('图片缓存已清除')
  }
})

6. 代码优化

6.1 减少包体积

1. 清理无用代码

# 使用构建工具
npm run build

# 压缩图片
tinypng、imagemin

2. 使用npm包

// 按需引入
import { formatTime } from 'miniprogram-date-utils'

// 而不是
import * as utils from 'utils'

3. 代码分离

// 将公共代码抽离到utils
// utils/format.js
export function formatPrice(price) {
  return '¥' + (price / 100).toFixed(2)
}

6.2 避免不必要的渲染

<!-- ❌ 错误:wx:if频繁切换 -->
<view wx:if="{{show}}">内容</view>

<!-- ✅ 正确:hidden切换 -->
<view hidden="{{!show}}">内容</view>

<!-- 说明:
  wx:if: 条件为false时不渲染,适合不频繁切换
  hidden: 始终渲染,只是隐藏,适合频繁切换
-->

6.3 使用自定义组件

// components/product-item/product-item.js
Component({
  properties: {
    product: Object
  },
  
  methods: {
    onTap() {
      this.triggerEvent('tap', { id: this.data.product.id })
    }
  }
})
<!-- pages/list/list.wxml -->
<product-item 
  wx:for="{{products}}" 
  wx:key="id"
  product="{{item}}"
  bind:tap="onProductTap"
></product-item>

7. 性能监控

7.1 性能数据上报

// app.js
App({
  onLaunch() {
    // 获取性能数据
    const performance = wx.getPerformance()
    
    // 监听性能数据
    const observer = performance.createObserver((entryList) => {
      console.log('性能数据', entryList.getEntries())
      
      // 上报到后端
      this.reportPerformance(entryList.getEntries())
    })
    
    observer.observe({ entryTypes: ['navigation', 'render', 'script'] })
  },
  
  reportPerformance(entries) {
    wx.request({
      url: 'https://api.example.com/performance',
      method: 'POST',
      data: { entries }
    })
  }
})

7.2 体验评分

// 小程序后台 → 运维中心 → 性能监控
// 可以看到:
// - 启动耗时
// - 页面切换耗时
// - 首屏渲染时间
// - 内存占用
// - 卡顿次数

8. 最佳实践

8.1 代码规范

// 1. 使用ES6+语法
const { list } = this.data
const newList = [...list, newItem]

// 2. async/await替代回调
async loadData() {
  const res = await wx.request({ url: '...' })
  this.setData({ data: res.data })
}

// 3. 合理的注释
/**
 * 加载商品列表
 * @param {number} page 页码
 * @param {number} size 每页数量
 */
async loadProducts(page, size) {
  // ...
}

// 4. 错误处理
try {
  await this.loadData()
} catch (error) {
  console.error(error)
  wx.showToast({
    title: '加载失败',
    icon: 'none'
  })
}

8.2 目录结构

miniprogram/
├── pages/               # 页面
│   ├── index/
│   ├── detail/
│   └── profile/
├── components/          # 组件
│   ├── product-item/
│   └── user-card/
├── utils/               # 工具函数
│   ├── request.js
│   ├── cache.js
│   └── util.js
├── api/                 # API接口
│   ├── product.js
│   └── user.js
├── styles/              # 公共样式
│   └── common.wxss
├── images/              # 图片资源
└── config/              # 配置文件
    └── index.js

8.3 用户体验

// 1. 加载提示
wx.showLoading({ title: '加载中' })
await loadData()
wx.hideLoading()

// 3. 错误提示
wx.showToast({
  title: '操作失败',
  icon: 'none',
  duration: 2000
})
<!-- 2. 空状态 -->
<view wx:if="{{list.length === 0}}">
  <text>暂无数据</text>
</view>

<!-- 4. 骨架屏 -->
<view wx:if="{{loading}}" class="skeleton">
  <view class="skeleton-item"></view>
  <view class="skeleton-item"></view>
</view>

9. 常见问题

Q1: 如何调试性能问题?

A:

  1. 开发者工具 → 调试器 → Performance

  2. 查看setData调用频率和数据量

  3. 使用Trace工具分析

Q2: 如何优化首屏加载速度?

A:

  1. 减少首屏请求数量

  2. 使用骨架屏

  3. 预加载关键资源

  4. 启用分包预下载

Q3: 如何减少包体积?

A:

  1. 压缩图片资源

  2. 使用分包加载

  3. 清理无用代码

  4. 使用CDN存储静态资源


参考资源

  • 小程序性能优化指南

  • 小程序体验评分规则

  • 微信小程序最佳实践