Skip to content

Performance Optimization ​

Performance optimization is crucial for creating smooth and responsive mini programs. This guide covers key optimization strategies, techniques, and best practices.

Startup Performance ​

App Launch Optimization ​

javascript
// app.js - Optimize app startup
App({
  onLaunch(options) {
    // Minimize work in onLaunch
    this.initEssentials()
    
    // Defer non-critical initialization
    setTimeout(() => {
      this.initNonCritical()
    }, 100)
  },
  
  initEssentials() {
    // Only essential initialization
    this.globalData = {
      userInfo: null,
      systemInfo: wx.getSystemInfoSync()
    }
    
    // Check login status
    this.checkLoginStatus()
  },
  
  initNonCritical() {
    // Non-critical initialization
    this.initAnalytics()
    this.preloadResources()
    this.setupGlobalListeners()
  },
  
  checkLoginStatus() {
    const token = wx.getStorageSync('token')
    if (token) {
      // Validate token in background
      this.validateToken(token)
    }
  },
  
  async validateToken(token) {
    try {
      // Validate token without blocking UI
      const isValid = await this.api.validateToken(token)
      if (!isValid) {
        wx.removeStorageSync('token')
      }
    } catch (error) {
      console.error('Token validation failed:', error)
    }
  },
  
  preloadResources() {
    // Preload critical images
    const criticalImages = [
      '/images/logo.png',
      '/images/default-avatar.png'
    ]
    
    criticalImages.forEach(src => {
      wx.getImageInfo({ src })
    })
  }
})

Page Load Optimization ​

javascript
// Optimize page loading
Page({
  data: {
    loading: true,
    items: [],
    hasMore: true
  },
  
  onLoad(options) {
    // Start loading immediately
    this.loadInitialData()
    
    // Set up page optimizations
    this.setupOptimizations()
  },
  
  async loadInitialData() {
    try {
      // Load critical data first
      const criticalData = await this.loadCriticalData()
      
      this.setData({
        ...criticalData,
        loading: false
      })
      
      // Load non-critical data after render
      setTimeout(() => {
        this.loadNonCriticalData()
      }, 50)
      
    } catch (error) {
      console.error('Failed to load initial data:', error)
      this.setData({ loading: false })
    }
  },
  
  async loadCriticalData() {
    // Load only essential data for first render
    const [basicInfo, firstPageItems] = await Promise.all([
      this.api.getBasicInfo(),
      this.api.getItems({ page: 1, limit: 10 })
    ])
    
    return {
      basicInfo,
      items: firstPageItems
    }
  },
  
  async loadNonCriticalData() {
    // Load additional data that's not immediately visible
    try {
      const [userPreferences, recommendations] = await Promise.all([
        this.api.getUserPreferences(),
        this.api.getRecommendations()
      ])
      
      this.setData({
        userPreferences,
        recommendations
      })
    } catch (error) {
      console.error('Failed to load non-critical data:', error)
    }
  },
  
  setupOptimizations() {
    // Enable pull-to-refresh
    wx.enablePullDownRefresh()
    
    // Set up intersection observer for lazy loading
    this.setupLazyLoading()
  }
})

Rendering Performance ​

Efficient Data Updates ​

