微信小程序电商实战 09:性能优化——分包加载、虚拟列表与图片懒加载

微信小程序电商实战 01 技术选型 | 02 脚手架搭建 | 03 NutUI 配置 | 04 云开发环境 | 05 商品模块 | 06 购物车与订单 | 07 微信支付 | 08 用户中心 | 09 性能优化 ← 本文 | 10 上线部署


# 小程序电商性能优化 ## 包体积优化 ### 分包加载 - 主包 ≤ 2MB(tabBar页面) - 子包:商品详情/订单/用户 ### NutUI 按需加载 - babel-plugin-import - 只打入用到的组件 ### 图片不入包 - 云存储 URL,不放 assets ## 列表性能 ### 虚拟列表 - @tarojs/components VirtualList - 商品列表 100+ 条 ### 分页 + 无限滚动 - 每页 10 条 - scrolltolower 触发 ## 图片优化 ### 懒加载(lazy-load) - nut-image 属性 ### 格式优化 - 云存储支持 WebP - 缩略图参数 ### 骨架屏 - 加载期间占位 ## 启动优化 ### 数据预拉取 - wx.setBackgroundFetchToken ### 周期性更新 - 首页分类数据缓存 ## 渲染优化 ### setData 最小化 - 避免整个列表 setData ### 组件化拆分 - 减少单页节点数

一、分包加载(最重要)

微信小程序主包限制 2MB,整个包限制 20MB。电商项目 NutUI + Taro 运行时已接近限制,必须做分包。

分包策略

主包(≤2MB):
  pages/index/       # 首页(tabBar)
  pages/cart/        # 购物车(tabBar)
  pages/order/list   # 订单列表(tabBar)
  pages/user/index   # 用户中心(tabBar)

子包 A — 商品:
  pages/product/     # 商品详情
  pages/index/list   # 商品列表

子包 B — 交易:
  pages/order/confirm   # 订单确认
  pages/order/pay       # 支付页
  pages/order/result    # 支付结果

子包 C — 用户管理:
  pages/user/address
  pages/user/address-edit
  pages/user/about

在 app.config.ts 配置分包

// src/app.config.ts
export default defineAppConfig({
  pages: [
    'pages/index/index',
    'pages/cart/index',
    'pages/order/list',
    'pages/user/index',
  ],
  subPackages: [
    {
      root: 'packageProduct',
      pages: [
        'pages/product/index',
        'pages/index/list',
      ],
    },
    {
      root: 'packageOrder',
      pages: [
        'pages/order/confirm',
        'pages/order/pay',
        'pages/order/result',
      ],
    },
    {
      root: 'packageUser',
      pages: [
        'pages/user/address',
        'pages/user/address-edit',
        'pages/user/about',
      ],
    },
  ],
  preloadRule: {
    'pages/index/index': {
      network: 'all',
      packages: ['packageProduct'],  // 进首页时预加载商品子包
    },
  },
})

跳转子包页面时路径前加子包根目录:

// 跳转到子包内的页面
Taro.navigateTo({ url: '/packageProduct/pages/product/index?id=xxx' })

二、虚拟列表(商品列表 100+ 条)

当商品列表超过 100 条时,直接渲染会导致内存占用飙升、滚动卡顿。使用 Taro 内置的 VirtualList 只渲染可视区域内的节点:

<!-- 商品虚拟列表 -->
<template>
  <virtual-list
    class="product-virtual-list"
    :height="windowHeight"
    :item-count="products.length"
    :item-size="240"
    :render-count="10"
  >
    <template #default="{ index, style }">
      <view :style="style">
        <ProductCard :product="products[index]" @open-sku="openSkuPanel" />
      </view>
    </template>
  </virtual-list>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import Taro from '@tarojs/taro'
import { VirtualList } from '@tarojs/components'
import ProductCard from '@/components/ProductCard.vue'

const windowHeight = Taro.getSystemInfoSync().windowHeight
</script>

适用场景:商品列表超过 80 条时启用。50 条以内的普通 v-for 性能已足够,不必过早优化。


三、图片优化

1. 云存储图片格式转换

云存储 URL 支持通过参数获取 WebP 格式和缩略图,大幅减小图片体积:

// src/utils/image.ts

