2024 年我们一个微信小程序日活突破 80 万,首次启动加载从 3 秒涨到 8 秒,关键路径页面卡顿严重。投了一个月做性能优化,启动 8s→1.2s,关键页面 FMP 1.8s→400ms,小程序评分从 78 升到 96。本文复盘小程序性能优化全套手段:分包加载、骨架屏、setData 优化、图片懒加载、自定义 Web 组件渲染。
性能现状
小程序:微信小程序(电商类)
代码量:300 个页面,800 个组件,主包 6.8MB(超 2MB 上限)
日活:80 万,峰值 QPS 5000
性能数据(优化前):
- 启动时长:p50=5s, p99=8s
- 首页 FMP(First Meaningful Paint):3.5s
- 商品详情页 FMP:4s
- setData 频率:有的页面 100 次/秒(疯狂渲染)
- 内存:iOS 800MB(快被微信杀)
- 评分:微信开发者工具 78
体验问题:
- 首次启动白屏长
- 列表滚动卡顿
- 点击后页面切换慢
- 频繁卡死后台
启动优化:分包加载
// app.json 分包配置
{
"pages": [
"pages/index/index",
"pages/cart/cart",
"pages/user/user"
],
"subpackages": [
{
"root": "pages/goods",
"name": "goods",
"pages": [
"detail/detail",
"list/list",
"category/category"
],
"independent": false
},
{
"root": "pages/order",
"name": "order",
"pages": [
"create/create",
"pay/pay",
"list/list"
]
},
{
"root": "pages/marketing",
"name": "marketing",
"pages": [
"activity/activity",
"coupon/coupon",
"lottery/lottery"
]
}
],
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["goods"]
},
"pages/goods/detail/detail": {
"network": "wifi",
"packages": ["order"]
}
}
}
// 结果:
// 主包从 6.8MB → 1.5MB
// 启动只下载主包(1.5MB → 加载时长降 75%)
// 进入"商品详情"自动后台预加载"订单"包
独立分包(独立运行)
// 独立分包:不依赖主包,可单独运行
{
"subpackages": [
{
"root": "pages/share",
"name": "share",
"pages": [
"post/post",
"topic/topic"
],
"independent": true // 独立分包
}
]
}
// 独立分包优势:
// - 分享卡片直接打开,无需先加载主包
// - 启动只下载独立分包(可能只 500KB)
// - 适合分享流量高的场景
骨架屏:首屏体感优化
{{goods.title}}
¥{{goods.price}}
{{goods.desc}}
setData 性能优化
// 错误 1:频繁 setData
Page({
data: { list: [] },
onLoad() {
// 100 次 setData → 100 次渲染
for (let i = 0; i < 100; i++) {
this.setData({
[`list[${i}]`]: { id: i, name: 'item' + i }
});
}
}
});
// 正确 1:批量 setData
Page({
data: { list: [] },
onLoad() {
const newList = [];
for (let i = 0; i < 100; i++) {
newList.push({ id: i, name: 'item' + i });
}
this.setData({ list: newList }); // 1 次 setData
}
});
// 错误 2:setData 大对象
this.setData({
goods: { /* 100KB 的商品数据 */ }
});
// 数据量大,渲染慢
// 正确 2:只更新变化的字段
this.setData({
'goods.price': 99,
'goods.stock': 50
});
// 错误 3:setData 触发不必要的渲染
data: {
goods: {},
cart: [],
filterOpen: false
}
// 改 filterOpen 时,整个 data 重新 diff
this.setData({ filterOpen: true });
// 即使只改一个布尔值,WXML 全 diff
// 正确 3:用 nextTick 合并
let pending = null;
function batchSetData(updates) {
if (pending) {
Object.assign(pending, updates);
return;
}
pending = updates;
wx.nextTick(() => {
this.setData(pending);
pending = null;
});
}
长列表优化
{{item.name}}
{{item.name}}
...
图片懒加载
分包预下载
// 关键路径预加载下一个页面的资源
App({
onLaunch() {
// 启动后 2 秒,预加载商品分包
setTimeout(() => {
wx.preloadSubpackage({
name: 'goods',
success: () => console.log('goods 预加载完成'),
fail: console.error
});
}, 2000);
}
});
// 路由预加载
wx.preloadAssets({
data: [
{ type: 'image', src: 'https://cdn.../banner.jpg' },
{ type: 'image', src: 'https://cdn.../logo.png' }
]
});
API 请求优化
// 1. 请求并发限制(微信限 10 个并发)
class RequestQueue {
constructor(limit = 6) {
this.limit = limit;
this.running = 0;
this.queue = [];
}
add(fn) {
return new Promise((resolve, reject) => {
this.queue.push({ fn, resolve, reject });
this._next();
});
}
_next() {
if (this.running >= this.limit || !this.queue.length) return;
const { fn, resolve, reject } = this.queue.shift();
this.running++;
fn().then(resolve, reject).finally(() => {
this.running--;
this._next();
});
}
}
const queue = new RequestQueue(6);
function request(options) {
return queue.add(() => new Promise((resolve, reject) => {
wx.request({
...options,
success: resolve,
fail: reject
});
}));
}
// 2. 请求合并(同时多个组件请求相同 API)
const pendingRequests = new Map();
function dedupRequest(key, fn) {
if (pendingRequests.has(key)) {
return pendingRequests.get(key);
}
const p = fn().finally(() => pendingRequests.delete(key));
pendingRequests.set(key, p);
return p;
}
// 用法
dedupRequest('user_profile', () => api.getUserProfile());
// 3. 缓存 + SWR
async function fetchWithCache(key, fetcher, ttl = 60000) {
const cached = wx.getStorageSync(key);
if (cached && Date.now() - cached.ts < ttl) {
// 立即返回缓存
fetcher().then(fresh => {
wx.setStorageSync(key, { data: fresh, ts: Date.now() });
});
return cached.data;
}
const fresh = await fetcher();
wx.setStorageSync(key, { data: fresh, ts: Date.now() });
return fresh;
}
自定义组件性能
// 1. observer 不要触发 setData 死循环
Component({
properties: {
list: {
type: Array,
observer(newVal) {
// ❌ 错:setData 触发 observer 又改 list
this.setData({ list: newVal.filter(...) });
}
}
}
});
// 正确:用 _data 私有属性 + 计算属性模式
Component({
data: { _filtered: [] },
properties: {
list: {
type: Array,
observer(newVal) {
// 不要回写 list,setData 内部状态
this.setData({ _filtered: newVal.filter(...) });
}
}
}
});
// 2. pureDataPattern 不参与渲染
Component({
options: {
pureDataPattern: /^_/ // _ 开头不参与 WXML 渲染
},
data: {
visible: true, // 参与渲染
_cache: {} // 不参与渲染,不触发 diff
}
});
// 3. 大对象用 wx:if 包裹,不渲染时不存在
性能监控
// 1. 内置 performance API
const observer = wx.getPerformance().createObserver(entryList => {
entryList.getEntries().forEach(entry => {
console.log(entry.entryType, entry.name, entry.duration);
// 上报
report('performance', {
type: entry.entryType,
name: entry.name,
duration: entry.duration,
route: getCurrentPages().pop().route
});
});
});
observer.observe({
entryTypes: ['navigation', 'render', 'script', 'loadPackage']
});
// 2. setData 监控
const originalSetData = Page.prototype.setData;
Page.prototype.setData = function(data, callback) {
const start = Date.now();
const size = JSON.stringify(data).length;
const result = originalSetData.call(this, data, () => {
report('setData', {
route: this.route,
duration: Date.now() - start,
size,
keys: Object.keys(data).join(',')
});
callback && callback();
});
if (size > 256 * 1024) {
console.warn('setData too large', size, this.route);
}
return result;
};
// 3. 启动性能埋点
const launchStart = Date.now();
App({
onLaunch() {
report('launch', { duration: Date.now() - launchStart });
},
onShow() {
report('show', { duration: Date.now() - launchStart });
}
});
优化后效果
指标 优化前 优化后 变化
========================================================
启动 p50 5s 1.2s -76%
启动 p99 8s 2.5s -69%
首页 FMP 3.5s 400ms -89%
商品详情 FMP 4s 600ms -85%
主包大小 6.8MB 1.5MB -78%
setData 频率峰值 100/s 10/s -90%
内存(iOS) 800MB 300MB -63%
微信开发者工具评分 78 96 +23%
日均崩溃率 2.3% 0.3% -87%
业务影响:
- 启动慢的流失用户从 18% → 3%
- 首页转化率 +12%
- 商品页跳出率 -25%
避坑清单
- 主包 < 2MB,大于则必须分包
- setData 批量调用,避免 100 次循环 setData
- setData 数据 < 256KB,不要传大对象
- 长列表用 recycle-view 或分页,不要一次渲染上千
- 图片用 lazy-load + CDN 缩放 + WebP
- 骨架屏比 loading 转圈更有体感
- 分包预加载关键路径资源
- 请求合并 + 缓存 + SWR 减少 API 调用
- 组件用 pureDataPattern 区分渲染/非渲染数据
- 性能监控全埋点,SLI/SLO 持续跟踪
总结
小程序性能优化是个综合工程:启动 + 渲染 + 网络 + 内存四个维度都要优化。我们这次最大的收获是分包 + 骨架屏 + setData 优化三板斧,90% 的性能问题都靠这三个解决。日活 80w 的小程序,启动慢 1 秒可能流失几万用户,性能不是技术问题是业务问题。最大的认知改变:小程序运行环境是受限的(WebView + 双线程),不能直接套用 Web 性能优化经验 — setData 是核心瓶颈,理解双线程渲染机制是优化前提。微信开发者工具自带的 Audit + Performance Trace 是入门必学。
—— 别看了 · 2026