javascript
// Optimize setData usage
Page({
  data: {
    users: [],
    selectedIds: [],
    filters: {
      status: 'active',
      category: 'all'
    }
  },
  
  // ❌ Bad: Multiple setData calls
  updateUsersBad(newUsers) {
    this.setData({ users: newUsers })
    this.setData({ loading: false })
    this.setData({ lastUpdated: Date.now() })
  },
  
  // ✅ Good: Single setData call
  updateUsersGood(newUsers) {
    this.setData({
      users: newUsers,
      loading: false,
      lastUpdated: Date.now()
    })
  },
  
  // ✅ Good: Partial updates for large datasets
  updateSingleUser(userId, userData) {
    const userIndex = this.data.users.findIndex(u => u.id === userId)
    if (userIndex !== -1) {
      this.setData({
        [`users[${userIndex}]`]: { ...this.data.users[userIndex], ...userData }
      })
    }
  },
  
  // ✅ Good: Batch updates
  batchUpdateUsers(updates) {
    const setDataObject = {}
    
    updates.forEach(({ userId, data }) => {
      const userIndex = this.data.users.findIndex(u => u.id === userId)
      if (userIndex !== -1) {
        Object.keys(data).forEach(key => {
          setDataObject[`users[${userIndex}].${key}`] = data[key]
        })
      }
    })
    
    this.setData(setDataObject)
  },
  
  // ✅ Good: Optimize filter updates
  updateFilter(key, value) {
    this.setData({
      [`filters.${key}`]: value
    })
    
    // Debounce filter application
    this.debouncedApplyFilters()
  },
  
  onLoad() {
    // Create debounced function
    this.debouncedApplyFilters = this.debounce(this.applyFilters, 300)
  },
  
  debounce(func, wait) {
    let timeout
    return function executedFunction(...args) {
      const later = () => {
        clearTimeout(timeout)
        func.apply(this, args)
      }
      clearTimeout(timeout)
      timeout = setTimeout(later, wait)
    }
  }
})

Virtual List Implementation ​

javascript
// Virtual list for large datasets
Component({
  properties: {
    items: {
      type: Array,
      value: []
    },
    itemHeight: {
      type: Number,
      value: 100
    },
    visibleCount: {
      type: Number,
      value: 10
    }
  },
  
  data: {
    scrollTop: 0,
    visibleItems: [],
    startIndex: 0,
    endIndex: 0,
    totalHeight: 0
  },
  
  observers: {
    'items, itemHeight, visibleCount': function(items, itemHeight, visibleCount) {
      this.updateVirtualList()
    }
  },
  
  methods: {
    updateVirtualList() {
      const { items, itemHeight, visibleCount } = this.properties
      const { scrollTop } = this.data
      
      const startIndex = Math.floor(scrollTop / itemHeight)
      const endIndex = Math.min(startIndex + visibleCount + 2, items.length)
      
      const visibleItems = items.slice(startIndex, endIndex).map((item, index) => ({
        ...item,
        _index: startIndex + index,
        _top: (startIndex + index) * itemHeight
      }))
      
      this.setData({
        visibleItems,
        startIndex,
        endIndex,
        totalHeight: items.length * itemHeight
      })
    },
    
    onScroll(e) {
      const scrollTop = e.detail.scrollTop
      
      this.setData({ scrollTop })
      
      // Throttle virtual list updates
      if (!this.scrollTimer) {
        this.scrollTimer = setTimeout(() => {
          this.updateVirtualList()
          this.scrollTimer = null
        }, 16) // ~60fps
      }
    }
  }
})
html
<!-- Virtual list template -->
<scroll-view 
  class="virtual-list"
  scroll-y
  scroll-top="{{scrollTop}}"
  bindscroll="onScroll"
  style="height: 500px;"
>
  <view class="virtual-container" style="height: {{totalHeight}}px;">
    <view 
      wx:for="{{visibleItems}}" 
      wx:key="_index"
      class="virtual-item"
      style="position: absolute; top: {{item._top}}px; height: {{itemHeight}}px;"
    >
      <!-- Item content -->
      <text>{{item.name}}</text>
    </view>
  </view>
</scroll-view>

Image Optimization ​

Lazy Loading Images ​

