uni-app 首屏优化

章节介绍

首屏加载速度是影响用户体验的关键因素之一。根据研究,应用的首屏加载时间超过 3 秒,用户流失率会显著增加。uni-app 作为一个跨平台开发框架,其首屏加载速度受到多种因素的影响,包括代码体积、资源加载、网络请求等。因此,首屏优化对于提升 uni-app 应用的用户体验至关重要。本章节将详细介绍 uni-app 的首屏优化技术,包括首屏加载流程、资源预加载和代码分割,帮助开发者优化应用首屏加载速度。

核心知识点

1. 首屏加载流程

了解首屏加载的完整流程是进行首屏优化的基础。uni-app 应用的首屏加载流程主要包括以下几个阶段:

1.1 应用启动阶段

  • 冷启动:应用从完全关闭状态启动

    • 加载应用资源(如二进制文件、资源包等)
    • 初始化运行环境
    • 执行应用入口代码
  • 热启动:应用从后台恢复到前台

    • 恢复应用状态
    • 刷新页面内容

1.2 页面加载阶段

  • 路由解析:解析页面路由,确定要加载的页面
  • 资源加载:加载页面所需的资源(如 JavaScript、CSS、图片等)
  • 代码执行:执行页面的生命周期函数
  • 数据获取:发起网络请求,获取页面所需的数据
  • DOM 构建:构建页面的 DOM 结构
  • 样式计算:计算页面元素的样式
  • 页面渲染:将页面渲染到屏幕上

1.3 首屏加载时间

首屏加载时间是指从用户启动应用到首屏内容完全显示的时间。它包括以下几个关键时间点:

  • 开始时间:用户点击应用图标
  • 白屏时间:应用启动但首屏内容尚未显示的时间
  • 首屏内容绘制时间:首屏开始显示内容的时间
  • 首屏完全加载时间:首屏内容完全显示且可交互的时间

2. 首屏优化策略

针对首屏加载的各个阶段,uni-app 提供了多种优化策略:

2.1 代码优化

  • 减小代码体积

    • 移除冗余代码
    • 使用 Tree Shaking 移除未使用的代码
    • 压缩代码(如使用 Terser 压缩 JavaScript)
    • 按需引入第三方库
  • 优化代码结构

    • 减少入口文件的代码量
    • 合理组织代码,避免不必要的计算
    • 使用异步加载和懒加载

2.2 资源优化

  • 图片优化

    • 使用适当尺寸的图片
    • 压缩图片(如使用 TinyPNG 压缩)
    • 使用 WebP 等现代图片格式
    • 实现图片懒加载
  • CSS 优化

    • 压缩 CSS 文件
    • 减少 CSS 选择器的复杂度
    • 避免使用 @import 引入 CSS
    • 使用 CSS Modules 或 CSS-in-JS
  • JavaScript 优化

    • 压缩 JavaScript 文件
    • 减少 JavaScript 的执行时间
    • 避免使用 eval 和 with 语句
    • 使用防抖和节流优化频繁触发的事件

2.3 网络优化

  • HTTP 优化

    • 使用 HTTP/2 或 HTTP/3
    • 启用 Gzip 压缩
    • 减少 HTTP 请求次数
    • 使用持久连接
  • CDN 优化

    • 使用 CDN 加速静态资源的加载
    • 选择离用户最近的 CDN 节点
    • 配置 CDN 缓存策略
  • 缓存优化

    • 合理设置 HTTP 缓存头
    • 使用 Service Worker 缓存
    • 实现资源预缓存

2.4 预加载技术

  • 资源预加载

    • 使用 <link rel="preload"> 预加载关键资源
    • 使用 <link rel="prefetch"> 预加载非关键资源
    • 预加载字体文件
    • 预加载图片资源
  • 数据预加载

    • 预加载首屏所需的数据
    • 使用骨架屏或占位符
    • 实现数据缓存

2.5 代码分割

  • 路由级代码分割

    • 按路由拆分代码
    • 实现按需加载
    • 减少首屏加载的代码量
  • 组件级代码分割

    • 按组件拆分代码
    • 实现组件懒加载
    • 优化组件渲染性能

