小程序的性能红线比 Web 苛刻得多:主包 2MB 上限、启动加载有评分、setData 频繁会掉帧。本文围绕包体积、启动速度、运行时性能三个维度,给出可直接落地的优化清单。
一、包体积优化:分包是第一要务
为什么必须分包
微信小程序限制:主包 ≤ 2MB,单个分包 ≤ 2MB,总包 ≤ 30MB(普通分包)。业务做多了,主包必然爆。分包加载(subPackages)把非首屏页面拆出去,用户进入对应模块时才下载。
分包配置
// pages.json
{
"pages": [
"pages/index/index",
"pages/order/list"
],
"subPackages": [
{
"root": "subpkg-order",
"pages": [
"pages/detail/detail",
"pages/refund/refund",
"pages/invoice/invoice"
]
},
{
"root": "subpkg-member",
"pages": [
"pages/coupon/coupon",
"pages/points/points"
]
}
],
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["subpkg-order"]
},
"pages/order/list": {
"network": "wifi",
"packages": ["subpkg-member"]
}
}
}目录结构相应调整:
src/
├── pages/ # 主包:只放首屏路径
├── subpkg-order/
│ └── pages/
│ ├── detail/
│ ├── refund/
│ └── invoice/
├── subpkg-member/
│ └── pages/
├── static/ # 只放主包用的静态资源
└── components/分包的两个关键规则:
- 分包页面跳转用完整路径:
uni.navigateTo({ url: '/subpkg-order/pages/detail/detail?id=1' }) - 分包可以引用主包的组件/工具,反之不行(主包不能 import 分包内资源,否则等于没拆)
preloadRule:分包预下载
上面配置里用户在首页时,预下载订单分包(全网络),进入订单列表时预下载会员分包(仅 WiFi)。配置得当能把"进入子模块的白屏时间"压到无感。
静态资源治理
主包 2MB 里最容易被图片吃掉:
- 图片 CDN 化:static 目录里只留 tab 图标等必须本地的资源,其余全部走远程 URL
- 压缩:tinypng 压一遍,通常能砍 60%+
- 按需引入组件库:uni-ui 等支持 easycom 按需引入,但要确认 tree-shaking 生效;uview-plus 建议用 its 按需加载配置
// vite.config.js 分析包体积
import { visualizer } from 'rollup-plugin-visualizer'
export default {
plugins: [visualizer({ filename: 'stats.html' })]
}构建后打开 stats.html,哪些模块占了体积一目了然。
二、启动性能:首屏体验
启动流程与耗时构成
小程序启动 = 下载代码包 → 初始化 → 页面首次渲染。开发者能控制的是初始化阶段的同步任务量和首屏依赖的数据请求。
优化一:App.onLaunch 里别做重活
// App.vue
export default {
onLaunch() {
// ❌ 常见错误:启动时同步拉一堆配置
// this.initConfig() // 200ms+ 的同步阻塞
// ✅ 延迟到首页渲染后再做
// this.getConfigId = setTimeout(() => this.initConfig(), 0)
}
}启动阶段每个同步 API 调用、每个 await 都直接推迟首屏。原则:onLaunch 只做必须的(登录态检查),其余全部后置。
优化二:骨架屏
// pages.json
{ "path": "pages/index/index", "style": { "navigationStyle": "custom" } }HBuilderX 可以把页面骨架生成微信快照。手写方案是在数据未到时渲染结构占位:
<template>
<view>
<template v-if="loaded">
<view v-for="item in list" :key="item.id" class="goods-card">
<image :src="item.cover" />
<text>{{ item.title }}</text>
</view>
</template>
<template v-else>
<view v-for="i in 4" :key="i" class="goods-card skeleton">
<view class="skeleton-img" />
<view class="skeleton-line" />
</view>
</template>
</view>
</template>
<style>
.skeleton-img, .skeleton-line {
background: linear-gradient(90deg, #f2f2f2 25%, #e6e6e6 50%, #f2f2f2 75%);
background-size: 200% 100%;
animation: shimmer 1.2s infinite;
}
@keyframes shimmer { to { background-position: -200% 0; } }
</style>优化三:初始渲染缓存
微信端开启后,第二次打开直接展示上次渲染的快照,再用新数据更新:
{ "path": "pages/index/index", "style": { "initialRenderingCache": "static" } }三、运行时性能:setData 与长列表
理解 setData 的成本
uni-app 编译到小程序后,逻辑层和渲染层分离,每次响应式数据变化都会产生一次 setData 通信。数据量大、频率高时,通信本身成为瓶颈。
规则一:不要频繁 setData 小数据
// ❌ 倒计时每秒 setData 一次整个对象
this.timer = setInterval(() => {
this.leftSeconds-- // 对象级更新
}, 1000)
// ✅ 高频更新只改必要字段,或者干脆用 wx 层优化
// 页面数据结构上把高频变化的字段隔离出来规则二:长列表分页 + 局部更新
<script>
export default {
data() {
return {
list: [],
page: 1,
finished: false,
loading: false
}
},
onReachBottom() {
this.loadMore()
},
methods: {
async loadMore() {
if (this.loading || this.finished) return
this.loading = true
try {
const { records, hasMore } = await this.$api.getOrders({
page: this.page + 1
})
// 追加而不是替换,配合 :key 复用节点
this.list.push(...records)
this.page++
this.finished = !hasMore
} finally {
this.loading = false
}
}
}
}
</script>长列表的进阶方案是虚拟列表:只渲染可视区域附近的节点。长列表组件(如 z-paging、uview 的 u-list)内置了该能力:
<z-paging
v-model="list"
@query="queryList"
:default-page-size="20"
use-virtual-list
use-inner-scroll
>
<view v-for="(item, index) in list" :key="item.id">
<text>{{ item.title }}</text>
</view>
</z-paging>上千条数据滚动如丝般顺滑的核心:滚动容器内只保留视口 ± 缓冲区内的真实节点,用撑高的占位元素维持滚动条。
规则三:图片懒加载
<image :src="item.cover" lazy-load mode="aspectFill" />原生 image 组件的 lazy-load 让图片进入视口前后才加载,列表页流量和渲染压力立减。
规则四:避免大对象进 data
不变的数据(城市列表、配置表)放 this 挂载而非 data,避免进入响应式系统产生 setData 开销:
// ❌ 一万条城市数据放 data,每次任意 setData 都可能全量传输
// ✅ 非渲染数据脱离响应式
created() {
this.allCities = cityData // 不进 data,只做查找用
}四、优化检查清单
上线前对照过一遍:
包体积
- [ ] 主包 < 1.5MB(留余量)
- [ ] 非首屏页面全部拆分包
- [ ] preloadRule 覆盖高频跳转路径
- [ ] static 目录无超过 50KB 的图片
- [ ] stats.html 确认无意外的大依赖
启动
- [ ] onLaunch 只留登录态检查
- [ ] 首页有骨架屏或初始渲染缓存
- [ ] 首屏接口合并,首屏数据 < 3 个请求
运行时
- [ ] 长列表分页 + 虚拟列表
- [ ] image 全部 lazy-load + 合适的 mode
- [ ] 高频更新字段做隔离
- [ ] 定时器在 onHide 暂停、onUnload 清除
- [ ] 真机(低端安卓)过一遍核心页面流畅度
五、工具
- 微信开发者工具 → 审计:代码依赖分析、包体积构成
- 体验评分:自动检测常见性能问题
- 真机调试 Performance 面板:setData 频率和耗时可视化
优化永远要以数据为依据:先跑一遍体验评分和真机 profile,找到真实瓶颈再动手,而不是凭感觉瞎改。
总结
- 分包是包体积问题的根本解,preloadRule 消除分包切换白屏
- 启动性能核心是砍掉 onLaunch 的同步任务,骨架屏改善体感
- setData 通信是小程序性能的第一杀手:合并更新、隔离高频字段、大对象不进 data
- 千级长列表必须上虚拟列表
- 一切优化先 profile 后动手
性能优化做到位的小程序,启动快、滚动顺、耗电少,用户体验评分和留存都会直观反映出来。
评论 (0)