javascript
// Image lazy loading component
Component({
  properties: {
    src: String,
    placeholder: {
      type: String,
      value: '/images/placeholder.png'
    },
    threshold: {
      type: Number,
      value: 100
    }
  },
  
  data: {
    loaded: false,
    currentSrc: ''
  },
  
  lifetimes: {
    attached() {
      this.setData({
        currentSrc: this.properties.placeholder
      })
      
      this.createIntersectionObserver()
    },
    
    detached() {
      if (this.observer) {
        this.observer.disconnect()
      }
    }
  },
  
  methods: {
    createIntersectionObserver() {
      this.observer = this.createIntersectionObserver({
        rootMargin: `${this.properties.threshold}px`
      })
      
      this.observer.relativeToViewport().observe('.lazy-image', (res) => {
        if (res.intersectionRatio > 0 && !this.data.loaded) {
          this.loadImage()
        }
      })
    },
    
    loadImage() {
      const { src } = this.properties
      
      if (!src) return
      
      wx.getImageInfo({
        src,
        success: () => {
          this.setData({
            currentSrc: src,
            loaded: true
          })
          
          // Disconnect observer after loading
          if (this.observer) {
            this.observer.disconnect()
          }
        },
        fail: (error) => {
          console.error('Failed to load image:', error)
        }
      })
    },
    
    onImageLoad() {
      this.triggerEvent('load')
    },
    
    onImageError() {
      this.triggerEvent('error')
    }
  }
})
html
<!-- Lazy image template -->
<image 
  class="lazy-image"
  src="{{currentSrc}}"
  mode="aspectFill"
  lazy-load
  bindload="onImageLoad"
  binderror="onImageError"
/>

Image Compression and Caching ​

javascript
// Image optimization utility
class ImageOptimizer {
  constructor() {
    this.cache = new Map()
    this.maxCacheSize = 50
  }
  
  async optimizeImage(src, options = {}) {
    const {
      quality = 80,
      width,
      height,
      format = 'jpg'
    } = options
    
    const cacheKey = `${src}_${quality}_${width}_${height}_${format}`
    
    // Check cache first
    if (this.cache.has(cacheKey)) {
      return this.cache.get(cacheKey)
    }
    
    try {
      // Get image info
      const imageInfo = await this.getImageInfo(src)
      
      // Calculate optimal dimensions
      const optimizedDimensions = this.calculateOptimalDimensions(
        imageInfo.width,
        imageInfo.height,
        width,
        height
      )
      
      // Compress if needed
      let optimizedSrc = src
      if (this.shouldCompress(imageInfo, optimizedDimensions, quality)) {
        optimizedSrc = await this.compressImage(src, {
          ...optimizedDimensions,
          quality,
          format
        })
      }
      
      // Cache result
      this.addToCache(cacheKey, optimizedSrc)
      
      return optimizedSrc
      
    } catch (error) {
      console.error('Image optimization failed:', error)
      return src // Return original on error
    }
  }
  
  getImageInfo(src) {
    return new Promise((resolve, reject) => {
      wx.getImageInfo({
        src,
        success: resolve,
        fail: reject
      })
    })
  }
  
  calculateOptimalDimensions(originalWidth, originalHeight, targetWidth, targetHeight) {
    if (!targetWidth && !targetHeight) {
      return { width: originalWidth, height: originalHeight }
    }
    
    const aspectRatio = originalWidth / originalHeight
    
    if (targetWidth && !targetHeight) {
      return {
        width: targetWidth,
        height: Math.round(targetWidth / aspectRatio)
      }
    }
    
    if (!targetWidth && targetHeight) {
      return {
        width: Math.round(targetHeight * aspectRatio),
        height: targetHeight
      }
    }
    
    return { width: targetWidth, height: targetHeight }
  }
  
  shouldCompress(imageInfo, targetDimensions, quality) {
    const { width: originalWidth, height: originalHeight } = imageInfo
    const { width: targetWidth, height: targetHeight } = targetDimensions
    
    // Compress if dimensions are significantly different or quality is low
    return (
      originalWidth > targetWidth * 1.5 ||
      originalHeight > targetHeight * 1.5 ||
      quality < 90
    )
  }
  
  async compressImage(src, options) {
    return new Promise((resolve, reject) => {
      const canvas = wx.createCanvasContext('imageCanvas')
      
      // This is a simplified example
      // In practice, you might use a third-party service or native compression
      wx.compressImage({
        src,
        quality: options.quality,
        success: (res) => resolve(res.tempFilePath),
        fail: reject
      })
    })
  }
  