3. uni-app 特有的优化策略

3.1 应用配置优化

  • manifest.json 配置

    • 合理配置应用的启动模式
    • 优化应用的权限配置
    • 配置适当的屏幕方向
  • pages.json 配置

    • 优化页面路由配置
    • 配置页面的预加载
    • 合理设置页面的导航栏和状态栏

3.2 原生能力利用

  • 原生插件

    • 使用原生插件替代纯 JavaScript 实现
    • 优化原生插件的加载和初始化
  • 原生渲染

    • 使用 nvue 页面提升渲染性能
    • 合理使用原生组件

3.3 平台特性适配

  • App 端优化

    • 优化原生代码的执行效率
    • 合理使用原生存储
  • 小程序端优化

    • 遵守小程序的代码体积限制
    • 优化小程序的启动性能
  • Web 端优化

    • 优化浏览器的渲染性能
    • 合理使用浏览器缓存

实用案例分析

案例一:实现资源预加载

功能说明

实现资源预加载功能,提前加载首屏所需的资源,减少首屏加载时间。

实现步骤

  1. 预加载图片资源

    // utils/preload.js
    class PreloadService {
      constructor() {
        this.preloadedResources = [];
      }
      
      // 预加载图片
      preloadImages(imageUrls) {
        return new Promise((resolve, reject) => {
          let loadedCount = 0;
          const totalCount = imageUrls.length;
          
          if (totalCount === 0) {
            resolve();
            return;
          }
          
          imageUrls.forEach((url) => {
            const image = new Image();
            image.src = url;
            
            image.onload = () => {
              loadedCount++;
              this.preloadedResources.push(url);
              if (loadedCount === totalCount) {
                resolve();
              }
            };
            
            image.onerror = () => {
              loadedCount++;
              if (loadedCount === totalCount) {
                resolve();
              }
            };
          });
        });
      }
      
      // 预加载字体
      preloadFonts(fonts) {
        // 这里可以实现字体预加载逻辑
        console.log('预加载字体', fonts);
      }
      
      // 预加载脚本
      preloadScripts(scripts) {
        // 这里可以实现脚本预加载逻辑
        console.log('预加载脚本', scripts);
      }
    }
    
    export default new PreloadService();
  2. 在应用启动时使用预加载

    // app.vue
    import preloadService from '@/utils/preload';
    
    export default {
      onLaunch() {
        // 预加载首屏所需的资源
        this.preloadResources();
      },
      methods: {
        async preloadResources() {
          try {
            // 预加载首屏所需的图片
            const imageUrls = [
              'https://example.com/images/logo.png',
              'https://example.com/images/banner.jpg',
              'https://example.com/images/icon1.png',
              'https://example.com/images/icon2.png'
            ];
            
            await preloadService.preloadImages(imageUrls);
            console.log('图片预加载完成');
            
            // 预加载字体
            const fonts = [
              'https://example.com/fonts/main.woff2'
            ];
            
            preloadService.preloadFonts(fonts);
            console.log('字体预加载完成');
          } catch (error) {
            console.log('预加载失败', error);
          }
        }
      }
    };

案例二:实现代码分割和按需加载

功能说明

实现代码分割和按需加载功能,减少首屏加载的代码量,提升首屏加载速度。

实现步骤

  1. 配置路由级代码分割

    // router/index.js
    export default {
      routes: [
        {
          path: '/',
          name: 'Home',
          component: () => import('@/pages/home/index.vue'),
          meta: {
            title: '首页'
          }
        },
        {
          path: '/about',
          name: 'About',
          component: () => import('@/pages/about/index.vue'),
          meta: {
            title: '关于我们'
          }
        },
        {
          path: '/detail/:id',
          name: 'Detail',
          component: () => import('@/pages/detail/index.vue'),
          meta: {
            title: '详情页'
          }
        }
      ]
    };
  2. 实现组件级按需加载

    <template>
      <view class="home-page">
        <text class="title">首页</text>
        <button @click="loadHeavyComponent">加载 heavy 组件</button>
        <heavy-component v-if="showHeavyComponent" />
      </view>
    </template>
    
    <script>
    export default {
      data() {
        return {
          showHeavyComponent: false
        };
      },
      methods: {
        async loadHeavyComponent() {
          // 按需加载组件
          const { default: HeavyComponent } = await import('@/components/HeavyComponent.vue');
          this.$options.components.HeavyComponent = HeavyComponent;
          this.showHeavyComponent = true;
        }
      }
    };
    </script>
    
    <style>
    .home-page {
      padding: 20px;
    }
    
    .title {
      font-size: 24px;
      font-weight: bold;
      margin-bottom: 20px;
    }
    </style>
  3. 使用动态 import 加载第三方库

    // utils/lazyLoad.js
    export async function loadThirdPartyLibs() {
      // 按需加载第三方库
      const [lodash, moment, axios] = await Promise.all([
        import('lodash'),
        import('moment'),
        import('axios')
      ]);
      
      return {
        lodash: lodash.default,
        moment: moment.default,
        axios: axios.default
      };
    }

