Files
MoviePilot-Frontend/PWA_Performance_Analysis.md
2025-07-06 07:22:01 +00:00

10 KiB
Raw Blame History

PWA 后台切换前台体验优化分析报告

问题描述

从后台切换到前台时会显示加载界面同时界面会有闪烁切换的体验不是很好跟原生APP还有差距。

问题分析

1. 核心问题原因

1.1 状态恢复时机不当

  • PWA状态管理器初始化延迟:在 main.tsPWA状态管理器的初始化是在Vue应用挂载后进行的
  • 异步状态恢复:状态恢复是异步的,不会阻塞应用渲染,导致用户先看到初始状态
  • 渲染完成检测不准确ensureRenderComplete 只检测Vue组件渲染完成未考虑PWA状态恢复

1.2 加载界面移除时机过早

// 在App.vue中的问题代码
ensureRenderComplete(() => {
  nextTick(() => {
    // 移除加载动画,显示页面
    animateAndRemoveLoader()
    // 页面完全显示后,检查未读消息
    checkAndEmitUnreadMessages()
  })
})

加载界面在Vue组件渲染完成后立即移除但此时

  • PWA状态尚未恢复
  • 用户界面状态可能不完整
  • 导致用户看到不一致的状态

1.3 Service Worker状态同步延迟

Service Worker中的状态恢复通过消息传递实现存在延迟

// 在service-worker.ts中
self.addEventListener('message', function (event) {
  if (event.data && event.data.type === 'GET_PWA_STATE') {
    getStateFromCache()
      .then(response => response.json())
      .then(state => {
        event.ports[0]?.postMessage({ state })
      })
  }
})

2. 具体表现

  1. 界面闪烁

    • 用户首先看到默认状态
    • 然后看到状态恢复过程
    • 最后看到完整的恢复状态
  2. 加载界面显示

    • 从后台切换到前台时,某些组件可能需要重新渲染
    • 背景图片需要重新加载
    • 网络请求需要重新发起
  3. 状态不一致

    • 滚动位置不对
    • 表单数据丢失
    • 用户选择状态丢失

解决方案建议

1. 优化状态恢复时机

1.1 提前初始化PWA状态管理器

// 修改main.ts在应用挂载前初始化
const initializePWABeforeMount = async () => {
  const isPWA = window.matchMedia('(display-mode: standalone)').matches || 
                (window.navigator as any).standalone || 
                document.referrer.includes('android-app://')
  
  if (isPWA) {
    console.log('检测到PWA模式预初始化状态管理器')
    const pwaStateController = new PWAStateController()
    
    // 等待状态恢复完成
    await pwaStateController.waitForStateRestore()
    
    // 将状态管理器绑定到全局对象
    ;(window as any).pwaStateController = pwaStateController
    
    return true
  }
  
  return false
}

// 在创建Vue应用前调用
const pwaInitialized = await initializePWABeforeMount()
const app = createApp(App)

1.2 实现状态恢复等待机制

// 在PWAStateController中添加
export class PWAStateController {
  private stateRestorePromise: Promise<void> | null = null
  private stateRestoreResolve: (() => void) | null = null
  
  constructor() {
    this.stateRestorePromise = new Promise((resolve) => {
      this.stateRestoreResolve = resolve
    })
    this.init()
  }
  
  async waitForStateRestore(): Promise<void> {
    return this.stateRestorePromise
  }
  
  private async checkAndRestoreState(): Promise<void> {
    // 现有的状态恢复逻辑
    // ...
    
    // 状态恢复完成后解决Promise
    if (this.stateRestoreResolve) {
      this.stateRestoreResolve()
      this.stateRestoreResolve = null
    }
  }
}

2. 优化加载界面管理

2.1 条件化加载界面移除

// 修改App.vue中的加载界面移除逻辑
const removeLoadingWithStateCheck = async () => {
  // 检查PWA状态是否已恢复
  const pwaController = (window as any).pwaStateController
  if (pwaController) {
    await pwaController.waitForStateRestore()
  }
  
  // 确保关键资源已加载
  await Promise.all([
    // 等待背景图片加载完成
    loadBackgroundImages(),
    // 等待关键API数据加载完成
    loadCriticalData()
  ])
  
  // 移除加载界面
  animateAndRemoveLoader()
}

onMounted(async () => {
  await globalSettingsStore.initialize()
  configureApexCharts()
  updateHtmlThemeAttribute(globalTheme.name.value)
  
  // 使用优化后的加载界面移除逻辑
  ensureRenderComplete(() => {
    nextTick(removeLoadingWithStateCheck)
  })
})

2.2 实现智能加载状态检测

// 新增工具函数
export class PWALoadingStateManager {
  private loadingStates: Map<string, boolean> = new Map()
  
  setLoadingState(key: string, loading: boolean) {
    this.loadingStates.set(key, loading)
  }
  
  isAnyLoading(): boolean {
    return Array.from(this.loadingStates.values()).some(loading => loading)
  }
  
  waitForAllComplete(): Promise<void> {
    return new Promise((resolve) => {
      const checkComplete = () => {
        if (!this.isAnyLoading()) {
          resolve()
        } else {
          setTimeout(checkComplete, 100)
        }
      }
      checkComplete()
    })
  }
}

