性能优化与最佳实践
💡 核心结论
setData是性能瓶颈,需控制频率和数据量
图片懒加载和预加载可显著提升体验
分包加载可突破2MB主包限制
长列表使用虚拟列表,避免一次性渲染大量节点
合理使用缓存,减少网络请求
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:
开发者工具 → 调试器 → Performance
查看setData调用频率和数据量
使用Trace工具分析
Q2: 如何优化首屏加载速度?
A:
减少首屏请求数量
使用骨架屏
预加载关键资源
启用分包预下载
Q3: 如何减少包体积?
A:
压缩图片资源
使用分包加载
清理无用代码
使用CDN存储静态资源
参考资源
小程序性能优化指南
小程序体验评分规则
微信小程序最佳实践