案例三:实现骨架屏和预加载数据

功能说明

实现骨架屏和预加载数据功能,提升用户对首屏加载的感知速度。

实现步骤

  1. 创建骨架屏组件

    <template>
      <view class="skeleton-container">
        <view class="skeleton-header"></view>
        <view class="skeleton-content">
          <view class="skeleton-item" v-for="i in 3" :key="i">
            <view class="skeleton-avatar"></view>
            <view class="skeleton-text">
              <view class="skeleton-line" v-for="j in 2" :key="j"></view>
            </view>
          </view>
        </view>
      </view>
    </template>
    
    <style scoped>
    .skeleton-container {
      width: 100%;
      padding: 20px;
    }
    
    .skeleton-header {
      height: 80px;
      background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
      background-size: 200% 100%;
      animation: loading 1.5s infinite;
      border-radius: 8px;
      margin-bottom: 20px;
    }
    
    .skeleton-content {
      width: 100%;
    }
    
    .skeleton-item {
      display: flex;
      margin-bottom: 20px;
    }
    
    .skeleton-avatar {
      width: 60px;
      height: 60px;
      background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
      background-size: 200% 100%;
      animation: loading 1.5s infinite;
      border-radius: 50%;
      margin-right: 16px;
    }
    
    .skeleton-text {
      flex: 1;
    }
    
    .skeleton-line {
      height: 16px;
      background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
      background-size: 200% 100%;
      animation: loading 1.5s infinite;
      border-radius: 4px;
      margin-bottom: 8px;
    }
    
    .skeleton-line:last-child {
      width: 70%;
    }
    
    @keyframes loading {
      0% {
        background-position: 200% 0;
      }
      100% {
        background-position: -200% 0;
      }
    }
    </style>
  2. 在页面中使用骨架屏

    <template>
      <view class="home-page">
        <!-- 骨架屏 -->
        <skeleton v-if="loading" />
        
        <!-- 实际内容 -->
        <view v-else class="content">
          <view class="header">
            <image :src="banner.image" mode="aspectFill" />
            <text class="header-title">{{ banner.title }}</text>
          </view>
          <view class="list">
            <view class="list-item" v-for="item in list" :key="item.id">
              <image :src="item.avatar" mode="aspectFill" class="avatar" />
              <view class="item-content">
                <text class="item-title">{{ item.title }}</text>
                <text class="item-desc">{{ item.description }}</text>
              </view>
            </view>
          </view>
        </view>
      </view>
    </template>
    
    <script>
    import Skeleton from '@/components/Skeleton.vue';
    
    export default {
      components: {
        Skeleton
      },
      data() {
        return {
          loading: true,
          banner: {},
          list: []
        };
      },
      async onLoad() {
        // 预加载数据
        await this.preloadData();
      },
      methods: {
        async preloadData() {
          try {
            // 并行请求数据
            const [bannerRes, listRes] = await Promise.all([
              uni.request({ url: 'https://example.com/api/banner' }),
              uni.request({ url: 'https://example.com/api/list' })
            ]);
            
            // 更新数据
            this.banner = bannerRes.data.data;
            this.list = listRes.data.data;
          } catch (error) {
            console.log('数据加载失败', error);
          } finally {
            // 关闭加载状态
            this.loading = false;
          }
        }
      }
    };
    </script>
    
    <style>
    .home-page {
      padding: 20px;
    }
    
    .header {
      margin-bottom: 20px;
    }
    
    .header image {
      width: 100%;
      height: 200rpx;
      border-radius: 8px;
    }
    
    .header-title {
      font-size: 20px;
      font-weight: bold;
      margin-top: 10px;
    }
    
    .list {
      width: 100%;
    }
    
    .list-item {
      display: flex;
      margin-bottom: 20px;
    }
    
    .avatar {
      width: 60px;
      height: 60px;
      border-radius: 50%;
      margin-right: 16px;
    }
    
    .item-content {
      flex: 1;
    }
    
    .item-title {
      font-size: 18px;
      font-weight: bold;
      margin-bottom: 8px;
    }
    
    .item-desc {
      font-size: 14px;
      color: #666;
    }
    </style>

