uniapp 从入门到精通(第33章)
第7章 7.3 性能优化:分包 + 懒加载 + 图片压缩
🎯 开场 3 分钟:为什么要学这个?
上一章我们学会了用 Pinia 管理全局状态,终于告别了「这个变量到底从哪来的」灵魂拷问。但问题来了——项目越做越大,首屏加载越来越慢,用户点开就转圈圈,等得不耐烦直接跑了。
你有没有遇到过这些情况:
- 微信小程序包体积超限,被迫删功能
- 页面一多就卡顿,滑动像在看幻灯片
- 图片一多就白屏,用户以为你 app 挂了
这一章我们来解决这些问题。学完你就能掌握三把利剑:分包加载让首屏飞起来、懒加载让内存不爆炸、图片压缩让流量不心疼。
🧱 基础 25 分钟:核心概念
什么是分包?
生活类比:想象你去超市买东西,如果你一口气把整个超市的货都搬回家(搬运车爆炸警告),不如先把今天要用的放篮子里,其他的等需要再拿。
为什么要用:小程序有包体积限制(主包不能超过 2MB),全塞一起会超限。而且加载一堆用不到的东西,纯粹浪费用户流量和\n\n
\n\n
\n\n时间。
怎么用:
在 uniapp 项目中,修改 pages.json:
{
"pages": [
{
"path": "pages/index/index",
"style": { "navigationBarTitleText": "首页" }
}
],
"subPackages": [
{
"root": "pages-sub/order",
"pages": [
{
"path": "list",
"style": { "navigationBarTitleText": "订单列表" }
},
{
"path": "detail",
"style": { "navigationBarTitleText": "订单详情" }
}
]
}
]
}
这段配置的意思是:把 pages-sub/order 目录下的页面打包成独立分包,用户访问时才下载。
什么是懒加载?
生活类比:你刷抖音,不会一口气把 1000 条视频全部加载完,而是看到哪条才加载哪条。这叫「用时再取」,不占地方。
为什么要用:一个页面可能有 100 张商品图,如果一次全加载,内存直接爆掉。懒加载只加载用户看得见的部分,省内存又省流量。
怎么用:
方式一:uniapp 原生的懒加载图片
<template>
<view>
<image
v-for="item in productList"
:key="item.id"
:src="item.image"
lazy-load
mode="widthFix"
style="width: 750rpx;"
/>
</view>
</template>
<script>
export default {
data() {
return {
productList: [
{ id: 1, image: 'https://picsum.photos/400/300?random=1' },
{ id: 2, image: 'https://picsum.photos/400/300?random=2' },
{ id: 3, image: 'https://picsum.photos/400/300?random=3' },
{ id: 4, image: 'https://picsum.photos/400/300?random=4' },
{ id: 5, image: 'https://picsum.photos/400/300?random=5' }
]
}
}
}
</script>
lazy-load 属性一加,图片就变成「看到了才加载」模式。
方式二:列表数据懒加载(触底加载更多)
export default {
data() {
return {
productList: [],
page: 1,
pageSize: 10,
hasMore: true
}
},
onReachBottom() {
if (this.hasMore) {
this.loadMore()
}
},
methods: {
async loadMore() {
const res = await uni.request({
url: `https://api.example.com/products?page=${this.page}&size=${this.pageSize}`
})
if (res.data.length < this.pageSize) {
this.hasMore = false
}
this.productList = [...this.productList, ...res.data]
this.page++
}
}
}
onReachBottom 是 uniapp 的生命周期钩子,等用户滑到底了才加载下一页。
什么是图片压缩?
生活类比:你拍了一张 10MB 的照片发朋友圈,如果不压缩,传上去又慢又耗流量。压缩一下变成 200KB,既清晰又飞快。
为什么要用:原图动不动几 MB,加载慢、耗流量、占内存。压缩后体积骤降,体验飞起。
怎么用:
uniapp 提供了 uni.compressImage API:
uni.compressImage({
src: '/static/photos/original.jpg',
quality: 80,
success: (res) => {
console.log('压缩后路径:', res.tempFilePath)
// 上传到服务器
uni.uploadFile({
url: 'https://api.example.com/upload',
filePath: res.tempFilePath,
name: 'image',
success: (uploadRes) => {
console.log('上传成功:', uploadRes.data)
}
})
},
fail: (err) => {
console.error('压缩失败:', err)
uni.showToast({ title: '图片处理失败', icon: 'none' })
}
})
quality: 80 表示压缩到原图的 80% 清晰度(数值越小压缩越狠)。
🔥 实战 35 分钟:3 个递进的小项目
项目 1:5 分钟 - 主页 + 订单分包的完整配置
目标:学会配置一个完整的主包 + 分包结构。
完整代码:
pages.json:
{
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationBarTitleText": "首页",
"enablePullDownRefresh": true
}
},
{
"path": "pages/cart/cart",
"style": {
"navigationBarTitleText": "购物车"
}
}
],
"subPackages": [
{
"root": "pages-sub/order",
"pages": [
{
"path": "list",
"style": { "navigationBarTitleText": "我的订单" }
},
{
"path": "detail",
"style": { "navigationBarTitleText": "订单详情" }
}
]
},
{
"root": "pages-sub/user",
"pages": [
{
"path": "profile",
"style": { "navigationBarTitleText": "个人中心" }
},
{
"path": "settings",
"style": { "navigationBarTitleText": "设置" }
}
]
}
],
"preloadRule": {
"pages-sub/order/list": {
"network": "all",
"packages": ["pages-sub/order"]
}
}
}
预期输出:控制台无报错,分包页面能正常访问。
解释:preloadRule 是预加载规则,提前把订单分包下载好,用户点进去时就不用等了。
项目 2:15 分钟 - 带懒加载的商品列表页
目标:从 JSON 文件读取商品数据,实现触底加载更多 + 图片懒加载。
商品数据文件 static/data/products.json:
[
{ "id": 1, "name": "iPhone 15", "price": 5999, "image": "https://picsum.photos/200/200?random=1" },
{ "id": 2, "name": "MacBook Pro", "price": 9999, "image": "https://picsum.photos/200/200?random=2" },
{ "id": 3, "name": "AirPods Pro", "price": 1899, "image": "https://picsum.photos/200/200?random=3" },
{ "id": 4, "name": "iPad Air", "price": 4399, "image": "https://picsum.photos/200/200?random=4" },
{ "id": 5, "name": "Apple Watch", "price": 2999, "image": "https://picsum.photos/200/200?random=5" }
]
商品列表页面 pages/product/list.vue:
<template>
<view class="container">
<view class="product-grid">
<view
v-for="item in productList"
:key="item.id"
class="product-card"
>
<image
:src="item.image"
lazy-load
mode="aspectFill"
class="product-image"
/>
<view class="product-info">
<text class="product-name">{{ item.name }}</text>
<text class="product-price">¥{{ item.price }}</text>
</view>
</view>
</view>
<view v-if="!hasMore" class="no-more">
<text>— 没有更多了 —</text>
</view>
</view>
</template>
<script>
export default {
data() {
return {
productList: [],
allProducts: [],
page: 0,
pageSize: 5,
hasMore: true,
loading: false
}
},
onLoad() {
this.loadAllProducts()
},
onReachBottom() {
if (!this.loading && this.hasMore) {
this.loadMore()
}
},
methods: {
loadAllProducts() {
// 模拟从本地 JSON 加载数据
const products = require('@/static/data/products.json')
// 模拟后端返回多条数据(实际项目从 API 获取)
this.allProducts = [...products, ...products, ...products]
this.loadMore()
},
loadMore() {
this.loading = true
setTimeout(() => {
const start = this.page * this.pageSize
const end = start + this.pageSize
const newItems = this.allProducts.slice(start, end)
if (newItems.length === 0) {
this.hasMore = false
} else {
this.productList = [...this.productList, ...newItems]
this.page++
if (newItems.length < this.pageSize) {
this.hasMore = false
}
}
this.loading = false
}, 500)
}
}
}
</script>
<style>
.container {
padding: 20rpx;
background-color: #f5f5f5;
min-height: 100vh;
}
.product-grid {
display: flex;
flex-wrap: wrap;
justify-content: space-between;
}
.product-card {
width: 49%;
background: #fff;
border-radius: 16rpx;
margin-bottom: 20rpx;
overflow: hidden;
}
.product-image {
width: 100%;
height: 340rpx;
}
.product-info {
padding: 20rpx;
}
.product-name {
display: block;
font-size: 28rpx;
color: #333;
margin-bottom: 10rpx;
}
.product-price {
display: block;
font-size: 32rpx;
color: #ff6034;
font-weight: bold;
}
.no-more {
text-align: center;
padding: 30rpx;
color: #999;
font-size: 24rpx;
}
</style>
预期输出:首次加载 5 个商品,向下滑动加载更多,直到全部加载完显示「没有更多了」。
解释:用 lazy-load 让图片「看到了再加载」,用 onReachBottom 实现触底加载更多,逻辑和之前讲的完全一样。
项目 3:15 分钟 - 图片压缩上传小工具
目标:做一个简单的「选图 → 压缩 → 上传」工具,组合使用分包 + 图片压缩。
主包页面 pages/index/index.vue:
<template>
<view class="container">
<view class="title">图片压缩上传工具</view>
<view class="upload-area" @click="chooseImage">
<image
v-if="selectedImage"
:src="compressedPath || selectedImage"
mode="aspectFit"
class="preview-image"
/>
<view v-else class="placeholder">
<text class="icon">+</text>
<text class="text">点击选择图片</text>
</view>
</view>
<view v-if="selectedImage" class="info-panel">
<view class="info-row">
<text>原图大小:</text>
<text>{{ originalSize }} KB</text>
</view>
<view v-if="compressedSize" class="info-row">
<text>压缩后:</text>
<text class="highlight">{{ compressedSize }} KB</text>
</view>
<view v-if="compressedSize" class="info-row">
<text>节省:</text>
<text class="highlight">{{ savePercent }}%</text>
</view>
</view>
<button
v-if="selectedImage && !compressedPath"
type="primary"
@click="compressImage"
:loading="compressing"
>
{{ compressing ? '压缩中...' : '压缩图片' }}
</button>
<button
v-if="compressedPath"
type="warn"
@click="uploadImage"
:loading="uploading"
>
{{ uploading ? '上传中...' : '上传到服务器' }}
</button>
</view>
</template>
<script>
export default {
data() {
return {
selectedImage: '',
compressedPath: '',
originalSize: 0,
compressedSize: 0,
compressing: false,
uploading: false
}
},
computed: {
savePercent() {
if (!this.originalSize || !this.compressedSize) return 0
return Math.round((1 - this.compressedSize / this.originalSize) * 100)
}
},
methods: {
chooseImage() {
uni.chooseImage({
count: 1,
sizeType: ['original'],
sourceType: ['album', 'camera'],
success: (res) => {
this.selectedImage = res.tempFilePaths[0]
this.compressedPath = ''
this.compressedSize = 0
// 获取原图大小
uni.getFileInfo({
filePath: res.tempFilePaths[0],
success: (info) => {
this.originalSize = Math.round(info.size / 1024)
}
})
}
})
},
compressImage() {
this.compressing = true
uni.compressImage({
src: this.selectedImage,
quality: 60,
success: (res) => {
this.compressedPath = res.tempFilePath
this.compressing = false
// 获取压缩后大小
uni.getFileInfo({
filePath: res.tempFilePath,
success: (info) => {
this.compressedSize = Math.round(info.size / 1024)
}
})
},
fail: (err) => {
this.compressing = false
uni.showToast({
title: '压缩失败',
icon: 'none'
})
console.error('压缩失败:', err)
}
})
},
uploadImage() {
this.uploading = true
// 模拟上传(实际项目替换为真实接口)
setTimeout(() => {
this.uploading = false
uni.showToast({
title: '上传成功',
icon: 'success'
})
console.log('上传成功,远程路径:https://cdn.example.com/image.jpg')
}, 1500)
}
}
}
</script>
<style>
.container {
padding: 40rpx;
min-height: 100vh;
background: #f8f8f8;
}
.title {
font-size: 40rpx;
font-weight: bold;
text-align: center;
margin-bottom: 60rpx;
color: #333;
}
.upload-area {
width: 100%;
height: 500rpx;
background: #fff;
border: 2rpx dashed #ddd;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 40rpx;
}
.preview-image {
width: 100%;
height: 100%;
}
.placeholder {
display: flex;
flex-direction: column;
align-items: center;
color: #999;
}
.placeholder .icon {
font-size: 80rpx;
line-height: 1;
margin-bottom: 20rpx;
}
.placeholder .text {
font-size: 28rpx;
}
.info-panel {
background: #fff;
border-radius: 12rpx;
padding: 30rpx;
margin-bottom: 40rpx;
}
.info-row {
display: flex;
justify-content: space-between;
padding: 15rpx 0;
font-size: 28rpx;
color: #666;
border-bottom: 1rpx solid #f0f0f0;
}
.info-row:last-child {
border-bottom: none;
}
.highlight {
color: #ff6034;
font-weight: bold;
}
button {
margin-top: 20rpx;
}
</style>
预期输出:点击选择图片 → 显示原图和大小 → 点击压缩 → 显示压缩后大小和节省比例 → 点击上传 → 提示上传成功。
解释:这个小工具把「选图」「压缩」「上传」三步串起来,压缩后的图片体积小很多,上传又快又省流量。
💪 进阶 20 分钟:常见坑 + 性能小贴士
坑 1:分包路径写错了
❌ 错误写法:
"root": "/pages-sub/order"
路径多了个 /,会导致分包加载失败。
✅ 正确写法:
"root": "pages-sub/order"
root 不能以 / 开头,直接写目录名就行。
坑 2:懒加载在某些机型不生效
❌ 错误用法:
<image :src="item.image" lazy-load />
在某些小程序版本,lazy-load 可能不工作。
✅ 正确用法:
配合 v-if 控制显示:
<image v-if="item.visible" :src="item.image" lazy-load />
或者使用成熟的第三方懒加载组件(如 easy-loadimage)。
坑 3:图片压缩质量设太低
❌ 错误代码:
uni.compressImage({
src: path,
quality: 10 // 太低导致图片糊成一团
})
✅ 正确代码:
uni.compressImage({
src: path,
quality: 70 // 70-80 是平衡清晰度和体积的最佳区间
})
坑 4:onReachBottom 在某些页面不触发
❌ 错误:在 pages.json 给页面加了 scroll-view 又监听 onReachBottom。
✅ 正确做法:
方案一:使用 scroll-view 的滚动事件
<scroll-view scroll-y @scrolltolower="loadMore">
方案二:确保页面没有阻止触底事件的全屏 scroll-view
坑 5:分包预加载规则写错
❌ 错误:
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["pages-sub/order"] // 路径写错了
}
}
✅ 正确:
"preloadRule": {
"pages/index/index": {
"network": "all",
"packages": ["pages-sub/order"] // 要和 subPackages 中的 root 一致
}
}
性能小贴士:使用骨架屏减少白屏等待
用户等待加载时看到一个白屏会觉得 app 卡死了。加点骨架屏(loading 占位),体验好很多:
<template>
<view>
<!-- 加载中显示骨架屏 -->
<view v-if="loading" class="skeleton">
<view class="skeleton-img"></view>
<view class="skeleton-text"></view>
<view class="skeleton-text short"></view>
</view>
<!-- 加载完显示真实内容 -->
<view v-else>
<image :src="product.image" lazy-load />
<text>{{ product.name }}</text>
</view>
</view>
</template>
调试技巧:查看分包加载情况
打开微信开发者工具 → 详情 → 分包信息,可以查看每个分包的大小和加载状态。
// 打印当前加载的分包
console.log('当前分包信息:', getCurrentSubPackage())
✏️ 练习题 + 作业题
练习题(10 分钟)
练习 1(2 分钟):分包路径纠错
- 输入:以下 JSON 配置中,root 路径写错了吗?
"root": "/pages-sub/shop"
- 预期输出:指出错误并给出正确答案
- 提示:检查是否有不必要的字符在开头
练习 2(2 分钟):添加懒加载
- 输入:给这张图片添加懒加载属性
<image :src="item.url" />
- 预期输出:
<image :src="item.url" lazy-load /> - 提示:只需要加一个属性
练习 3(3 分钟):触底加载更多
- 输入:在商品列表中加入「下拉加载更多」功能,已知 loadMore() 方法存在
- 预期输出:在 onReachBottom 中调用 loadMore
- 提示:找到 onReachBottom 生命周期钩子
练习 4(3 分钟):图片压缩质量调整
- 输入:把压缩质量从 50 调整到 80
- 预期输出:quality: 80
- 提示:只改一个数字
练习 5(挑战题,5 分钟):分析报错
- 输入:用户反馈「点击压缩没反应,控制台报错 compressImage:fail」
- 预期输出:列出 3 个可能的原因
- 提示:考虑图片路径、格式、权限等
作业题(30 分钟 - 2 小时)
作业:做一个「图片工具箱」
-
需求描述:整合本章所学的分包、懒加载、图片压缩知识,做一个小工具集合
-
功能点:
1. 使用分包加载不同工具页面(压缩工具、查看 EXIF 信息)
2. 图片列表使用懒加载
3. 实现图片压缩功能,可调节压缩质量 -
加分项:
1. 添加压缩前后对比
2. 支持批量压缩多张图片 -
验收标准:
- 分包配置正确,能正常访问
- 图片懒加载正常工作
- 压缩功能可用,体积明显减小
-
代码有适当注释
-
提交方式:评论区贴代码或 GitHub 链接
📚 总结 + 资源
本文学了 3 件事:
1. 分包:把不常用的页面拆出去,首屏加载飞起来
2. 懒加载:图片「看到了再加载」,省内存省流量
3. 图片压缩:uni.compressImage 一行搞定,体积骤降体验飙升
延伸学习资源:
- uniapp 官方性能优化指南 - 官方出品,必属精品
- 微信小程序分包加载文档 - 分包原理解释得很清楚
- 《uniapp 跨平台开发详解》- 系统学习 uniapp 各平台差异
互动钩子:你在项目中最头疼的性能问题是什么?首屏加载慢?图片太多卡顿?评论区聊聊,老粉优先回复!
下一章预告:学会了性能优化,但 app 总有崩溃的时候……下一章我们来做「错误监控与日志」,让你的 app 能「自查身体」,报错了第一时间知道原因。敬请期待!

评论(0)