3. 优化页面可见性处理

3.1 改进页面可见性监听

// 修改VisibilityStateManager
export class VisibilityStateManager {
  private isRestoring = false
  private restorePromise: Promise<void> | null = null
  
  private handlePageVisible(): void {
    if (this.isRestoring) return
    
    this.isRestoring = true
    this.restorePromise = this.performStateRestore()
  }
  
  private async performStateRestore(): Promise<void> {
    try {
      // 显示恢复指示器
      this.showRestoreIndicator()
      
      const restoredState = this.stateManager.restoreState()
      if (restoredState) {
        await this.restoreAppState(restoredState)
        console.log('页面显示,已恢复状态')
      }
    } finally {
      this.isRestoring = false
      this.hideRestoreIndicator()
    }
  }
  
  private showRestoreIndicator(): void {
    // 显示轻量级的状态恢复指示器,而不是完整的加载界面
    const indicator = document.createElement('div')
    indicator.id = 'pwa-restore-indicator'
    indicator.innerHTML = `
      <div class="restore-indicator">
        <div class="restore-spinner"></div>
        <div class="restore-text">正在恢复状态...</div>
      </div>
    `
    document.body.appendChild(indicator)
  }
  
  private hideRestoreIndicator(): void {
    const indicator = document.getElementById('pwa-restore-indicator')
    if (indicator) {
      indicator.remove()
    }
  }
}

4. 优化Service Worker缓存策略

4.1 改进缓存预热

// 修改vite.config.ts中的PWA配置
VitePWA({
  workbox: {
    // 添加导航预加载
    navigationPreload: true,
    // 优化缓存策略
    runtimeCaching: [
      {
        urlPattern: ({ request }) => request.destination === 'document',
        handler: 'StaleWhileRevalidate', // 改为更快的策略
        options: {
          cacheName: 'pages-cache',
          cacheKeyWillBeUsed: async ({ request }) => {
            // 忽略状态参数,提高缓存命中率
            const url = new URL(request.url)
            url.searchParams.delete('restored')
            return url.toString()
          }
        }
      }
    ]
  }
})

4.2 实现状态预缓存

// 在service-worker.ts中添加
self.addEventListener('install', event => {
  event.waitUntil(
    (async () => {
      // 预缓存关键状态数据
      const cache = await caches.open(STATE_CACHE_NAME)
      const existingState = await cache.match(STATE_ENDPOINT)
      
      if (existingState) {
        // 预热状态数据
        const state = await existingState.json()
        console.log('预缓存状态数据:', state)
      }
    })()
  )
})

5. 增强用户体验

5.1 添加过渡动画

// 添加到App.vue的样式中
.pwa-transition {
  transition: opacity 0.3s ease, transform 0.3s ease;
}

.pwa-restoring {
  opacity: 0.8;
  transform: scale(0.98);
}

.restore-indicator {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 10000;
  background: rgba(0, 0, 0, 0.8);
  color: white;
  padding: 20px;
  border-radius: 10px;
  display: flex;
  align-items: center;
  gap: 10px;
}

.restore-spinner {
  width: 20px;
  height: 20px;
  border: 2px solid transparent;
  border-top: 2px solid white;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

5.2 实现骨架屏

<!-- 添加到App.vue中 -->
<template>
  <div class="app-wrapper">
    <!-- 骨架屏 -->
    <div v-if="isRestoring" class="skeleton-screen">
      <div class="skeleton-header"></div>
      <div class="skeleton-content">
        <div class="skeleton-line"></div>
        <div class="skeleton-line"></div>
        <div class="skeleton-line short"></div>
      </div>
    </div>
    
    <!-- 主要内容 -->
    <VApp v-else :class="{ 'transparent-app': isTransparentTheme }">
      <RouterView />
    </VApp>
  </div>
</template>

实施建议

优先级排序

  1. 高优先级

    • 优化状态恢复时机
    • 改进加载界面管理
    • 实现状态恢复等待机制
  2. 中优先级

    • 优化页面可见性处理
    • 添加过渡动画
    • 改进Service Worker缓存策略
  3. 低优先级

    • 实现骨架屏
    • 添加高级用户体验功能

测试建议

  1. 真机测试

    • 在iOS Safari上测试PWA模式
    • 在Android Chrome上测试PWA模式
    • 测试后台切换前台的各种场景
  2. 性能测试

    • 使用Chrome DevTools监控状态恢复性能
    • 测试不同网络状况下的表现
    • 监控内存使用情况
  3. 用户体验测试

    • 测试快速切换应用场景
    • 测试长时间后台后的恢复
    • 测试网络断开重连场景

预期效果

实施以上优化后,预期能够:

  1. 消除界面闪烁:状态恢复完成后再显示界面
  2. 减少加载时间:智能预加载和缓存策略
  3. 提升用户体验:平滑的过渡动画和反馈
  4. 接近原生体验:快速的状态恢复和响应

这些优化将显著提升PWA应用从后台切换到前台时的体验使其更接近原生应用的表现。