案例三:实现首屏加载性能监控

功能说明

实现首屏加载性能监控功能,收集首屏加载的各项指标,帮助开发者分析和优化首屏加载性能。

实现步骤

  1. 创建首屏性能监控服务

    // utils/firstScreenPerformance.js
    class FirstScreenPerformanceService {
      constructor() {
        this.performanceData = {
          navigationStart: performance.timing.navigationStart,
          firstPaint: 0,
          firstContentfulPaint: 0,
          largestContentfulPaint: 0,
          domContentLoaded: 0,
          loadEvent: 0,
          firstApiRequest: 0,
          firstApiResponse: 0,
          firstRender: 0
        };
        
        this.init();
      }
      
      // 初始化监控
      init() {
        // 监听性能指标
        this.observePerformance();
        
        // 监听 API 请求
        this.observeApiRequests();
      }
      
      // 监听性能指标
      observePerformance() {
        // 监听首次绘制
        if (typeof performance !== 'undefined' && performance.getEntriesByType) {
          // 监听首次内容绘制
          const observer = new PerformanceObserver((list) => {
            list.getEntries().forEach((entry) => {
              if (entry.name === 'first-paint') {
                this.performanceData.firstPaint = entry.startTime;
              } else if (entry.name === 'first-contentful-paint') {
                this.performanceData.firstContentfulPaint = entry.startTime;
              } else if (entry.name === 'largest-contentful-paint') {
                this.performanceData.largestContentfulPaint = entry.startTime;
              }
            });
          });
          
          observer.observe({ entryTypes: ['paint', 'largest-contentful-paint'] });
        }
        
        // 监听 DOM 内容加载完成
        if (typeof document !== 'undefined') {
          document.addEventListener('DOMContentLoaded', () => {
            this.performanceData.domContentLoaded = performance.now();
          });
          
          // 监听页面加载完成
          window.addEventListener('load', () => {
            this.performanceData.loadEvent = performance.now();
            // 上报性能数据
            this.reportPerformance();
          });
        }
      }
      
      // 监听 API 请求
      observeApiRequests() {
        const originalRequest = uni.request;
        uni.request = (options) => {
          const requestStart = Date.now();
          
          // 保存原始的 success 回调
          const originalSuccess = options.success;
          
          // 重写 success 回调
          options.success = (res) => {
            const requestEnd = Date.now();
            
            // 记录第一次 API 请求和响应时间
            if (!this.performanceData.firstApiRequest) {
              this.performanceData.firstApiRequest = requestStart;
              this.performanceData.firstApiResponse = requestEnd;
            }
            
            // 调用原始的 success 回调
            if (originalSuccess) {
              originalSuccess(res);
            }
          };
          
          // 调用原始的 request 方法
          return originalRequest(options);
        };
      }
      
      // 记录首次渲染时间
      recordFirstRender() {
        this.performanceData.firstRender = performance.now();
      }
      
      // 上报性能数据
      reportPerformance() {
        // 计算各项指标
        const metrics = {
          timeToFirstPaint: this.performanceData.firstPaint - this.performanceData.navigationStart,
          timeToFirstContentfulPaint: this.performanceData.firstContentfulPaint - this.performanceData.navigationStart,
          timeToLargestContentfulPaint: this.performanceData.largestContentfulPaint - this.performanceData.navigationStart,
          timeToDomContentLoaded: this.performanceData.domContentLoaded,
          timeToLoad: this.performanceData.loadEvent,
          timeToFirstApiRequest: this.performanceData.firstApiRequest - this.performanceData.navigationStart,
          timeToFirstApiResponse: this.performanceData.firstApiResponse - this.performanceData.navigationStart,
          timeToFirstRender: this.performanceData.firstRender
        };
        
        console.log('首屏性能指标', metrics);
        
        // 上报到服务器
        uni.request({
          url: 'https://example.com/api/performance',
          method: 'POST',
          data: metrics
        });
      }
    }
    
    export default new FirstScreenPerformanceService();
  2. 在应用中使用首屏性能监控

    // app.vue
    import firstScreenPerformance from '@/utils/firstScreenPerformance';
    
    export default {
      onLaunch() {
        // 初始化应用
        this.initApp();
      },
      methods: {
        async initApp() {
          try {
            // 初始化各种服务
            await this.initServices();
            
            // 加载首页数据
            await this.loadHomeData();
            
            // 记录首次渲染时间
            firstScreenPerformance.recordFirstRender();
          } catch (error) {
            console.log('应用初始化失败', error);
          }
        },
        
        async initServices() {
          // 初始化各种服务
          console.log('初始化服务');
          await new Promise(resolve => setTimeout(resolve, 100));
        },
        
        async loadHomeData() {
          // 加载首页数据
          console.log('加载首页数据');
          await uni.request({
            url: 'https://example.com/api/home/data'
          });
        }
      }
    };