// 商品列表缩略图:转 WebP,400px 宽
export function getThumbnail(cloudUrl: string, width = 400): string {
  if (!cloudUrl.startsWith('cloud://')) return cloudUrl
  // 腾讯云 COS 图片处理参数
  return `${cloudUrl}?imageView2/2/w/${width}/format/webp/q/80`
}

// 商品详情大图:原始尺寸但 WebP
export function getDetailImage(cloudUrl: string): string {
  if (!cloudUrl.startsWith('cloud://')) return cloudUrl
  return `${cloudUrl}?imageMogr2/format/webp/quality/85`
}

2. 懒加载 + 骨架屏

<!-- 带骨架屏的商品卡片 -->
<template>
  <view class="product-card">
    <view v-if="!imageLoaded" class="skeleton-image" />
    <nut-image
      :src="getThumbnail(product.coverImage)"
      width="100%"
      height="180px"
      fit="cover"
      lazy-load
      @load="imageLoaded = true"
    />
    <!-- ... -->
  </view>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { getThumbnail } from '@/utils/image'
const imageLoaded = ref(false)
</script>

<style lang="scss">
.skeleton-image {
  width: 100%;
  height: 180px;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 400% 100%;
  animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
  0% { background-position: 100% 0; }
  100% { background-position: -100% 0; }
}
</style>

四、setData 优化

Taro 底层会把数据变化转为小程序的 setData,频繁的大量 setData 是性能杀手。

列表更新时只追加,不替换整个数组

// ❌ 错误:每次加载都替换整个列表
products.value = [...products.value, ...newItems]  // 触发全量 setData

// ✅ 正确:使用 push 追加(Taro 会生成差量 setData)
products.value.push(...newItems)

高频更新场景(如搜索输入)加防抖

import { debounce } from 'lodash-es'

const search = debounce(async (keyword: string) => {
  const res = await searchProducts(keyword)
  products.value = res.list
}, 300)

五、首屏数据预加载

利用微信的「数据预拉取」在小程序冷启动时提前请求数据:

// app.ts — onLaunch 阶段并行初始化
onLaunch() {
  initCloud()

  // 并行请求首屏数据,不阻塞 UI 渲染
  Promise.all([
    userStore.silentLogin(),
    prefetchHomeData(),
  ])
}

async function prefetchHomeData() {
  // 首页分类和推荐数据写入本地缓存
  const [cats, products] = await Promise.all([
    getCategories(),
    getProductList({ page: 1, pageSize: 10 }),
  ])
  Taro.setStorageSync('home_categories', cats)
  Taro.setStorageSync('home_products', products)
  Taro.setStorageSync('home_cache_time', Date.now())
}

首页读取时优先用缓存,5 分钟内不重复请求:

onMounted(async () => {
  const cacheTime = Taro.getStorageSync('home_cache_time')
  const isFresh = Date.now() - cacheTime < 5 * 60 * 1000

  if (isFresh) {
    categories.value = Taro.getStorageSync('home_categories')
    products.value = Taro.getStorageSync('home_products')?.list ?? []
  } else {
    await loadHomeData()
  }
})

微信小程序电商实战 01 技术选型 | 02 脚手架搭建 | 03 NutUI 配置 | 04 云开发环境 | 05 商品模块 | 06 购物车与订单 | 07 微信支付 | 08 用户中心 | 09 性能优化 ← 本文 | 10 上线部署

实操清单

  • app.config.ts 配置分包:主包只留 4 个 tabBar 页面,其余移入子包
  • pnpm build:weapp 后查看 dist/ 目录,验证主包大小 ≤ 2MB
  • 配置 preloadRule,进首页时预加载商品子包
  • 确认 babel.config.js 的按需加载配置已生效(对比开启前后包体积)
  • 在商品数量超过 50 条的列表页引入 VirtualList,对比滚动流畅度
  • 实现 getThumbnail() 工具函数,对比原始图和转 WebP 的加载时间
  • 为商品卡片图片加入骨架屏占位动画
  • 将列表追加从 value = [...old, ...new] 改为 value.push(...new)
  • 对搜索输入加 300ms 防抖
  • 实现首页数据预拉取 + 5 分钟缓存,验证刷新首页时不重复请求接口