  addToCache(key, value) {
    if (this.cache.size >= this.maxCacheSize) {
      // Remove oldest entry
      const firstKey = this.cache.keys().next().value
      this.cache.delete(firstKey)
    }
    
    this.cache.set(key, value)
  }
  
  clearCache() {
    this.cache.clear()
  }
}

const imageOptimizer = new ImageOptimizer()

// Usage
Page({
  async loadOptimizedImage(src) {
    try {
      const optimizedSrc = await imageOptimizer.optimizeImage(src, {
        width: 300,
        height: 200,
        quality: 80
      })
      
      this.setData({
        imageSrc: optimizedSrc
      })
    } catch (error) {
      console.error('Failed to optimize image:', error)
    }
  }
})

Memory Management ​

Component Lifecycle Optimization ​

javascript
// Optimized component with proper cleanup
Component({
  data: {
    timer: null,
    observers: [],
    eventListeners: []
  },
  
  lifetimes: {
    attached() {
      this.initComponent()
    },
    
    detached() {
      this.cleanup()
    }
  },
  
  pageLifetimes: {
    show() {
      this.onPageShow()
    },
    
    hide() {
      this.onPageHide()
    }
  },
  
  methods: {
    initComponent() {
      // Set up timers
      this.setupTimer()
      
      // Set up observers
      this.setupObservers()
      
      // Set up event listeners
      this.setupEventListeners()
    },
    
    setupTimer() {
      this.data.timer = setInterval(() => {
        this.updateData()
      }, 5000)
    },
    
    setupObservers() {
      // Intersection observer
      const intersectionObserver = this.createIntersectionObserver()
      intersectionObserver.relativeToViewport().observe('.target', (res) => {
        // Handle intersection
      })
      
      this.data.observers.push(intersectionObserver)
    },
    
    setupEventListeners() {
      // Global event listeners
      const networkListener = (res) => {
        this.handleNetworkChange(res)
      }
      
      wx.onNetworkStatusChange(networkListener)
      this.data.eventListeners.push({
        type: 'network',
        listener: networkListener,
        off: wx.offNetworkStatusChange
      })
    },
    
    onPageShow() {
      // Resume timers and observers when page is visible
      if (!this.data.timer) {
        this.setupTimer()
      }
    },
    
    onPageHide() {
      // Pause timers when page is hidden
      if (this.data.timer) {
        clearInterval(this.data.timer)
        this.data.timer = null
      }
    },
    
    cleanup() {
      // Clear timers
      if (this.data.timer) {
        clearInterval(this.data.timer)
        this.data.timer = null
      }
      
      // Disconnect observers
      this.data.observers.forEach(observer => {
        observer.disconnect()
      })
      this.data.observers = []
      
      // Remove event listeners
      this.data.eventListeners.forEach(({ listener, off }) => {
        off(listener)
      })
      this.data.eventListeners = []
    }
  }
})

Memory Leak Prevention ​

javascript
// Memory leak prevention utilities
class MemoryManager {
  constructor() {
    this.refs = new WeakMap()
    this.timers = new Set()
    this.observers = new Set()
  }
  
  // Safe timer management
  setTimeout(callback, delay) {
    const timer = setTimeout(() => {
      callback()
      this.timers.delete(timer)
    }, delay)
    
    this.timers.add(timer)
    return timer
  }
  
  setInterval(callback, interval) {
    const timer = setInterval(callback, interval)
    this.timers.add(timer)
    return timer
  }
  
  clearTimer(timer) {
    clearTimeout(timer)
    clearInterval(timer)
    this.timers.delete(timer)
  }
  
  // Safe observer management
  createIntersectionObserver(component, options) {
    const observer = component.createIntersectionObserver(options)
    this.observers.add(observer)
    return observer
  }
  
  disconnectObserver(observer) {
    observer.disconnect()
    this.observers.delete(observer)
  }
  
