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 端优化:
- 优化浏览器的渲染性能
- 合理使用浏览器缓存
实用案例分析
案例一:实现资源预加载
功能说明
实现资源预加载功能,提前加载首屏所需的资源,减少首屏加载时间。
实现步骤
预加载图片资源
// 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();在应用启动时使用预加载
// 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); } } } };
案例二:实现代码分割和按需加载
功能说明
实现代码分割和按需加载功能,减少首屏加载的代码量,提升首屏加载速度。
实现步骤
配置路由级代码分割
// 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: '详情页' } } ] };实现组件级按需加载
<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>使用动态 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 }; }
案例三:实现骨架屏和预加载数据
功能说明
实现骨架屏和预加载数据功能,提升用户对首屏加载的感知速度。
实现步骤
创建骨架屏组件
<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>在页面中使用骨架屏
<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>
案例三:实现首屏加载性能监控
功能说明
实现首屏加载性能监控功能,收集首屏加载的各项指标,帮助开发者分析和优化首屏加载性能。
实现步骤
创建首屏性能监控服务
// 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();在应用中使用首屏性能监控
// 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' }); } } };
技术要点总结
首屏加载流程优化:
- 了解首屏加载的完整流程
- 识别首屏加载的瓶颈
- 针对性地优化各个阶段的性能
资源优化:
- 减小资源体积(压缩图片、代码等)
- 优化资源加载顺序
- 使用合适的资源格式
网络优化:
- 使用 HTTP/2 或 HTTP/3
- 启用 Gzip 压缩
- 使用 CDN 加速
- 合理设置缓存策略
代码优化:
- 实现代码分割和按需加载
- 减少首屏加载的代码量
- 优化代码执行效率
预加载技术:
- 预加载首屏所需的资源
- 预加载首屏所需的数据
- 使用骨架屏提升用户体验
性能监控:
- 收集首屏加载的各项指标
- 分析性能数据,找出瓶颈
- 持续优化首屏加载性能
平台特性适配:
- 根据不同平台的特性进行优化
- 利用平台特有的优化手段
- 测试不同平台的首屏加载性能
学习目标
- 了解 uni-app 应用的首屏加载流程
- 掌握 uni-app 首屏优化的核心技术
- 学会实现资源预加载功能
- 理解如何实现代码分割和按需加载
- 掌握骨架屏的实现方法
- 了解首屏加载性能监控的实现
- 能够根据实际情况制定首屏优化策略
章节小结
本章节详细介绍了 uni-app 应用的首屏优化技术,包括首屏加载流程、资源预加载和代码分割,帮助开发者优化应用首屏加载速度。通过三个实用案例,展示了如何实现资源预加载、代码分割和按需加载,以及首屏加载性能监控。
在进行首屏优化时,开发者需要注意以下几点:
- 了解首屏加载的完整流程,识别性能瓶颈
- 采取多种优化策略,包括资源优化、网络优化、代码优化等
- 合理使用预加载技术,提前加载首屏所需的资源和数据
- 实现代码分割和按需加载,减少首屏加载的代码量
- 使用骨架屏提升用户对首屏加载的感知体验
- 建立首屏加载性能监控机制,持续分析和优化性能
- 根据不同平台的特性进行针对性优化
通过合理应用这些首屏优化技术,开发者可以显著提升 uni-app 应用的首屏加载速度,提供更加流畅的用户体验,从而提升用户满意度和应用的竞争力。