技术要点总结

  1. 首屏加载流程优化

    • 了解首屏加载的完整流程
    • 识别首屏加载的瓶颈
    • 针对性地优化各个阶段的性能
  2. 资源优化

    • 减小资源体积(压缩图片、代码等)
    • 优化资源加载顺序
    • 使用合适的资源格式
  3. 网络优化

    • 使用 HTTP/2 或 HTTP/3
    • 启用 Gzip 压缩
    • 使用 CDN 加速
    • 合理设置缓存策略
  4. 代码优化

    • 实现代码分割和按需加载
    • 减少首屏加载的代码量
    • 优化代码执行效率
  5. 预加载技术

    • 预加载首屏所需的资源
    • 预加载首屏所需的数据
    • 使用骨架屏提升用户体验
  6. 性能监控

    • 收集首屏加载的各项指标
    • 分析性能数据,找出瓶颈
    • 持续优化首屏加载性能
  7. 平台特性适配

    • 根据不同平台的特性进行优化
    • 利用平台特有的优化手段
    • 测试不同平台的首屏加载性能

学习目标

  1. 了解 uni-app 应用的首屏加载流程
  2. 掌握 uni-app 首屏优化的核心技术
  3. 学会实现资源预加载功能
  4. 理解如何实现代码分割和按需加载
  5. 掌握骨架屏的实现方法
  6. 了解首屏加载性能监控的实现
  7. 能够根据实际情况制定首屏优化策略

章节小结

本章节详细介绍了 uni-app 应用的首屏优化技术,包括首屏加载流程、资源预加载和代码分割,帮助开发者优化应用首屏加载速度。通过三个实用案例,展示了如何实现资源预加载、代码分割和按需加载,以及首屏加载性能监控。

在进行首屏优化时,开发者需要注意以下几点:

  1. 了解首屏加载的完整流程,识别性能瓶颈
  2. 采取多种优化策略,包括资源优化、网络优化、代码优化等
  3. 合理使用预加载技术,提前加载首屏所需的资源和数据
  4. 实现代码分割和按需加载,减少首屏加载的代码量
  5. 使用骨架屏提升用户对首屏加载的感知体验
  6. 建立首屏加载性能监控机制,持续分析和优化性能
  7. 根据不同平台的特性进行针对性优化

通过合理应用这些首屏优化技术,开发者可以显著提升 uni-app 应用的首屏加载速度,提供更加流畅的用户体验,从而提升用户满意度和应用的竞争力。

« 上一篇 uni-app 性能监控 下一篇 » uni-app 内存管理