  // Cleanup all resources
  cleanup() {
    // Clear all timers
    this.timers.forEach(timer => {
      clearTimeout(timer)
      clearInterval(timer)
    })
    this.timers.clear()
    
    // Disconnect all observers
    this.observers.forEach(observer => {
      observer.disconnect()
    })
    this.observers.clear()
  }
}

// Usage in components
Component({
  lifetimes: {
    attached() {
      this.memoryManager = new MemoryManager()
      this.initComponent()
    },
    
    detached() {
      if (this.memoryManager) {
        this.memoryManager.cleanup()
      }
    }
  },
  
  methods: {
    initComponent() {
      // Use memory manager for timers
      this.updateTimer = this.memoryManager.setInterval(() => {
        this.updateData()
      }, 1000)
      
      // Use memory manager for observers
      this.intersectionObserver = this.memoryManager.createIntersectionObserver(this)
    }
  }
})

Network Optimization ​

Request Optimization ​

javascript
// Optimized request manager
class RequestOptimizer {
  constructor() {
    this.cache = new Map()
    this.pendingRequests = new Map()
    this.requestQueue = []
    this.maxConcurrent = 6
    this.activeRequests = 0
  }
  
  async request(url, options = {}) {
    const cacheKey = this.generateCacheKey(url, options)
    
    // Check cache first
    if (options.useCache !== false) {
      const cached = this.getFromCache(cacheKey)
      if (cached) {
        return cached
      }
    }
    
    // Check for duplicate requests
    if (this.pendingRequests.has(cacheKey)) {
      return this.pendingRequests.get(cacheKey)
    }
    
    // Create request promise
    const requestPromise = this.executeRequest(url, options)
    this.pendingRequests.set(cacheKey, requestPromise)
    
    try {
      const result = await requestPromise
      
      // Cache successful results
      if (options.useCache !== false) {
        this.addToCache(cacheKey, result)
      }
      
      return result
    } finally {
      this.pendingRequests.delete(cacheKey)
    }
  }
  
  async executeRequest(url, options) {
    // Queue request if too many concurrent requests
    if (this.activeRequests >= this.maxConcurrent) {
      await this.waitForSlot()
    }
    
    this.activeRequests++
    
    try {
      return await this.makeRequest(url, options)
    } finally {
      this.activeRequests--
      this.processQueue()
    }
  }
  
  makeRequest(url, options) {
    return new Promise((resolve, reject) => {
      wx.request({
        url,
        ...options,
        success: (res) => {
          if (res.statusCode >= 200 && res.statusCode < 300) {
            resolve(res.data)
          } else {
            reject(new Error(`Request failed: ${res.statusCode}`))
          }
        },
        fail: reject
      })
    })
  }
  
  waitForSlot() {
    return new Promise(resolve => {
      this.requestQueue.push(resolve)
    })
  }
  
  processQueue() {
    if (this.requestQueue.length > 0 && this.activeRequests < this.maxConcurrent) {
      const resolve = this.requestQueue.shift()
      resolve()
    }
  }
  
  generateCacheKey(url, options) {
    const { data, method = 'GET' } = options
    return `${method}:${url}:${JSON.stringify(data || {})}`
  }
  
  getFromCache(key) {
    const cached = this.cache.get(key)
    if (cached && Date.now() - cached.timestamp < 300000) { // 5 minutes
      return cached.data
    }
    return null
  }
  
  addToCache(key, data) {
    this.cache.set(key, {
      data,
      timestamp: Date.now()
    })
    
    // Limit cache size
    if (this.cache.size > 100) {
      const firstKey = this.cache.keys().next().value
      this.cache.delete(firstKey)
    }
  }
}

const requestOptimizer = new RequestOptimizer()

// Usage
Page({
  async loadData() {
    try {
      const [users, posts] = await Promise.all([
        requestOptimizer.request('/api/users'),
        requestOptimizer.request('/api/posts')
      ])
      
      this.setData({ users, posts })
    } catch (error) {
      console.error('Failed to load data:', error)
    }
  }
})

Performance Monitoring ​

Performance Metrics Collection ​

