返回 筑基・网络云路秘径
前端性能优化实战
博主
大约 21 分钟
前端性能优化实战
一、问题引入:首屏加载的生死时速
1.1 真实案例:电商大促页面性能危机
场景:2024年双11大促,商品详情页性能告急
数据:首屏时间5.8秒,跳出率65%,转化率下降40%
问题分析过程:
┌─────────────────────────────────────────────────────────────┐
│ 阶段1:性能诊断 │
│ - Lighthouse评分:32/100(Poor) │
│ - FCP(首次内容绘制):2.1s │
│ - LCP(最大内容绘制):5.8s │
│ - TTI(可交互时间):8.3s │
│ - CLS(累积布局偏移):0.35 │
├─────────────────────────────────────────────────────────────┤
│ 阶段2:瓶颈定位 │
│ - 资源体积:JS 2.8MB,CSS 450KB,图片 4.2MB │
│ - 请求数量:127个HTTP请求 │
│ - 阻塞渲染:3个同步JS阻塞了首屏 │
│ - 未压缩:Gzip未启用,资源体积膨胀300% │
│ - 图片问题:使用PNG代替WebP,无响应式图片 │
│ - 缓存缺失:静态资源缓存策略不当 │
├─────────────────────────────────────────────────────────────┤
│ 阶段3:优化实施 │
│ - 代码分割:路由级+组件级懒加载 │
│ - 资源压缩:启用Brotli,JS/CSS压缩率70% │
│ - 图片优化:WebP格式,响应式图片,懒加载 │
│ - 缓存策略:静态资源1年缓存,API响应ETag │
│ - 预加载:关键资源preload,DNS预解析 │
├─────────────────────────────────────────────────────────────┤
│ 阶段4:优化效果 │
│ - Lighthouse评分:32 → 96 │
│ - FCP:2.1s → 0.8s │
│ - LCP:5.8s → 1.2s │
│ - TTI:8.3s → 2.1s │
│ - CLS:0.35 → 0.02 │
│ - 跳出率:65% → 35% │
│ - 转化率提升:58% │
└─────────────────────────────────────────────────────────────┘
核心教训:
1. 性能优化是系统工程,需要全链路考虑
2. 资源体积是首屏性能的关键
3. 缓存策略能显著降低重复访问成本
4. 图片优化往往是投入产出比最高的优化
1.2 核心Web指标(Core Web Vitals)
Google核心Web指标:
┌──────────────────────────────────────────────────────────────┐
│ │
│ LCP - Largest Contentful Paint(最大内容绘制) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 定义:视口内最大可见内容元素的渲染时间 │ │
│ │ │ │
│ │ 目标值: │ │
│ │ 🟢 Good: ≤ 2.5s │ │
│ │ 🟡 Needs Improvement: ≤ 4.0s │ │
│ │ 🔴 Poor: > 4.0s │ │
│ │ │ │
│ │ 常见LCP元素: │ │
│ │ - <img> 图片 │ │
│ │ - <video> 视频封面 │ │
│ │ - 背景图片(通过CSS url()加载) │ │
│ │ - 块级文本节点 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ FID - First Input Delay(首次输入延迟)→ INP(交互到绘制) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 定义:用户首次交互到浏览器响应的时间 │ │
│ │ │ │
│ │ 目标值: │ │
│ │ 🟢 Good: ≤ 100ms │ │
│ │ 🟡 Needs Improvement: ≤ 300ms │ │
│ │ 🔴 Poor: > 300ms │ │
│ │ │ │
│ │ 优化方向: │ │
│ │ - 减少主线程阻塞 │ │
│ │ - 代码分割和懒加载 │ │
│ │ - Web Workers处理复杂计算 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ CLS - Cumulative Layout Shift(累积布局偏移) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 定义:页面生命周期内发生的所有意外布局偏移的总和 │ │
│ │ │ │
│ │ 目标值: │ │
│ │ 🟢 Good: ≤ 0.1 │ │
│ │ 🟡 Needs Improvement: ≤ 0.25 │ │
│ │ 🔴 Poor: > 0.25 │ │
│ │ │ │
│ │ 常见原因: │ │
│ │ - 图片/视频无尺寸属性 │ │
│ │ - 字体加载导致FOIT/FOUT │ │
│ │ - 动态插入内容 │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ TTFB - Time to First Byte(首字节时间) │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ 定义:浏览器收到响应第一个字节的时间 │ │
│ │ │ │
│ │ 目标值: │ │
│ │ 🟢 Good: ≤ 600ms │ │
│ │ 🟡 Needs Improvement: ≤ 800ms │ │
│ │ 🔴 Poor: > 800ms │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
二、资源加载优化
2.1 关键渲染路径优化
关键渲染路径(Critical Rendering Path):
┌──────────────────────────────────────────────────────────────┐
│ │
│ 浏览器渲染流程: │
│ │
│ HTML ──▶ DOM树 ──┐ │
│ ├──▶ 渲染树 ──▶ 布局 ──▶ 绘制 ──▶ 合成 │
│ CSS ───▶ CSSOM ──┘ │
│ │
│ 阻塞渲染的资源: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ CSS(Render Blocking) │ │
│ │ - 浏览器会等待所有CSS加载完成才渲染 │ │
│ │ - 解决方案: │ │
│ │ 1. 内联关键CSS(Critical CSS) │ │
│ │ 2. 异步加载非关键CSS │ │
│ │ 3. 使用media查询减少阻塞 │ │
│ ├──────────────────────────────────────────────────────┤ │
│ │ JavaScript(Parser Blocking) │ │
│ │ - 同步JS会阻塞HTML解析 │ │
│ │ - 解决方案: │ │
│ │ 1. async属性(异步下载,下载完立即执行) │ │
│ │ 2. defer属性(异步下载,DOM解析后执行) │ │
│ │ 3. 代码分割,延迟加载非关键JS │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
2.2 Vue3性能优化实战
<!-- Vue3性能优化示例:产品详情页 --> <template> <div class="product-page"> <!-- 1. 骨架屏优化首屏体验 --> <ProductSkeleton v-if="loading" /> <template v-else> <!-- 2. 关键内容优先渲染 --> <ProductHeader :product="product" @add-to-cart="handleAddToCart" /> <!-- 3. 异步加载非关键组件 --> <Suspense> <template #default> <ProductGallery :images="product.images" /> </template> <template #fallback> <ImageSkeleton /> </template> </Suspense> <!-- 4. 懒加载非首屏内容 --> <LazyProductSpecs :specs="product.specs" /> <LazyProductReviews :product-id="product.id" /> <LazyRelatedProducts :category="product.category" /> </template> </div> </template> <script setup> import { defineAsyncComponent, ref, onMounted } from 'vue'; import { useIntersectionObserver } from '@vueuse/core'; // 同步加载关键组件 import ProductHeader from './components/ProductHeader.vue'; import ProductSkeleton from './components/ProductSkeleton.vue'; // 异步加载非关键组件(自动代码分割) const ProductGallery = defineAsyncComponent(() => import('./components/ProductGallery.vue') ); // 懒加载非首屏组件 const LazyProductSpecs = defineAsyncComponent(() => import('./components/ProductSpecs.vue') ); const LazyProductReviews = defineAsyncComponent(() => import('./components/ProductReviews.vue') ); const LazyRelatedProducts = defineAsyncComponent(() => import('./components/RelatedProducts.vue') ); const loading = ref(true); const product = ref(null); // 预加载关键数据 onMounted(async () => { // 使用Promise.race确保首屏数据快速返回 const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 3000) ); try { product.value = await Promise.race([ fetchProduct(), timeout ]); } catch (e) { // 降级处理:显示简化版页面 product.value = await fetchProductMinimal(); } finally { loading.value = false; } }); // 图片懒加载指令 const vLazy = { mounted(el, binding) { const { stop } = useIntersectionObserver( el, ([{ isIntersecting }]) => { if (isIntersecting) { el.src = binding.value; stop(); } }, { rootMargin: '50px' } ); } }; </script> <style> /* 5. 关键CSS内联 */ /* 这些样式应该内联到HTML中 */ .product-page { contain: layout style paint; } .product-header { content-visibility: auto; contain-intrinsic-size: 0 500px; } /* 6. 使用CSS Containment优化渲染 */ .product-card { contain: layout style; } /* 7. will-change优化动画 */ .product-image:hover { will-change: transform; transform: scale(1.05); } </style>
2.3 资源预加载策略
<!-- index.html 资源预加载配置 -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- 1. DNS预解析 - 提前解析可能访问的域名 -->
<link rel="dns-prefetch" href="//cdn.example.com">
<link rel="dns-prefetch" href="//api.example.com">
<link rel="dns-prefetch" href="//img.example.com">
<!-- 2. 预连接 - 提前建立TCP连接 -->
<link rel="preconnect" href="https://api.example.com" crossorigin>
<link rel="preconnect" href="https://cdn.example.com" crossorigin>
<!-- 3. 预加载关键资源 -->
<!-- 关键CSS -->
<link rel="preload" href="/css/critical.css" as="style">
<!-- 关键字体 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<!-- 首屏图片 -->
<link rel="preload" href="/images/hero.webp" as="image" type="image/webp">
<!-- 关键JS模块 -->
<link rel="modulepreload" href="/js/app.js">
<!-- 4. 预获取下一页资源 -->
<link rel="prefetch" href="/js/product-page.js">
<link rel="prefetch" href="/css/product-page.css">
<!-- 5. 预渲染(对确定会访问的页面) -->
<link rel="prerender" href="/product-list">
<!-- 内联关键CSS -->
<style>
/* Critical CSS - 首屏必需样式 */
body { margin: 0; font-family: system-ui, sans-serif; }
.header { height: 60px; background: #fff; }
.hero { min-height: 400px; }
/* ... */
</style>
<!-- 异步加载非关键CSS -->
<link rel="preload" href="/css/non-critical.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="/css/non-critical.css"></noscript>
</head>
<body>
<div id="app"></div>
<!-- 异步加载主JS -->
<script type="module" src="/js/app.js"></script>
<!-- 预加载关键数据 -->
<link rel="preload" href="/api/config" as="fetch" crossorigin>
</body>
</html>
三、图片优化策略
3.1 现代图片格式与响应式图片
<!-- Vue3响应式图片组件 --> <template> <picture class="responsive-image"> <!-- 1. AVIF格式(最佳压缩率,浏览器支持逐步完善) --> <source :srcset="avifSrcset" type="image/avif" sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw" > <!-- 2. WebP格式(广泛支持,比JPEG小25-35%) --> <source :srcset="webpSrcset" type="image/webp" sizes="(max-width: 600px) 100vw, (max-width: 1200px) 50vw, 33vw" > <!-- 3. JPEG回退 --> <img :src="fallbackSrc" :srcset="jpegSrcset" :alt="alt" :width="width" :height="height" loading="lazy" decoding="async" @load="handleLoad" > </picture> </template> <script setup> import { computed } from 'vue'; const props = defineProps({ src: String, alt: String, width: Number, height: Number, sizes: { type: Array, default: () => [320, 640, 960, 1280, 1920] } }); // 生成响应式srcset const generateSrcset = (format) => { return props.sizes .map(size => `/images/${props.src}-${size}w.${format} ${size}w`) .join(', '); }; const avifSrcset = computed(() => generateSrcset('avif')); const webpSrcset = computed(() => generateSrcset('webp')); const jpegSrcset = computed(() => generateSrcset('jpg')); const fallbackSrc = computed(() => `/images/${props.src}-640w.jpg`); const handleLoad = () => { // 图片加载完成后的处理 console.log('Image loaded:', props.src); }; </script> <style scoped> .responsive-image img { width: 100%; height: auto; /* 防止布局偏移 */ aspect-ratio: attr(width) / attr(height); } </style>
3.2 图片懒加载与占位
<!-- 智能图片懒加载组件 --> <template> <div class="lazy-image-container" :style="{ aspectRatio: aspectRatio }" > <!-- 1. 低质量占位图(LQIP - Low Quality Image Placeholder) --> <img v-if="!loaded" :src="placeholderSrc" class="placeholder" :alt="alt" > <!-- 2. 实际图片 --> <img ref="imageRef" :src="currentSrc" :alt="alt" :class="['lazy-image', { loaded }]" @load="onLoad" > </div> </template> <script setup> import { ref, computed, onMounted } from 'vue'; import { useIntersectionObserver } from '@vueuse/core'; const props = defineProps({ src: String, placeholder: String, // Base64编码的模糊图 alt: String, width: Number, height: Number, threshold: { type: Number, default: 0.1 } }); const imageRef = ref(null); const loaded = ref(false); const inView = ref(false); const aspectRatio = computed(() => `${props.width} / ${props.height}`); const placeholderSrc = computed(() => props.placeholder || generatePlaceholder()); const currentSrc = computed(() => inView.value ? props.src : ''); // 生成SVG占位图 const generatePlaceholder = () => { const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${props.width} ${props.height}"> <rect fill="#f0f0f0" width="100%" height="100%"/> </svg>`; return `data:image/svg+xml,${encodeURIComponent(svg)}`; }; // 使用Intersection Observer实现懒加载 onMounted(() => { const { stop } = useIntersectionObserver( imageRef, ([{ isIntersecting }]) => { if (isIntersecting) { inView.value = true; stop(); } }, { threshold: props.threshold, rootMargin: '50px' } ); }); const onLoad = () => { loaded.value = true; }; </script> <style scoped> .lazy-image-container { position: relative; overflow: hidden; background: #f0f0f0; } .placeholder { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; filter: blur(10px); transform: scale(1.1); } .lazy-image { width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity 0.3s ease; } .lazy-image.loaded { opacity: 1; } </style>
3.3 图片CDN与自动优化
/**
* 图片URL构建工具
* 支持多种CDN的图片优化参数
*/
export class ImageOptimizer {
constructor(config = {}) {
this.cdn = config.cdn || 'cloudinary'; // cloudinary, imgix, aliyun
this.baseUrl = config.baseUrl || '';
}
/**
* 构建优化后的图片URL
*/
buildUrl(src, options = {}) {
const {
width,
height,
format = 'auto', // auto, webp, avif, jpg
quality = 80,
fit = 'cover', // cover, contain, fill, inside, outside
loading = 'lazy'
} = options;
switch (this.cdn) {
case 'cloudinary':
return this.buildCloudinaryUrl(src, { width, height, format, quality, fit });
case 'imgix':
return this.buildImgixUrl(src, { width, height, format, quality, fit });
case 'aliyun':
return this.buildAliyunUrl(src, { width, height, format, quality, fit });
default:
return src;
}
}
buildCloudinaryUrl(src, options) {
const { width, height, format, quality, fit } = options;
const transforms = [
`q_${quality}`,
`f_${format}`,
width && `w_${width}`,
height && `h_${height}`,
fit && `c_${fit}`
].filter(Boolean).join(',');
return `${this.baseUrl}/image/upload/${transforms}/${src}`;
}
buildImgixUrl(src, options) {
const { width, height, format, quality, fit } = options;
const params = new URLSearchParams({
q: quality,
fm: format,
...(width && { w: width }),
...(height && { h: height }),
...(fit && { fit })
});
return `${this.baseUrl}/${src}?${params.toString()}`;
}
buildAliyunUrl(src, options) {
const { width, height, format, quality } = options;
const params = new URLSearchParams({
'x-oss-process': `image/resize${width ? `,w_${width}` : ''}${height ? `,h_${height}` : ''}/quality,q_${quality}/format,${format}`
});
return `${this.baseUrl}/${src}?${params.toString()}`;
}
/**
* 生成响应式srcset
*/
generateSrcset(src, sizes, options = {}) {
return sizes
.map(size => {
const url = this.buildUrl(src, { ...options, width: size });
return `${url} ${size}w`;
})
.join(', ');
}
}
// 使用示例
const optimizer = new ImageOptimizer({
cdn: 'cloudinary',
baseUrl: 'https://res.cloudinary.com/demo'
});
// 生成响应式图片URL
const srcset = optimizer.generateSrcset(
'sample.jpg',
[320, 640, 960, 1280],
{ format: 'webp', quality: 85 }
);
console.log(srcset);
// 输出: https://.../w_320,q_85,f_webp/sample.jpg 320w, https://.../w_640,q_85,f_webp/sample.jpg 640w, ...
四、代码优化与分割
4.1 Webpack/Vite代码分割
// vite.config.js - Vite代码分割配置
import { defineConfig } from 'vite';
import vue from '@vitejs/plugin-vue';
import { visualizer } from 'rollup-plugin-visualizer';
export default defineConfig({
plugins: [
vue(),
visualizer({ open: true }) // 打包分析
],
build: {
// 代码分割策略
rollupOptions: {
output: {
// 1. 手动代码分割
manualChunks: {
// 第三方库分离
'vendor': [
'vue',
'vue-router',
'pinia'
],
'ui': [
'element-plus',
'@element-plus/icons-vue'
],
'utils': [
'lodash-es',
'dayjs',
'axios'
]
},
// 2. 动态导入自动分割
// 组件级懒加载会自动分割
// 3. 输出文件名策略
entryFileNames: 'js/[name]-[hash].js',
chunkFileNames: 'js/[name]-[hash].js',
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.');
const ext = info[info.length - 1];
if (/\.(png|jpe?g|gif|svg|webp|avif)$/i.test(assetInfo.name)) {
return 'images/[name]-[hash][extname]';
}
if (/\.(css)$/i.test(assetInfo.name)) {
return 'css/[name]-[hash][extname]';
}
if (/\.(woff2?|ttf|otf|eot)$/i.test(assetInfo.name)) {
return 'fonts/[name]-[hash][extname]';
}
return 'assets/[name]-[hash][extname]';
}
}
},
// 4. 压缩配置
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true,
pure_funcs: ['console.log']
}
},
// 5. 资源内联阈值
assetsInlineLimit: 4096, // 4KB以下内联为base64
// 6. CSS代码分割
cssCodeSplit: true,
// 7. 源码映射(生产环境关闭)
sourcemap: false
},
// 依赖预构建优化
optimizeDeps: {
include: [
'vue',
'vue-router',
'pinia',
'element-plus',
'lodash-es'
],
exclude: ['@your-org/large-lib'] // 排除不需要预构建的包
}
});
4.2 路由级与组件级懒加载
// router/index.js - Vue Router懒加载
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Home',
component: () => import(/* webpackChunkName: "home" */ '../views/Home.vue')
},
{
path: '/product/:id',
name: 'Product',
component: () => import(/* webpackChunkName: "product" */ '../views/Product.vue'),
// 预加载策略
meta: {
prefetch: true
}
},
{
path: '/cart',
name: 'Cart',
component: () => import(/* webpackChunkName: "cart" */ '../views/Cart.vue')
},
{
path: '/user',
name: 'User',
component: () => import(/* webpackChunkName: "user" */ '../views/User.vue'),
children: [
{
path: 'orders',
component: () => import(/* webpackChunkName: "user-orders" */ '../views/user/Orders.vue')
},
{
path: 'settings',
component: () => import(/* webpackChunkName: "user-settings" */ '../views/user/Settings.vue')
}
]
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
// 智能预加载
router.beforeEach((to, from, next) => {
// 预加载下一页可能访问的路由
const matched = to.matched;
matched.forEach(route => {
if (route.meta.prefetch) {
const component = route.component;
if (typeof component === 'function') {
// 触发组件加载
component();
}
}
});
next();
});
export default router;
<!-- 组件级懒加载示例 --> <template> <div class="dashboard"> <!-- 图表组件 - 重量级,需要懒加载 --> <Suspense> <template #default> <ChartComponent :data="chartData" /> </template> <template #fallback> <ChartSkeleton /> </template> </Suspense> <!-- 地图组件 - 仅在需要时加载 --> <LazyMap v-if="showMap" :locations="locations" /> <!-- 富文本编辑器 - 延迟加载 --> <LazyEditor v-if="isEditing" v-model="content" /> </div> </template> <script setup> import { defineAsyncComponent, ref, onMounted } from 'vue'; // 同步加载轻量级组件 import ChartSkeleton from './components/ChartSkeleton.vue'; // 异步加载重量级组件 const ChartComponent = defineAsyncComponent({ loader: () => import('./components/ChartComponent.vue'), loadingComponent: ChartSkeleton, delay: 200, // 延迟显示loading状态 timeout: 3000 // 超时时间 }); // 使用魔法注释进行代码分割 const LazyMap = defineAsyncComponent(() => import(/* webpackChunkName: "map" */ './components/Map.vue') ); const LazyEditor = defineAsyncComponent(() => import(/* webpackChunkName: "editor" */ './components/RichEditor.vue') ); // 使用requestIdleCallback延迟加载非关键组件 const showMap = ref(false); const isEditing = ref(false); onMounted(() => { // 在浏览器空闲时加载地图组件 if ('requestIdleCallback' in window) { requestIdleCallback(() => { showMap.value = true; }, { timeout: 2000 }); } else { setTimeout(() => { showMap.value = true; }, 2000); } }); </script>
五、缓存策略与Service Worker
5.1 HTTP缓存策略
# Nginx缓存配置 server { listen 80; server_name example.com; root /var/www/html; # 1. 强缓存 - 长期不变的资源 location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ { expires 1y; add_header Cache-Control "public, immutable"; add_header Vary "Accept-Encoding"; # 启用gzip/brotli压缩 gzip on; gzip_types text/css application/javascript image/svg+xml; } # 2. 协商缓存 - 可能变化的资源 location ~* \.(html|json|xml)$ { expires 1h; add_header Cache-Control "public, must-revalidate"; add_header ETag $request_filename; } # 3. API响应缓存 location /api/ { proxy_pass http://backend; # 根据响应类型设置缓存 location ~* \.(json)$ { expires 5m; add_header Cache-Control "public, must-revalidate"; } } # 4. 不缓存的资源 location ~* \.(manifest|appcache)$ { expires -1; add_header Cache-Control "no-store, no-cache, must-revalidate"; } }
5.2 Service Worker缓存策略
// sw.js - Service Worker缓存策略
const CACHE_NAME = 'app-v1';
const STATIC_ASSETS = [
'/',
'/index.html',
'/css/critical.css',
'/js/app.js',
'/offline.html'
];
// 安装:预缓存关键资源
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('Pre-caching static assets');
return cache.addAll(STATIC_ASSETS);
})
.then(() => self.skipWaiting())
);
});
// 激活:清理旧缓存
self.addEventListener('activate', (event) => {
event.waitUntil(
caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames
.filter((name) => name !== CACHE_NAME)
.map((name) => caches.delete(name))
);
}).then(() => self.clients.claim())
);
});
// 拦截请求
self.addEventListener('fetch', (event) => {
const { request } = event;
const url = new URL(request.url);
// 1. 静态资源:Cache First
if (isStaticAsset(url)) {
event.respondWith(cacheFirst(request));
return;
}
// 2. API请求:Network First
if (isAPIRequest(url)) {
event.respondWith(networkFirst(request));
return;
}
// 3. 页面请求:Stale While Revalidate
if (isPageRequest(url)) {
event.respondWith(staleWhileRevalidate(request));
return;
}
// 默认:网络请求
event.respondWith(fetch(request));
});
// 缓存策略实现
// Cache First:优先使用缓存,缓存未命中再请求网络
async function cacheFirst(request) {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
if (cached) {
return cached;
}
try {
const response = await fetch(request);
cache.put(request, response.clone());
return response;
} catch (error) {
return new Response('Offline', { status: 503 });
}
}
// Network First:优先请求网络,失败时回退到缓存
async function networkFirst(request) {
const cache = await caches.open(CACHE_NAME);
try {
const networkResponse = await fetch(request);
cache.put(request, networkResponse.clone());
return networkResponse;
} catch (error) {
const cached = await cache.match(request);
if (cached) {
return cached;
}
throw error;
}
}
// Stale While Revalidate:立即返回缓存,同时更新缓存
async function staleWhileRevalidate(request) {
const cache = await caches.open(CACHE_NAME);
const cached = await cache.match(request);
const networkPromise = fetch(request)
.then((response) => {
cache.put(request, response.clone());
return response;
})
.catch(() => null);
return cached || networkPromise;
}
// 辅助函数
function isStaticAsset(url) {
return /\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$/.test(url.pathname);
}
function isAPIRequest(url) {
return url.pathname.startsWith('/api/');
}
function isPageRequest(url) {
return url.pathname === '/' || /\.(html)$/.test(url.pathname);
}
// 后台同步
self.addEventListener('sync', (event) => {
if (event.tag === 'sync-cart') {
event.waitUntil(syncCartData());
}
});
// 推送通知
self.addEventListener('push', (event) => {
const data = event.data.json();
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: '/icon.png',
badge: '/badge.png'
})
);
});
六、性能监控与分析
6.1 Web Vitals监控
// utils/performance.js - 性能监控工具
import { getCLS, getFID, getFCP, getLCP, getTTFB, getINP } from 'web-vitals';
/**
* 初始化性能监控
*/
export function initPerformanceMonitoring() {
// 核心Web指标
getCLS(sendToAnalytics);
getFID(sendToAnalytics);
getFCP(sendToAnalytics);
getLCP(sendToAnalytics);
getTTFB(sendToAnalytics);
getINP(sendToAnalytics); // 新的交互指标
// 自定义性能指标
measureResourceLoading();
measureAPIPerformance();
measureLongTasks();
}
/**
* 发送数据到分析平台
*/
function sendToAnalytics(metric) {
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating, // good, needs-improvement, poor
delta: metric.delta,
entries: metric.entries,
id: metric.id,
navigationType: metric.navigationType,
// 添加业务上下文
page: window.location.pathname,
timestamp: Date.now(),
userAgent: navigator.userAgent,
connection: navigator.connection?.effectiveType
});
// 使用sendBeacon确保数据发送
if (navigator.sendBeacon) {
navigator.sendBeacon('/analytics/performance', body);
} else {
fetch('/analytics/performance', {
method: 'POST',
body,
keepalive: true
});
}
}
/**
* 资源加载性能监控
*/
function measureResourceLoading() {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'resource') {
// 只监控关键资源
if (isCriticalResource(entry.name)) {
console.log('Resource loaded:', {
name: entry.name,
duration: entry.duration,
size: entry.transferSize,
type: entry.initiatorType
});
}
}
}
});
observer.observe({ entryTypes: ['resource'] });
}
/**
* API性能监控
*/
function measureAPIPerformance() {
const originalFetch = window.fetch;
window.fetch = async function(...args) {
const start = performance.now();
const url = args[0];
try {
const response = await originalFetch.apply(this, args);
const duration = performance.now() - start;
// 上报API性能
sendToAnalytics({
name: 'api-timing',
value: duration,
entries: [{ url, status: response.status }]
});
return response;
} catch (error) {
const duration = performance.now() - start;
sendToAnalytics({
name: 'api-error',
value: duration,
entries: [{ url, error: error.message }]
});
throw error;
}
};
}
/**
* 长任务监控
*/
function measureLongTasks() {
if ('PerformanceLongTaskTiming' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.warn('Long task detected:', {
duration: entry.duration,
startTime: entry.startTime,
attribution: entry.attribution
});
}
});
observer.observe({ entryTypes: ['longtask'] });
}
}
function isCriticalResource(url) {
const criticalPatterns = [
/critical\.css/,
/app\.js/,
/hero\.(jpg|png|webp)/
];
return criticalPatterns.some(pattern => pattern.test(url));
}
/**
* 性能标记和测量
*/
export function mark(name) {
performance.mark(name);
}
export function measure(name, startMark, endMark) {
performance.measure(name, startMark, endMark);
const entries = performance.getEntriesByName(name);
return entries[entries.length - 1];
}
// 使用示例
export function trackPageLoad() {
mark('page-start');
window.addEventListener('load', () => {
mark('page-load');
const measurement = measure('page-load-time', 'page-start', 'page-load');
console.log('Page load time:', measurement.duration);
});
}
6.2 性能优化检查清单
┌─────────────────────────────────────────────────────────────────────┐
│ 前端性能优化检查清单 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 【资源优化】 │
│ □ 1. 启用Brotli/Gzip压缩 │
│ □ 2. 图片使用WebP/AVIF格式 │
│ □ 3. 实现响应式图片srcset │
│ □ 4. 图片懒加载loading="lazy" │
│ □ 5. 字体使用woff2格式 │
│ □ 6. 内联关键CSS │
│ □ 7. 异步加载非关键CSS │
│ │
│ 【代码优化】 │
│ □ 1. 路由级代码分割 │
│ □ 2. 组件级懒加载 │
│ □ 3. Tree Shaking移除无用代码 │
│ □ 4. JS/CSS压缩和混淆 │
│ □ 5. 移除console和debugger │
│ □ 6. 使用CDN加载第三方库 │
│ │
│ 【缓存策略】 │
│ □ 1. 静态资源长期缓存 │
│ □ 2. 使用文件名hash │
│ □ 3. 配置Service Worker │
│ □ 4. 使用localStorage缓存数据 │
│ │
│ 【渲染优化】 │
│ □ 1. 减少DOM节点数量 │
│ □ 2. 使用CSS Containment │
│ □ 3. 避免强制同步布局 │
│ □ 4. 使用transform代替top/left │
│ □ 5. 使用will-change优化动画 │
│ □ 6. 虚拟列表处理长列表 │
│ │
│ 【网络优化】 │
│ □ 1. 启用HTTP/2或HTTP/3 │
│ □ 2. 使用DNS预解析 │
│ □ 3. 使用预连接 │
│ □ 4. 预加载关键资源 │
│ □ 5. 使用资源提示prefetch/prerender │
│ │
│ 【监控指标】 │
│ □ 1. LCP < 2.5s │
│ □ 2. FID/INP < 100ms │
│ □ 3. CLS < 0.1 │
│ □ 4. TTFB < 600ms │
│ □ 5. Lighthouse评分 > 90 │
│ │
└─────────────────────────────────────────────────────────────────────┘
七、经验总结
7.1 常见性能问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
| LCP过高 | 首屏图片过大 | 压缩图片、使用WebP、响应式图片 |
| CLS偏移 | 图片无尺寸 | 设置width/height、使用aspect-ratio |
| FID延迟 | 主线程阻塞 | 代码分割、Web Workers、延迟加载 |
| TTFB过高 | 服务端慢 | 启用缓存、CDN、优化数据库查询 |
| JS体积大 | 未代码分割 | 路由懒加载、Tree Shaking |
| 缓存失效 | 文件名无hash | 配置webpack contenthash |
7.2 性能优化优先级
┌─────────────────┐
│ 性能优化优先级 │
└────────┬────────┘
│
▼
┌──────────────────────────────┐
│ 高影响 + 低投入 │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ • 图片优化(WebP/压缩) │
│ • 启用Gzip/Brotli │
│ • 配置HTTP缓存 │
│ • 异步加载非关键JS │
└─────────────┬────────────────┘
│
▼
┌──────────────────────────────┐
│ 高影响 + 中等投入 │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ • 代码分割 │
│ • 懒加载组件 │
│ • Service Worker │
│ • 关键CSS内联 │
└─────────────┬────────────────┘
│
▼
┌──────────────────────────────┐
│ 中等影响 + 高投入 │
│ ━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ • SSR/SSG │
│ • 边缘计算 │
│ • 微前端架构 │
└──────────────────────────────┘
系列上一篇:TLS/SSL安全传输原理与实践
系列下一篇:后端服务优化策略
知识点测试
读完文章了?来测试一下你对知识点的掌握程度吧!
评论区
使用 GitHub 账号登录后即可发表评论,支持 Markdown 格式。
如果评论系统无法加载,请确保:
- 您的网络可以访问 GitHub
- giscus GitHub App 已安装到仓库
- 仓库已启用 Discussions 功能