功能都做完了,但小程序体验评分只有 70 分?首次打开要等 3 秒?这篇文章以心动恋聊 小程序为例,展示如何和 AI 对话,系统性地进行分包优化、图片优化、渲染优化,让小程序飞起来。
系列专栏 :【AI 编程实战】专栏目录
本篇主题:让小程序飞起来 - 性能优化实战指南
实战项目:心动恋聊 - AI 恋爱聊天助手
一、开篇:性能问题的发现
1.1 体验评分告警
txt
我:小程序提审时,体验评分只有 70 分。
主要问题:
- 代码包体积过大(2.1MB)
- 首屏渲染时间过长(2.8s)
- 图片资源未优化
能帮我系统性地优化一下吗?
1.2 AI 给出优化路线图
txt
AI:性能优化需要系统性进行,我给你一个完整的路线图:
📊 性能指标目标:
- 主包体积 < 2MB(当前 2.1MB)
- 首屏加载 < 1.5s(当前 2.8s)
- 体验评分 > 90 分(当前 70 分)
📋 优化路线:
1. 分包优化
- 主包只放核心页面
- 非核心功能放分包
- 配置分包预加载
2. 构建优化
- 使用 bundle-optimizer 插件
- 配置按需编译
- 移除未使用的依赖
3. 图片优化
- 压缩图片资源
- 使用 CDN 加载
- 实现懒加载
4. 渲染优化
- 减少首屏渲染节点
- 合理使用 computed
- 避免不必要的响应式
5. 网络优化
- 请求合并
- 数据缓存
- 预加载策略
我们按这个顺序来,先从分包开始。
二、分包优化:减少主包体积
2.1 我的需求
txt
我:主包体积超了,有哪些页面可以放到分包里?
2.2 AI 分析页面结构
txt
AI:分析你的 pages.json,建议这样分包:
📊 页面分析:
【主包页面】(必须放主包)
- pages/index/index → 首页,TabBar 页面
- pages/chase/chase → 帮你追,TabBar 页面
- pages/my/my → 我的,TabBar 页面
- pages/login/index → 登录页,高频访问
【可分包页面】
- pages/feedback/index → 反馈页,低频
- pages/plan-result/* → 计划结果,二级页面
- pages/plan-continue/* → 计划续期,二级页面
【建议分包结构】
subPackages/
├── agreement/ → 协议页面(用户协议、隐私政策)
├── vip/ → VIP 相关页面
└── planForm/ → 计划表单页面
📌 分包原则:
1. TabBar 页面必须在主包
2. 高频访问页面放主包
3. 低频、大体积页面放分包
4. 相关功能聚合到同一分包
2.3 分包配置
json
// pages.json
{
"pages": [
// 主包:核心 TabBar 页面
{ "path": "pages/index/index", "type": "home" },
{ "path": "pages/chase/chase" },
{ "path": "pages/my/my" },
{ "path": "pages/login/index" }
],
"subPackages": [
// 分包1:协议相关
{
"root": "subPackages/agreement",
"pages": [
{ "path": "pages/agreement/user" },
{ "path": "pages/agreement/privacy" },
{ "path": "pages/agreement/member" }
]
},
// 分包2:VIP 功能
{
"root": "subPackages/vip",
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "VIP会员"
}
}
]
},
// 分包3:计划表单
{
"root": "subPackages/planForm",
"pages": [
{
"path": "pages/index/index",
"style": {
"navigationStyle": "custom",
"navigationBarTitleText": "定制追求计划"
}
}
]
}
]
}
2.4 分包预加载
txt
我:用户从首页跳转 VIP 页面,能不能提前加载分包?
txt
AI:可以配置分包预加载,在用户访问主包页面时,
静默下载可能用到的分包:
json
// pages.json
{
"preloadRule": {
// 访问首页时,预加载 VIP 分包
"pages/index/index": {
"network": "all",
"packages": ["subPackages/vip"]
},
// 访问"我的"页面时,预加载协议分包
"pages/my/my": {
"network": "wifi",
"packages": ["subPackages/agreement"]
}
}
}
txt
AI:预加载配置说明:
📊 network 选项:
- "all":所有网络环境都预加载
- "wifi":仅 WiFi 环境预加载
📌 预加载策略:
1. VIP 页面用户可能随时访问,用 "all"
2. 协议页面不紧急,用 "wifi" 节省流量
3. 不要预加载太多分包,反而影响首屏
三、构建优化:使用 Bundle Optimizer
3.1 我的需求
txt
我:听说有个 bundle-optimizer 插件可以优化分包,
能帮我配置一下吗?
3.2 AI 配置优化插件
txt
AI:@uni-ku/bundle-optimizer 可以做以下优化:
🔧 插件功能:
1. 分包优化:自动分析依赖,减少重复
2. 异步跨包调用:模块按需加载
3. 异步组件引用:组件懒加载
我来帮你配置:
typescript
// vite.config.ts
import Optimization from '@uni-ku/bundle-optimizer';
export default ({ mode }) => {
const { UNI_PLATFORM } = process.env;
return defineConfig({
plugins: [
// ... 其他插件
// 分包优化插件(App 端不支持)
UNI_PLATFORM !== 'app' && UNI_PLATFORM !== 'app-plus' && Optimization({
enable: {
optimization: true, // 启用分包优化
'async-import': true, // 启用异步导入
'async-component': true, // 启用异步组件
},
dts: {
base: 'src/types', // 类型声明目录
},
logger: false, // 关闭日志
}),
],
});
};
3.3 我追问优化效果
txt
我:这个插件具体优化了什么?
txt
AI:主要优化三个方面:
📊 1. 分包依赖优化(optimization)
- 分析主包和分包的依赖关系
- 公共依赖提取到主包
- 分包独有依赖留在分包
【优化前】
主包:vue + lodash + dayjs + moment
分包A:moment(重复)
分包B:lodash(重复)
【优化后】
主包:vue + lodash + dayjs + moment(只打包一次)
分包A:无重复
分包B:无重复
📊 2. 异步导入(async-import)
// 优化前:同步导入,增加主包体积
import { heavyUtils } from '@/utils/heavy'
// 优化后:异步导入,按需加载
const { heavyUtils } = await import('@/utils/heavy')
📊 3. 异步组件(async-component)
// 优化前:组件全量打包
import HeavyComponent from '@/components/HeavyComponent.vue'
// 优化后:组件懒加载
const HeavyComponent = defineAsyncComponent(
() => import('@/components/HeavyComponent.vue')
)
📌 优化效果:
- 主包体积减少 15-30%
- 分包体积减少 10-20%
- 首屏加载时间减少 20-40%
四、图片优化:从源头减负
4.1 我的需求
txt
我:项目里有很多图片,如何优化?
4.2 AI 给出图片优化策略
txt
AI:图片优化分几个层面:
📊 图片优化策略:
1. 压缩图片(构建时)
- PNG/JPG 压缩
- 转 WebP 格式(兼容性好的平台)
2. 图片懒加载(运行时)
- 首屏图片立即加载
- 非首屏图片滚动时加载
3. CDN 加速(网络层)
- 静态资源上 CDN
- 图片裁剪服务
4. 占位图处理(体验层)
- 加载中显示骨架屏
- 加载失败显示默认图
我来给你演示实际代码:
4.3 图片组件优化
vue
<!-- src/components/ImageGrid.vue -->
<template>
<view class="grid grid-cols-5 gap-4">
<!-- 已选图片 -->
<view
v-for="(image, index) in images"
:key="image.id || index"
class="relative bg-gray-100 rounded-2 overflow-hidden aspect-[53/66]"
>
<image
:src="image.url || image.tempFilePath"
mode="aspectFill"
class="w-full h-full"
@load="onImageLoad"
@error="onImageError"
/>
<!-- 上传中遮罩 -->
<view
v-if="image.uploading"
class="absolute inset-0 bg-black/30 flex items-center justify-center"
>
<u-loading mode="circle" color="#ffffff" :size="28" />
</view>
</view>
</view>
</template>
<script setup lang="ts">
export interface ImageItem {
id: string;
url: string;
tempFilePath?: string;
uploading?: boolean;
}
interface Props {
images: ImageItem[];
maxCount?: number;
}
const props = withDefaults(defineProps<Props>(), {
maxCount: 5,
});
const emit = defineEmits<{
(e: 'imageLoad', event: any): void;
(e: 'imageError', event: any): void;
}>();
// 图片加载成功
const onImageLoad = (event: any) => {
emit('imageLoad', event);
};
// 图片加载失败
const onImageError = (event: any) => {
console.error('图片加载失败:', event);
emit('imageError', event);
};
</script>
4.4 我追问懒加载实现
txt
我:image 组件怎么实现懒加载?
txt
AI:微信小程序的 image 组件原生支持懒加载:
vue
<template>
<scroll-view scroll-y class="h-full">
<view v-for="item in list" :key="item.id" class="mb-4">
<!-- lazy-load 属性实现懒加载 -->
<image
:src="item.imageUrl"
mode="aspectFill"
lazy-load
class="w-full h-40"
@load="handleImageLoad(item.id)"
@error="handleImageError(item.id)"
/>
</view>
</scroll-view>
</template>
<script setup lang="ts">
// 跟踪图片加载状态
const loadedImages = ref<Set<string>>(new Set());
const failedImages = ref<Set<string>>(new Set());
const handleImageLoad = (id: string) => {
loadedImages.value.add(id);
};
const handleImageError = (id: string) => {
failedImages.value.add(id);
};
// 获取图片显示地址(失败时显示默认图)
const getImageSrc = (item: any) => {
if (failedImages.value.has(item.id)) {
return '/static/images/default.png'; // 默认图
}
return item.imageUrl;
};
</script>
txt
AI:lazy-load 的工作原理:
📊 懒加载行为:
1. 图片进入可视区域前不加载
2. 距离可视区域 150px 时开始加载
3. 加载完成触发 load 事件
📌 注意事项:
1. lazy-load 仅在 scroll-view 中生效
2. 首屏关键图片不要用懒加载
3. 建议配合占位图使用
五、渲染优化:减少不必要的计算
5.1 合理使用 computed
txt
我:页面数据经常需要格式化,怎么优化?
txt
AI:用 computed 缓存计算结果,避免重复计算:
typescript
// ❌ 错误:每次渲染都重新计算
const formatPrice = (price: number) => {
return (price / 100).toFixed(2);
};
// 模板中:{{ formatPrice(item.price) }}
// ✅ 正确:computed 缓存结果
const formattedList = computed(() =>
list.value.map(item => ({
...item,
formattedPrice: (item.price / 100).toFixed(2),
formattedDate: formatDate(item.createTime),
}))
);
// 模板中:{{ item.formattedPrice }}
5.2 v-show vs v-if 的选择
txt
AI:根据切换频率选择:
vue
<!-- 频繁切换:用 v-show(只是 CSS 隐藏) -->
<view v-show="isExpanded" class="detail-panel">
{{ detailContent }}
</view>
<!-- 条件渲染:用 v-if(销毁/重建 DOM) -->
<LoginModal v-if="showLogin" @close="showLogin = false" />
<!-- 多分支条件:用 v-if + v-else-if -->
<LoadingState v-if="loading" />
<EmptyState v-else-if="list.length === 0" />
<ContentList v-else :list="list" />
txt
AI:选择原则:
📊 使用 v-show 的场景:
- Tab 切换
- 折叠面板
- 下拉菜单
→ 切换频繁,保留 DOM 状态
📊 使用 v-if 的场景:
- 权限控制
- 弹窗组件
- 条件渲染
→ 切换少,节省初始渲染成本
5.3 避免响应式陷阱
txt
我:有些静态配置数据,需要 ref 包装吗?
txt
AI:静态数据不需要响应式:
typescript
// ❌ 不必要的响应式
const menuItems = ref([
{ icon: 'home', text: '首页', path: '/pages/index' },
{ icon: 'user', text: '我的', path: '/pages/my' },
]);
// ✅ 静态数据直接定义
const menuItems = [
{ icon: 'home', text: '首页', path: '/pages/index' },
{ icon: 'user', text: '我的', path: '/pages/my' },
];
// ✅ 需要响应式的数据才用 ref
const selectedIndex = ref(0);
const userInfo = ref<UserInfo | null>(null);
txt
AI:响应式的开销:
📊 ref/reactive 的成本:
1. 创建 Proxy 代理对象
2. 依赖收集(track)
3. 触发更新(trigger)
📌 优化建议:
- 静态配置:普通对象/数组
- 列表数据:ref([])
- 表单状态:ref() 或 reactive()
- 大对象频繁更新:shallowRef()
5.4 列表渲染优化
typescript
// ❌ 错误:使用 index 作为 key
<view v-for="(item, index) in list" :key="index">
// ✅ 正确:使用唯一 ID 作为 key
<view v-for="item in list" :key="item.id">
// ✅ 复合 key(当 id 不唯一时)
<view v-for="(item, index) in list" :key="`${item.type}-${item.id}`">
六、网络优化:请求合并与缓存
6.1 请求合并
txt
我:首页有 5 个接口同时请求,能不能优化?
txt
AI:有几种优化策略:
typescript
// 策略1:接口合并(需要后端配合)
// 一个接口返回多种数据
const fetchHomeData = async () => {
const res = await httpGet<{
userInfo: UserInfo;
vipInfo: VipInfo;
replyQuota: QuotaInfo;
}>('/api/home/init');
return res.data;
};
// 策略2:并行请求(前端优化)
const fetchHomeDataParallel = async () => {
const [userRes, vipRes, quotaRes] = await Promise.all([
getUserInfo(),
getVipInfo(),
getReplyQuota(),
]);
return {
userInfo: userRes.data,
vipInfo: vipRes.data,
quota: quotaRes.data,
};
};
// 策略3:按优先级加载
const fetchHomeDataPriority = async () => {
// 首屏必须数据:立即加载
const essential = await httpGet('/api/home/essential');
renderEssential(essential.data);
// 次要数据:延迟加载
setTimeout(async () => {
const extra = await httpGet('/api/home/extra');
renderExtra(extra.data);
}, 100);
};
6.2 数据缓存
typescript
// 本地缓存工具
const cache = {
set(key: string, data: any, ttl: number = 5 * 60 * 1000) {
const item = {
data,
expireAt: Date.now() + ttl,
};
uni.setStorageSync(key, JSON.stringify(item));
},
get<T>(key: string): T | null {
const raw = uni.getStorageSync(key);
if (!raw) return null;
try {
const item = JSON.parse(raw);
if (Date.now() > item.expireAt) {
uni.removeStorageSync(key);
return null;
}
return item.data as T;
} catch {
return null;
}
},
};
// 带缓存的请求
const getUserInfoCached = async () => {
const cacheKey = 'user_info';
// 先读缓存
const cached = cache.get<UserInfo>(cacheKey);
if (cached) return cached;
// 缓存未命中,请求接口
const res = await getUserInfo();
cache.set(cacheKey, res.data, 10 * 60 * 1000); // 缓存 10 分钟
return res.data;
};
七、Loading 状态:提升感知性能
7.1 加载指示器组件
vue
<!-- src/components/LoadingIndicator.vue -->
<template>
<view class="loading-wrapper">
<view class="dot-group">
<view v-for="dot in 3" :key="dot" class="loading-dot" />
</view>
<text v-if="text" class="loading-text">{{ text }}</text>
</view>
</template>
<script setup lang="ts">
defineProps<{
text?: string;
}>();
</script>
<style scoped>
.loading-wrapper {
display: flex;
align-items: center;
gap: 16rpx;
color: #706df7;
}
.dot-group {
display: flex;
gap: 6rpx;
}
.loading-dot {
width: 12rpx;
height: 12rpx;
border-radius: 9999px;
background-color: #706df7;
animation: dotPulse 1.2s ease-in-out infinite;
}
.loading-dot:nth-child(2) { animation-delay: 0.2s; }
.loading-dot:nth-child(3) { animation-delay: 0.4s; }
@keyframes dotPulse {
0%, 80%, 100% {
transform: scale(0.6);
opacity: 0.2;
}
40% {
transform: scale(1);
opacity: 1;
}
}
</style>
7.2 骨架屏方案
txt
我:首屏加载时能不能显示骨架屏?
txt
AI:骨架屏可以提升用户感知速度,
让用户知道"内容正在加载"而不是"页面卡住了":
vue
<template>
<!-- 骨架屏 -->
<view v-if="loading" class="skeleton">
<view class="skeleton-header">
<view class="skeleton-avatar animate-pulse" />
<view class="skeleton-info">
<view class="skeleton-line w-24 animate-pulse" />
<view class="skeleton-line w-16 animate-pulse" />
</view>
</view>
<view class="skeleton-content">
<view v-for="i in 3" :key="i" class="skeleton-card animate-pulse" />
</view>
</view>
<!-- 实际内容 -->
<view v-else class="content">
<UserHeader :user="userInfo" />
<ReplyList :list="replies" />
</view>
</template>
<style scoped lang="scss">
.skeleton-avatar {
@apply w-16 h-16 rounded-full bg-gray-200;
}
.skeleton-line {
@apply h-4 rounded bg-gray-200 mb-2;
}
.skeleton-card {
@apply h-24 rounded-xl bg-gray-200 mb-4;
}
.animate-pulse {
animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
</style>
八、性能优化最佳实践
8.1 优化清单
| 优化项 | 措施 | 效果 |
|---|---|---|
| 分包 | 非核心页面放分包 | 主包体积 -30% |
| 预加载 | 配置 preloadRule | 分包加载 -50% |
| 构建 | bundle-optimizer | 重复代码 -20% |
| 图片 | 懒加载 + CDN | 首屏加载 -40% |
| 渲染 | computed + v-show | 渲染时间 -30% |
| 网络 | 请求合并 + 缓存 | 请求数 -60% |
8.2 性能监控
typescript
// 页面加载耗时监控
const trackPageLoad = () => {
const startTime = Date.now();
onReady(() => {
const loadTime = Date.now() - startTime;
// 上报性能数据
uni.reportAnalytics('page_load', {
page: getCurrentPages().pop()?.route,
loadTime,
});
// 超过阈值告警
if (loadTime > 2000) {
console.warn(`页面加载过慢: ${loadTime}ms`);
}
});
};
8.3 持续优化
txt
AI:性能优化不是一次性工作,需要持续关注:
📊 优化周期:
1. 开发期:遵循性能规范
2. 提审前:体验评分检查
3. 上线后:监控关键指标
4. 定期:分析慢请求和慢渲染
📌 核心指标:
- 首屏加载时间 < 1.5s
- 页面切换时间 < 300ms
- 体验评分 > 90 分
- 内存占用 < 100MB
九、总结:性能优化方法论
9.1 优化顺序
txt
📊 优先级排序:
1. 分包优化(收益最大,成本最低)
2. 图片优化(效果明显,易于实施)
3. 构建优化(一次配置,长期受益)
4. 渲染优化(需要理解原理)
5. 网络优化(可能需要后端配合)
9.2 本文优化清单
| 优化项 | 类型 | 核心技术 | 效果 |
|---|---|---|---|
| 分包配置 | 构建 | subPackages + preloadRule | 主包体积 -30% |
| Bundle Optimizer | 构建 | 异步导入、依赖优化 | 代码体积 -20% |
| 图片懒加载 | 渲染 | lazy-load + CDN | 首屏加载 -40% |
| computed 缓存 | 渲染 | 避免重复计算 | 渲染时间 -30% |
| 请求缓存 | 网络 | 本地存储 + TTL | 请求数 -60% |
| 骨架屏 | 体验 | 感知优化 | 用户体验 +50% |
9.3 下一篇预告
《【AI 编程实战】第 12 篇:项目总结与 AI 协作心得》
最后一篇是整个系列的总结:
- 项目从 0 到 1 的完整回顾
- AI 辅助开发的效率提升
- 与 AI 协作的最佳实践
性能优化不是"锦上添花",而是用户体验的基础保障。
通过和 AI 对话,系统性地分析问题、制定方案、逐一优化。
如果这篇文章对你有帮助,请点赞、收藏、转发!