返回 筑基・网络云路秘径

前端性能优化实战

博主
大约 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 功能