javascript
// Performance monitor
class PerformanceMonitor {
  constructor() {
    this.metrics = []
    this.observers = []
    this.startTime = Date.now()
  }
  
  // Measure page load time
  measurePageLoad(pageName) {
    const loadTime = Date.now() - this.startTime
    
    this.recordMetric({
      type: 'page_load',
      page: pageName,
      duration: loadTime,
      timestamp: Date.now()
    })
    
    console.log(`Page ${pageName} loaded in ${loadTime}ms`)
  }
  
  // Measure function execution time
  measureFunction(name, fn) {
    return async (...args) => {
      const startTime = Date.now()
      
      try {
        const result = await fn(...args)
        const duration = Date.now() - startTime
        
        this.recordMetric({
          type: 'function_execution',
          name,
          duration,
          success: true,
          timestamp: Date.now()
        })
        
        return result
      } catch (error) {
        const duration = Date.now() - startTime
        
        this.recordMetric({
          type: 'function_execution',
          name,
          duration,
          success: false,
          error: error.message,
          timestamp: Date.now()
        })
        
        throw error
      }
    }
  }
  
  // Monitor memory usage
  monitorMemory() {
    if (wx.getPerformance) {
      const performance = wx.getPerformance()
      
      this.recordMetric({
        type: 'memory_usage',
        usedJSHeapSize: performance.usedJSHeapSize,
        totalJSHeapSize: performance.totalJSHeapSize,
        timestamp: Date.now()
      })
    }
  }
  
  // Monitor network requests
  monitorRequest(url, startTime, endTime, success, size) {
    this.recordMetric({
      type: 'network_request',
      url,
      duration: endTime - startTime,
      success,
      size,
      timestamp: endTime
    })
  }
  
  recordMetric(metric) {
    this.metrics.push(metric)
    
    // Keep only recent metrics
    if (this.metrics.length > 1000) {
      this.metrics = this.metrics.slice(-500)
    }
    
    // Report critical performance issues
    this.checkPerformanceThresholds(metric)
  }
  
  checkPerformanceThresholds(metric) {
    const thresholds = {
      page_load: 3000,
      function_execution: 1000,
      network_request: 5000
    }
    
    const threshold = thresholds[metric.type]
    if (threshold && metric.duration > threshold) {
      console.warn(`Performance warning: ${metric.type} took ${metric.duration}ms`, metric)
      
      // Report to analytics service
      this.reportPerformanceIssue(metric)
    }
  }
  
  reportPerformanceIssue(metric) {
    // Send to analytics service
    wx.request({
      url: 'https://analytics.example.com/performance',
      method: 'POST',
      data: {
        ...metric,
        userAgent: wx.getSystemInfoSync(),
        appVersion: getApp().globalData.version
      }
    })
  }
  
  getMetrics() {
    return this.metrics
  }
  
  getAverageMetric(type) {
    const typeMetrics = this.metrics.filter(m => m.type === type)
    if (typeMetrics.length === 0) return 0
    
    const total = typeMetrics.reduce((sum, m) => sum + m.duration, 0)
    return total / typeMetrics.length
  }
}

const performanceMonitor = new PerformanceMonitor()

// Usage
Page({
  onLoad() {
    const startTime = Date.now()
    
    // Measure page load
    this.loadData().then(() => {
      performanceMonitor.measurePageLoad('home')
    })
  },
  
  // Wrap functions for performance monitoring
  loadData: performanceMonitor.measureFunction('loadData', async function() {
    // Your data loading logic
    const data = await api.getData()
    this.setData({ data })
  }),
  
  onShow() {
    // Monitor memory usage periodically
    this.memoryTimer = setInterval(() => {
      performanceMonitor.monitorMemory()
    }, 10000)
  },
  
  onHide() {
    if (this.memoryTimer) {
      clearInterval(this.memoryTimer)
    }
  }
})

Performance optimization is an ongoing process. Regular monitoring, profiling, and optimization based on real user data will help maintain smooth and responsive mini programs.

Connecting Multiple Platforms, Empowering Innovation