首屏优化深度解析:从加载性能到用户体验的全面优化
引言:为什么首屏性能如此重要?
首屏加载时间是影响用户体验和业务指标的关键因素。研究表明,页面加载时间每增加1秒,转化率就会下降7%,用户满意度降低16%。对于Vue单页应用来说,首屏优化更是至关重要,因为它直接决定了用户的第一印象和留存率。
本文将深入探讨Vue应用首屏优化的完整解决方案,从资源加载到代码执行,从网络优化到渲染优化,提供一套完整的性能优化体系。
一、资源加载优化
1.1 代码分割与懒加载
路由级代码分割:
// router/index.js - Vue 2
const routes = [
{
path: '/',
name: 'Home',
component: () => import(/* webpackChunkName: "home" */ '@/views/Home.vue')
},
{
path: '/about',
name: 'About',
component: () => import(/* webpackChunkName: "about" */ '@/views/About.vue')
},
{
path: '/user/:id',
name: 'User',
component: () => import(/* webpackChunkName: "user" */ '@/views/User.vue')
}
]
// Vue 3 + Vite
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
}
]
组件级懒加载:
<template>
<div>
<h1>用户仪表板</h1>
<Suspense>
<template #default>
<UserChart />
</template>
<template #fallback>
<div class="loading">图表加载中...</div>
</template>
</Suspense>
</div>
</template>
<script>
import { defineAsyncComponent } from 'vue'
// 异步组件加载
const UserChart = defineAsyncComponent({
loader: () => import('@/components/UserChart.vue'),
loadingComponent: () => import('@/components/LoadingSpinner.vue'),
delay: 200, // 延迟显示loading,避免闪烁
timeout: 3000 // 超时时间
})
export default {
components: {
UserChart
}
}
</script>
第三方库按需加载:
// 按需加载 Element Plus 组件
import { createApp } from 'vue'
import { ElButton, ElInput } from 'element-plus'
const app = createApp()
app.component(ElButton.name, ElButton)
app.component(ElInput.name, ElInput)
// 或者使用自动导入(Vite)
// vite.config.js
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default {
plugins: [
AutoImport({
resolvers: [ElementPlusResolver()],
}),
Components({
resolvers: [ElementPlusResolver()],
}),
],
}
1.2 资源预加载与预连接
资源优先级管理:
<!-- public/index.html -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<!-- DNS 预解析 -->
<link rel="dns-prefetch" href="https://cdn.example.com">
<!-- 预连接 -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- 关键CSS预加载 -->
<link rel="preload" href="/css/critical.css" as="style">
<!-- 关键字体预加载 -->
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
<!-- 关键图片预加载 -->
<link rel="preload" href="/images/hero-image.webp" as="image" type="image/webp">
<!-- 关键JS预加载 -->
<link rel="modulepreload" href="/src/main.js">
<title>我的Vue应用</title>
<!-- 内联关键CSS -->
<style>
/* 关键渲染路径CSS */
.header { position: fixed; top: 0; left: 0; right: 0; }
.hero { height: 100vh; background: #f5f5f5; }
/* ... 其他关键样式 */
</style>
</head>
<body>
<div id="app"></div>
<!-- 预加载非关键资源 -->
<link rel="prefetch" href="/src/components/HeavyComponent.vue" as="script">
<link rel="prefetch" href="/src/views/About.vue" as="script">
</body>
</html>
动态资源优先级:
// utils/preload.js
class ResourcePreloader {
constructor() {
this.preloaded = new Set()
}
// 预加载图片
preloadImage(src) {
return new Promise((resolve, reject) => {
if (this.preloaded.has(src)) {
resolve()
return
}
const img = new Image()
img.onload = () => {
this.preloaded.add(src)
resolve()
}
img.onerror = reject
img.src = src
})
}
// 预加载JS模块
preloadModule(path) {
if (this.preloaded.has(path)) return Promise.resolve()
const link = document.createElement('link')
link.rel = 'modulepreload'
link.href = path
return new Promise((resolve, reject) => {
link.onload = () => {
this.preloaded.add(path)
resolve()
}
link.onerror = reject
document.head.appendChild(link)
})
}
// 基于用户行为预测预加载
predictAndPreload(userBehavior) {
const predictions = this.getPredictions(userBehavior)
predictions.forEach(resource => {
if (resource.type === 'image') {
this.preloadImage(resource.url)
} else if (resource.type === 'module') {
this.preloadModule(resource.path)
}
})
}
getPredictions(userBehavior) {
// 基于用户行为分析预测可能访问的资源
const predictions = []
if (userBehavior.hoveredOnProducts) {
predictions.push(
{ type: 'module', path: '/src/views/ProductDetail.vue' },
{ type: 'image', url: '/images/product-gallery.webp' }
)
}
return predictions
}
}
// 在应用中使用
const preloader = new ResourcePreloader()
// 预加载关键资源
Promise.all([
preloader.preloadImage('/images/logo.webp'),
preloader.preloadModule('/src/components/HeroSection.vue')
]).then(() => {
console.log('关键资源预加载完成')
})
// 监听用户行为进行预测预加载
document.addEventListener('mouseover', (e) => {
if (e.target.classList.contains('product-link')) {
preloader.predictAndPreload({ hoveredOnProducts: true })
}
})
二、构建优化配置
2.1 Webpack优化配置
// vue.config.js
const { defineConfig } = require('@vue/cli-service')
const CompressionPlugin = require('compression-webpack-plugin')
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
module.exports = defineConfig({
transpileDependencies: true,
configureWebpack: {
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
// 第三方库单独打包
vendor: {
name: 'chunk-vendors',
test: /[\\/]node_modules[\\/]/,
priority: 10,
chunks: 'initial'
},
// 公共组件单独打包
common: {
name: 'chunk-common',
minChunks: 2,
priority: 5,
chunks: 'initial'
},
// Element UI 单独打包
elementUI: {
name: 'chunk-elementui',
test: /[\\/]node_modules[\\/]element-ui[\\/]/,
priority: 20
}
}
},
// 运行时代码单独提取
runtimeChunk: {
name: 'runtime'
}
},
plugins: [
// Gzip 压缩
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 8192,
minRatio: 0.8
}),
// Brotli 压缩
new CompressionPlugin({
filename: '[path][base].br',
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
compressionOptions: { level: 11 },
threshold: 8192,
minRatio: 0.8
}),
// 包分析工具
process.env.ANALYZE && new BundleAnalyzerPlugin()
].filter(Boolean),
resolve: {
alias: {
// 路径别名,减少解析时间
'@': path.resolve('src'),
'vue$': 'vue/dist/vue.esm-bundler.js'
},
// 减少文件搜索范围
modules: [
path.resolve('node_modules'),
path.resolve('src')
],
// 明确扩展名
extensions: ['.js', '.vue', '.json']
},
module: {
rules: [
// 图片压缩
{
test: /\.(png|jpe?g|gif|webp)(\?.*)?$/,
use: [
{
loader: 'url-loader',
options: {
limit: 4096,
fallback: {
loader: 'file-loader',
options: {
name: 'img/[name].[hash:8].[ext]'
}
}
}
},
{
loader: 'image-webpack-loader',
options: {
mozjpeg: {
progressive: true,
quality: 65
},
optipng: {
enabled: false
},
pngquant: {
quality: [0.65, 0.90],
speed: 4
},
gifsicle: {
interlaced: false
},
webp: {
quality: 75
}
}
}
]
}
]
}
},
chainWebpack: config => {
// 生产环境配置
if (process.env.NODE_ENV === 'production') {
// 移除 prefetch 插件,手动控制
config.plugins.delete('prefetch')
// 最小化CSS
config.optimization.minimizer('css').tap(args => {
args[0].cssnanoOptions.preset[1].cssDeclarationSorter = false
return args
})
}
// 预加载关键chunk
config.plugin('preload').tap(args => {
args[0] = {
rel: 'preload',
include: 'initial',
fileBlacklist: [/\.map$/, /hot-update\.js$/]
}
return args
})
}
})
2.2 Vite优化配置
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig({
plugins: [
vue(),
// 包分析
visualizer({
filename: 'dist/stats.html',
open: true,
gzipSize: true,
brotliSize: true
})
],
build: {
// 代码分割配置
rollupOptions: {
output: {
manualChunks: {
'vue-vendor': ['vue', 'vue-router', 'pinia'],
'ui-library': ['element-plus'],
'utils': ['lodash-es', 'axios', 'dayjs']
},
// 更细粒度的chunk分割
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: '[ext]/[name]-[hash].[ext]'
}
},
// 构建优化
target: 'es2015',
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
// chunk大小警告限制
chunkSizeWarningLimit: 1000,
// 资源内联限制
assetsInlineLimit: 4096
},
// 依赖优化
optimizeDeps: {
include: [
'vue',
'vue-router',
'pinia',
'axios',
'lodash-es'
],
exclude: ['some-big-dependency']
},
// 预加载配置
preview: {
headers: {
'Cache-Control': 'public, max-age=600'
}
},
server: {
// 开发服务器优化
hmr: {
overlay: false
}
}
})
三、渲染性能优化
3.1 虚拟滚动优化长列表
<template>
<div class="virtual-list" @scroll="handleScroll" ref="listContainer">
<div class="virtual-list__phantom" :style="phantomStyle"></div>
<div class="virtual-list__content" :style="contentStyle">
<div
v-for="item in visibleData"
:key="item.id"
class="virtual-list__item"
:style="getItemStyle(item)"
>
<slot name="item" :item="item"></slot>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted, onUnmounted } from 'vue'
export default {
name: 'VirtualList',
props: {
items: {
type: Array,
required: true
},
itemHeight: {
type: Number,
default: 60
},
buffer: {
type: Number,
default: 5
}
},
setup(props) {
const listContainer = ref(null)
const scrollTop = ref(0)
const containerHeight = ref(0)
// 计算总高度
const totalHeight = computed(() => props.items.length * props.itemHeight)
// 计算可见区域
const visibleCount = computed(() =>
Math.ceil(containerHeight.value / props.itemHeight) + props.buffer * 2
)
// 计算开始索引
const startIndex = computed(() => {
let index = Math.floor(scrollTop.value / props.itemHeight) - props.buffer
return Math.max(0, index)
})
// 计算结束索引
const endIndex = computed(() => {
let index = startIndex.value + visibleCount.value
return Math.min(props.items.length, index)
})
// 可见数据
const visibleData = computed(() =>
props.items.slice(startIndex.value, endIndex.value)
)
// 占位元素样式
const phantomStyle = computed(() => ({
height: `${totalHeight.value}px`
}))
// 内容容器样式
const contentStyle = computed(() => ({
transform: `translateY(${startIndex.value * props.itemHeight}px)`
}))
// 获取项目样式
const getItemStyle = (item) => ({
height: `${props.itemHeight}px`,
'line-height': `${props.itemHeight}px`
})
// 滚动处理
const handleScroll = () => {
if (listContainer.value) {
scrollTop.value = listContainer.value.scrollTop
}
}
// 容器大小监听
const updateContainerHeight = () => {
if (listContainer.value) {
containerHeight.value = listContainer.value.clientHeight
}
}
// 防抖滚动处理
let scrollTimer = null
const throttledScroll = () => {
if (scrollTimer) clearTimeout(scrollTimer)
scrollTimer = setTimeout(handleScroll, 16) // 60fps
}
onMounted(() => {
updateContainerHeight()
window.addEventListener('resize', updateContainerHeight)
})
onUnmounted(() => {
window.removeEventListener('resize', updateContainerHeight)
if (scrollTimer) clearTimeout(scrollTimer)
})
return {
listContainer,
visibleData,
phantomStyle,
contentStyle,
getItemStyle,
handleScroll: throttledScroll
}
}
}
</script>
<style scoped>
.virtual-list {
height: 100%;
overflow-y: auto;
position: relative;
}
.virtual-list__phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
z-index: -1;
}
.virtual-list__content {
position: absolute;
left: 0;
right: 0;
top: 0;
}
.virtual-list__item {
border-bottom: 1px solid #f0f0f0;
padding: 0 16px;
box-sizing: border-box;
}
</style>
3.2 图片懒加载与优化
<template>
<div class="lazy-image">
<!-- 占位符 -->
<div v-if="!isLoaded" class="lazy-image__placeholder">
<div class="lazy-image__skeleton"></div>
</div>
<!-- 实际图片 -->
<img
v-show="isLoaded"
:src="src"
:alt="alt"
:class="imageClass"
@load="handleLoad"
@error="handleError"
ref="imgElement"
/>
<!-- 加载失败显示 -->
<div v-if="hasError" class="lazy-image__error">
<slot name="error">
<span>图片加载失败</span>
</slot>
</div>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue'
export default {
name: 'LazyImage',
props: {
src: {
type: String,
required: true
},
alt: {
type: String,
default: ''
},
rootMargin: {
type: String,
default: '50px 0px'
},
threshold: {
type: Number,
default: 0.1
},
imageClass: {
type: String,
default: ''
}
},
setup(props) {
const imgElement = ref(null)
const isLoaded = ref(false)
const hasError = ref(false)
const observer = ref(null)
const handleLoad = () => {
isLoaded.value = true
hasError.value = false
}
const handleError = () => {
isLoaded.value = false
hasError.value = true
}
const loadImage = () => {
if (imgElement.value) {
imgElement.value.src = props.src
}
}
const initIntersectionObserver = () => {
if (!('IntersectionObserver' in window)) {
// 浏览器不支持,直接加载
loadImage()
return
}
observer.value = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
loadImage()
observer.value.unobserve(entry.target)
}
})
},
{
rootMargin: props.rootMargin,
threshold: props.threshold
}
)
if (imgElement.value) {
observer.value.observe(imgElement.value)
}
}
onMounted(() => {
initIntersectionObserver()
})
onUnmounted(() => {
if (observer.value) {
observer.value.disconnect()
}
})
return {
imgElement,
isLoaded,
hasError,
handleLoad,
handleError
}
}
}
</script>
<style scoped>
.lazy-image {
position: relative;
display: inline-block;
}
.lazy-image__placeholder {
background: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
}
.lazy-image__skeleton {
width: 100%;
height: 100%;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: loading 1.5s infinite;
}
.lazy-image__error {
background: #fff5f5;
color: #c53030;
padding: 8px;
text-align: center;
}
@keyframes loading {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
img {
display: block;
max-width: 100%;
height: auto;
transition: opacity 0.3s ease;
}
</style>
3.3 组件渲染优化
<template>
<div class="optimized-component">
<!-- 使用 v-show 替代 v-if 用于频繁切换 -->
<div v-show="isVisible" class="tooltip">
提示信息
</div>
<!-- 使用计算属性缓存复杂计算 -->
<div class="stats">
<div>总数: {{ totalCount }}</div>
<div>过滤后: {{ filteredCount }}</div>
</div>
<!-- 使用 v-once 静态内容 -->
<div v-once class="static-content">
<h1>静态标题</h1>
<p>这段内容永远不会改变</p>
</div>
<!-- 使用 v-memo 优化列表渲染 -->
<div
v-for="item in memoizedList"
:key="item.id"
v-memo="[item.id, item.status]"
class="list-item"
>
{{ item.name }} - {{ item.status }}
</div>
</div>
</template>
<script>
import { ref, computed, watch, nextTick } from 'vue'
export default {
name: 'OptimizedComponent',
setup() {
const isVisible = ref(false)
const list = ref([])
const filter = ref('')
// 计算属性缓存
const totalCount = computed(() => list.value.length)
const filteredList = computed(() => {
console.log('重新计算过滤列表')
return list.value.filter(item =>
item.name.includes(filter.value)
)
})
const filteredCount = computed(() => filteredList.value.length)
// 使用 v-memo 的列表
const memoizedList = computed(() => {
return filteredList.value.map(item => ({
id: item.id,
name: item.name,
status: item.status
}))
})
// 防抖的搜索
let searchTimer = null
const handleSearch = (value) => {
if (searchTimer) clearTimeout(searchTimer)
searchTimer = setTimeout(() => {
filter.value = value
}, 300)
}
// 批量更新数据
const updateListInBatch = (newItems) => {
// 使用 nextTick 批量更新
nextTick(() => {
list.value = newItems
})
}
// 监听器优化
watch(
() => list.value.length,
(newLength, oldLength) => {
console.log(`列表长度从 ${oldLength} 变为 ${newLength}`)
},
{ flush: 'post' } // 在组件更新后执行
)
return {
isVisible,
totalCount,
filteredCount,
memoizedList,
handleSearch,
updateListInBatch
}
},
// Vue 2 兼容的优化选项
// Vue 3 中这些选项仍然有效
data() {
return {
heavyData: null
}
},
computed: {
// 复杂计算缓存
processedData() {
if (!this.heavyData) return []
// 昂贵的计算操作
return this.heavyData.map(item => this.processItem(item))
}
},
methods: {
processItem(item) {
// 模拟昂贵的操作
return JSON.parse(JSON.stringify(item))
},
// 手动控制更新
forceUpdate() {
// 不推荐使用 $forceUpdate
// 应该使用响应式数据驱动更新
this.$set(this, 'timestamp', Date.now())
}
}
}
</script>
四、网络层优化
4.1 HTTP缓存策略
// nginx 配置示例
server {
listen 80;
server_name example.com;
# Gzip 压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_proxied any;
gzip_comp_level 6;
gzip_types
application/atom+xml
application/javascript
application/json
application/ld+json
application/manifest+json
application/rss+xml
application/vnd.geo+json
application/vnd.ms-fontobject
application/x-font-ttf
application/x-web-app-manifest+json
application/xhtml+xml
application/xml
font/opentype
image/bmp
image/svg+xml
image/x-icon
text/cache-manifest
text/css
text/plain
text/vcard
text/vnd.rim.location.xloc
text/vtt
text/x-component
text/x-cross-domain-policy;
location / {
root /usr/share/nginx/html;
index index.html;
# HTML 文件不缓存
location ~* \.html$ {
add_header Cache-Control "no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header Expires "0";
}
# 静态资源长期缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
# 启用 Brotli 压缩
brotli on;
brotli_comp_level 6;
brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
}
# API 请求
location /api/ {
proxy_pass http://api-server;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
# API 缓存策略
location ~* /api/static/ {
expires 1h;
add_header Cache-Control "public";
}
location ~* /api/dynamic/ {
add_header Cache-Control "no-cache";
}
}
# SPA 路由支持
try_files $uri $uri/ /index.html;
}
}
4.2 Service Worker缓存策略
// public/sw.js
const CACHE_NAME = 'vue-app-v1.0.0'
const API_CACHE_NAME = 'api-cache-v1.0.0'
// 需要缓存的资源
const STATIC_RESOURCES = [
'/',
'/static/js/main.chunk.js',
'/static/css/main.chunk.css',
'/static/media/logo.svg',
'/manifest.json'
]
// 安装事件
self.addEventListener('install', (event) => {
console.log('Service Worker 安装中...')
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('缓存静态资源')
return cache.addAll(STATIC_RESOURCES)
})
.then(() => {
console.log('跳过等待')
return self.skipWaiting()
})
)
})
// 激活事件
self.addEventListener('activate', (event) => {
console.log('Service Worker 激活中...')
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
// 删除旧缓存
if (cacheName !== CACHE_NAME && cacheName !== API_CACHE_NAME) {
console.log('删除旧缓存:', cacheName)
return caches.delete(cacheName)
}
})
)
}).then(() => {
console.log('接管客户端')
return self.clients.claim()
})
)
})
// 获取事件
self.addEventListener('fetch', (event) => {
const { request } = event
const url = new URL(request.url)
// API 请求 - 网络优先
if (url.pathname.startsWith('/api/')) {
event.respondWith(
caches.open(API_CACHE_NAME).then((cache) => {
return fetch(request).then((response) => {
// 缓存成功的GET请求
if (request.method === 'GET' && response.status === 200) {
cache.put(request, response.clone())
}
return response
}).catch(() => {
// 网络失败时返回缓存
return cache.match(request)
})
})
)
return
}
// 静态资源 - 缓存优先
if (request.destination === 'script' ||
request.destination === 'style' ||
request.destination === 'image') {
event.respondWith(
caches.match(request).then((cachedResponse) => {
if (cachedResponse) {
return cachedResponse
}
return fetch(request).then((response) => {
// 缓存新资源
if (response.status === 200) {
const responseToCache = response.clone()
caches.open(CACHE_NAME).then((cache) => {
cache.put(request, responseToCache)
})
}
return response
})
})
)
return
}
// 其他请求 - 网络优先
event.respondWith(
fetch(request).catch(() => {
return caches.match(request)
})
)
})
// 消息处理
self.addEventListener('message', (event) => {
if (event.data && event.data.type === 'SKIP_WAITING') {
self.skipWaiting()
}
})
4.3 CDN与资源优化
// utils/cdn.js
class CDNOptimizer {
constructor() {
this.cdnDomains = [
'https://cdn1.example.com',
'https://cdn2.example.com'
]
this.fallbackDomains = [
'https://fallback1.example.com',
'https://fallback2.example.com'
]
}
// 获取最优CDN域名
getOptimalCDN() {
// 基于地理位置、网络状况等选择最优CDN
const domain = this.cdnDomains[0] // 简化实现
return domain
}
// 资源URL优化
optimizeResourceURL(path) {
const cdnDomain = this.getOptimalCDN()
// 添加版本号避免缓存问题
const version = process.env.VUE_APP_VERSION || '1.0.0'
const timestamp = Date.now()
return `${cdnDomain}${path}?v=${version}&t=${timestamp}`
}
// 预连接最优CDN
preconnectOptimalCDN() {
const cdnDomain = this.getOptimalCDN()
const link = document.createElement('link')
link.rel = 'preconnect'
link.href = cdnDomain
link.crossOrigin = 'anonymous'
document.head.appendChild(link)
}
// 监控CDN性能
monitorCDNPerformance() {
const observer = new PerformanceObserver((list) => {
list.getEntries().forEach((entry) => {
if (entry.initiatorType === 'script' || entry.initiatorType === 'link') {
console.log(`资源加载: ${entry.name}, 耗时: ${entry.duration}ms`)
// 如果资源加载过慢,考虑切换到备用CDN
if (entry.duration > 5000) {
this.switchToFallbackCDN()
}
}
})
})
observer.observe({ entryTypes: ['resource'] })
}
switchToFallbackCDN() {
console.log('切换到备用CDN')
// 实现CDN切换逻辑
}
}
// 在main.js中使用
import { CDNOptimizer } from './utils/cdn'
const cdnOptimizer = new CDNOptimizer()
cdnOptimizer.preconnectOptimalCDN()
cdnOptimizer.monitorCDNPerformance()
// 配置Vue使用CDN资源
if (process.env.NODE_ENV === 'production') {
// 使用CDN版本的Vue
window.Vue = require('vue/dist/vue.runtime.esm-browser.prod.js')
}
五、监控与性能度量
5.1 性能指标监控
// utils/performance.js
class PerformanceMonitor {
constructor() {
this.metrics = {}
this.observers = []
}
// 初始化性能监控
init() {
this.observeLCP()
this.observeFID()
this.observeCLS()
this.observeFCP()
this.observeTTI()
}
// 观察最大内容绘制 (LCP)
observeLCP() {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
const lastEntry = entries[entries.length - 1]
this.metrics.LCP = lastEntry.renderTime || lastEntry.loadTime
this.reportMetric('LCP', this.metrics.LCP)
})
observer.observe({ entryTypes: ['largest-contentful-paint'] })
}
// 观察首次输入延迟 (FID)
observeFID() {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
entries.forEach(entry => {
this.metrics.FID = entry.processingStart - entry.startTime
this.reportMetric('FID', this.metrics.FID)
})
})
observer.observe({ entryTypes: ['first-input'] })
}
// 观察累积布局偏移 (CLS)
observeCLS() {
let clsValue = 0
let sessionValue = 0
let sessionEntries = []
const observer = new PerformanceObserver((entryList) => {
entryList.getEntries().forEach(entry => {
if (!entry.hadRecentInput) {
sessionEntries.push(entry)
sessionValue += entry.value
clsValue = Math.max(clsValue, sessionValue)
}
})
})
observer.observe({ entryTypes: ['layout-shift'] })
// 报告最终的CLS值
window.addEventListener('pagehide', () => {
this.metrics.CLS = clsValue
this.reportMetric('CLS', clsValue)
})
}
// 观察首次内容绘制 (FCP)
observeFCP() {
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
const firstPaint = entries[0]
this.metrics.FCP = firstPaint.startTime
this.reportMetric('FCP', this.metrics.FCP)
})
observer.observe({ entryTypes: ['paint'] })
}
// 观察可交互时间 (TTI)
observeTTI() {
// TTI 需要通过 Long Tasks API 计算
const observer = new PerformanceObserver((entryList) => {
const entries = entryList.getEntries()
// 计算TTI逻辑
this.calculateTTI()
})
observer.observe({ entryTypes: ['longtask'] })
}
calculateTTI() {
// 简化版的TTI计算
const navigationEntry = performance.getEntriesByType('navigation')[0]
const domContentLoaded = navigationEntry.domContentLoadedEventEnd
const longTasks = performance.getEntriesByType('longtask')
let lastLongTaskEnd = 0
longTasks.forEach(task => {
lastLongTaskEnd = Math.max(lastLongTaskEnd, task.startTime + task.duration)
})
this.metrics.TTI = Math.max(domContentLoaded, lastLongTaskEnd)
this.reportMetric('TTI', this.metrics.TTI)
}
// 自定义指标:首屏加载时间
measureAboveTheFoldTime() {
const observer = new MutationObserver(() => {
const foldElement = document.querySelector('.above-the-fold')
if (foldElement && this.isElementInViewport(foldElement)) {
this.metrics.aboveTheFoldTime = performance.now()
this.reportMetric('aboveTheFoldTime', this.metrics.aboveTheFoldTime)
observer.disconnect()
}
})
observer.observe(document, {
childList: true,
subtree: true
})
}
isElementInViewport(el) {
const rect = el.getBoundingClientRect()
return (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
)
}
// 报告指标
reportMetric(name, value) {
console.log(`性能指标 ${name}: ${value}ms`)
// 发送到监控服务
if (window.analytics) {
window.analytics.track('performance_metric', {
metric: name,
value: value,
timestamp: Date.now(),
userAgent: navigator.userAgent,
connection: navigator.connection ? navigator.connection.effectiveType : 'unknown'
})
}
// 触发观察者
this.observers.forEach(observer => {
if (observer.metric === name) {
observer.callback(value)
}
})
}
// 添加指标观察者
onMetric(metric, callback) {
this.observers.push({ metric, callback })
}
// 获取所有指标
getMetrics() {
return { ...this.metrics }
}
}
// 在应用中使用
const performanceMonitor = new PerformanceMonitor()
performanceMonitor.init()
// 监听关键指标
performanceMonitor.onMetric('LCP', (value) => {
if (value > 2500) {
console.warn('LCP 时间过长,考虑优化')
}
})
export default performanceMonitor
5.2 真实用户监控 (RUM)
// utils/rum.js
class RealUserMonitor {
constructor() {
this.data = {
navigationTiming: null,
resourceTiming: [],
userTiming: []
}
}
// 收集导航时序数据
collectNavigationTiming() {
const navigation = performance.getEntriesByType('navigation')[0]
if (navigation) {
this.data.navigationTiming = {
// 关键时间点
dnsLookup: navigation.domainLookupEnd - navigation.domainLookupStart,
tcpConnect: navigation.connectEnd - navigation.connectStart,
sslHandshake: navigation.secureConnectionStart > 0 ?
navigation.connectEnd - navigation.secureConnectionStart : 0,
ttfb: navigation.responseStart - navigation.requestStart,
contentLoad: navigation.domContentLoadedEventEnd - navigation.navigationStart,
fullLoad: navigation.loadEventEnd - navigation.navigationStart,
// 资源大小
transferSize: navigation.transferSize,
encodedSize: navigation.encodedBodySize,
decodedSize: navigation.decodedBodySize
}
}
}
// 收集资源时序数据
collectResourceTiming() {
const resources = performance.getEntriesByType('resource')
this.data.resourceTiming = resources.map(resource => ({
name: resource.name,
duration: resource.duration,
size: resource.transferSize,
type: this.getResourceType(resource.name)
}))
}
getResourceType(url) {
if (url.includes('.js')) return 'script'
if (url.includes('.css')) return 'stylesheet'
if (url.includes('.png') || url.includes('.jpg') || url.includes('.webp')) return 'image'
if (url.includes('/api/')) return 'api'
return 'other'
}
// 收集用户自定义指标
markUserTiming(name) {
performance.mark(name)
}
measureUserTiming(startMark, endMark, name) {
performance.measure(name, startMark, endMark)
const measures = performance.getEntriesByName(name)
this.data.userTiming.push({
name,
duration: measures[0].duration
})
}
// 收集用户体验指标
collectUXMetrics() {
// 页面可见性
document.addEventListener('visibilitychange', () => {
this.reportEvent('visibility_change', {
state: document.visibilityState,
duration: performance.now()
})
})
// 用户交互
document.addEventListener('click', (event) => {
this.reportEvent('user_click', {
target: event.target.tagName,
x: event.clientX,
y: event.clientY
})
})
// 滚动深度
let maxScrollDepth = 0
window.addEventListener('scroll', () => {
const scrollDepth = (window.scrollY + window.innerHeight) / document.documentElement.scrollHeight
if (scrollDepth > maxScrollDepth) {
maxScrollDepth = scrollDepth
this.reportEvent('scroll_depth', { depth: maxScrollDepth })
}
}, { passive: true })
}
// 错误监控
setupErrorTracking() {
// JS错误
window.addEventListener('error', (event) => {
this.reportError('javascript_error', {
message: event.message,
filename: event.filename,
lineno: event.lineno,
colno: event.colno
})
})
// Promise rejection
window.addEventListener('unhandledrejection', (event) => {
this.reportError('promise_rejection', {
reason: event.reason?.toString()
})
})
// 资源加载错误
window.addEventListener('error', (event) => {
if (event.target && event.target.src) {
this.reportError('resource_error', {
url: event.target.src,
tagName: event.target.tagName
})
}
}, true)
}
// 网络状况监控
monitorNetwork() {
if (navigator.connection) {
const connection = navigator.connection
this.reportEvent('network_info', {
effectiveType: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt
})
connection.addEventListener('change', () => {
this.reportEvent('network_change', {
effectiveType: connection.effectiveType,
downlink: connection.downlink,
rtt: connection.rtt
})
})
}
}
// 数据报告
reportEvent(type, data) {
const eventData = {
type,
data,
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent
}
// 发送到分析服务
this.sendToAnalytics(eventData)
}
reportError(type, error) {
this.reportEvent('error', { type, ...error })
}
sendToAnalytics(data) {
// 使用 navigator.sendBeacon 确保数据可靠发送
const blob = new Blob([JSON.stringify(data)], { type: 'application/json' })
navigator.sendBeacon('/api/analytics', blob)
}
// 生成性能报告
generateReport() {
this.collectNavigationTiming()
this.collectResourceTiming()
return {
navigation: this.data.navigationTiming,
resources: this.data.resourceTiming,
userTiming: this.data.userTiming,
timestamp: Date.now()
}
}
}
// 在应用中使用
const rum = new RealUserMonitor()
rum.setupErrorTracking()
rum.collectUXMetrics()
rum.monitorNetwork()
// 标记关键用户操作
rum.markUserTiming('app_mounted')
// ... 应用逻辑
rum.measureUserTiming('app_mounted', 'user_interaction_complete', 'app_ready_time')
export default rum
六、进阶优化策略
6.1 预渲染与SSG
// vue.config.js - 预渲染配置
const PrerenderSPAPlugin = require('prerender-spa-plugin')
const Renderer = require('@prerenderer/renderer-puppeteer')
module.exports = {
configureWebpack: {
plugins: [
new PrerenderSPAPlugin({
staticDir: path.join(__dirname, 'dist'),
routes: ['/', '/about', '/contact', '/products'],
renderer: new Renderer({
renderAfterTime: 5000,
// 或者使用事件触发
renderAfterDocumentEvent: 'app-rendered'
}),
postProcess(renderedRoute) {
// 优化HTML输出
renderedRoute.html = renderedRoute.html
.replace(/<script (.*?)>/g, '<script $1 defer>')
.replace(/<link (.*?)>/g, (match, p1) => {
if (p1.includes('stylesheet')) {
return `<link ${p1} media="print" onload="this.media='all'">`
}
return match
})
return renderedRoute
}
})
]
}
}
// 在main.js中触发渲染完成事件
import { createApp } from 'vue'
const app = createApp(App)
app.mount('#app')
// 通知预渲染器应用已渲染完成
setTimeout(() => {
document.dispatchEvent(new Event('app-rendered'))
}, 1000)
6.2 边缘计算优化
// Cloudflare Workers 示例
addEventListener('fetch', event => {
event.respondWith(handleRequest(event.request))
})
async function handleRequest(request) {
const url = new URL(request.url)
// 静态资源优化
if (url.pathname.startsWith('/static/')) {
return handleStaticAssets(request)
}
// API请求优化
if (url.pathname.startsWith('/api/')) {
return handleAPIRequest(request)
}
// HTML页面
return handleHTMLRequest(request)
}
async function handleStaticAssets(request) {
const cache = caches.default
let response = await cache.match(request)
if (!response) {
response = await fetch(request)
// 缓存静态资源
const headers = new Headers(response.headers)
headers.set('Cache-Control', 'public, max-age=31536000, immutable')
headers.set('CDN-Cache-Control', 'public, max-age=31536000')
response = new Response(response.body, {
status: response.status,
statusText: response.statusText,
headers
})
event.waitUntil(cache.put(request, response.clone()))
}
return response
}
async function handleHTMLRequest(request) {
const cache = caches.default
let response = await cache.match(request)
if (!response) {
response = await fetch(request)
// 优化HTML响应
let html = await response.text()
// 内联关键CSS
html = inlineCriticalCSS(html)
// 预加载关键资源
html = addResourceHints(html)
// 延迟非关键JS
html = deferNonCriticalJS(html)
response = new Response(html, response)
// 短期缓存HTML
event.waitUntil(cache.put(request, response.clone()))
}
return response
}
function inlineCriticalCSS(html) {
// 提取和内联关键CSS的逻辑
return html
}
function addResourceHints(html) {
// 添加资源提示的逻辑
return html
}
function deferNonCriticalJS(html) {
// 延迟非关键JS的逻辑
return html
}
总结
首屏优化是一个系统工程,需要从多个维度进行全面优化:
核心优化策略:
-
资源加载优化
- 代码分割与懒加载
- 资源预加载与预连接
- HTTP/2 服务器推送
-
构建优化
- Tree-shaking 与代码压缩
- 图片优化与格式选择
- 第三方库按需加载
-
渲染性能优化
- 虚拟滚动与列表优化
- 组件懒加载与代码分割
- 图片懒加载与响应式图片
-
网络层优化
- CDN 与边缘计算
- HTTP 缓存策略
- Service Worker 缓存
-
监控与度量
- Core Web Vitals 监控
- 真实用户监控 (RUM)
- 自动化性能测试
关键性能指标目标:
- LCP (最大内容绘制): < 2.5秒
- FID (首次输入延迟): < 100毫秒
- CLS (累积布局偏移): < 0.1
- FCP (首次内容绘制): < 1.8秒
- TTI (可交互时间): < 3.5秒
持续优化流程:
- 测量:使用性能监控工具收集数据
- 分析:识别性能瓶颈和优化机会
- 优化:实施具体的优化策略
- 验证:通过A/B测试验证优化效果
- 监控:持续监控确保优化效果持久
173万+

被折叠的 条评论
为什么被折叠?



