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