跳转至

前端性能优化

📚 章节目标

本章节将全面介绍前端性能优化的各种技术和策略,包括代码分割、懒加载、缓存策略、渲染优化等,帮助学习者掌握前端性能优化的核心方法。

学习目标

  1. 理解前端性能优化的核心指标
  2. 掌握代码分割和懒加载技术
  3. 掌握缓存策略优化
  4. 掌握渲染性能优化
  5. 掌握网络性能优化
  6. 掌握性能监控和分析工具

📊 性能指标

1. Core Web Vitals

1.1 LCP (Largest Contentful Paint)

JavaScript
// LCP - 最大内容绘制
// 衡量页面主要内容加载完成的时间
// 目标:< 2.5s

// 监控LCP
import { onLCP } from 'web-vitals';

onLCP((metric) => {
  console.log('LCP:', metric.value);
  // 发送到分析服务
  analytics.track('LCP', { value: metric.value });
});

// 优化LCP
// 1. 优化关键资源加载
// 2. 预加载重要资源
// 3. 优化服务器响应时间
// 4. 使用CDN加速
// 5. 压缩和优化图片

// 预加载关键资源
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/images/hero.jpg" as="image">
<link rel="preload" href="/styles/main.css" as="style">

1.2 INP (Interaction to Next Paint)

JavaScript
// INP - 交互到下一次绘制(已于 2024年3月取代 FID 成为 Core Web Vital)
// 衡量用户任意交互到视觉响应的时间
// 目标:< 200ms

// 监控INP
import { onINP } from 'web-vitals';

onINP((metric) => {
  console.log('INP:', metric.value);
  analytics.track('INP', { value: metric.value });
});

// 优化INP
// 1. 减少JavaScript执行时间
// 2. 代码分割和懒加载
// 3. 减少主线程工作
// 4. 使用Web Workers
// 5. 优化事件处理程序

// 使用Web Worker
const worker = new Worker('worker.js');

worker.postMessage({ data: largeData });

worker.onmessage = (e) => {
  console.log('Result:', e.data);
};

1.3 CLS (Cumulative Layout Shift)

JavaScript
// CLS - 累积布局偏移
// 衡量页面布局稳定性
// 目标:< 0.1

// 监控CLS
import { onCLS } from 'web-vitals';

onCLS((metric) => {
  console.log('CLS:', metric.value);
  analytics.track('CLS', { value: metric.value });
});

// 优化CLS
// 1. 为图片和视频设置尺寸
// 2. 为动态内容预留空间
// 3. 避免在现有内容上方插入内容
// 4. 使用CSS transform和opacity进行动画

// 为图片设置尺寸
<img
  src="image.jpg"
  width="800"
  height="600"
  loading="lazy"
  alt="Description"
>

// 为动态内容预留空间
<div style="min-height: 200px;">
  <!-- 动态加载的内容 -->
</div>

2. 其他性能指标

2.1 FCP (First Contentful Paint)

JavaScript
// FCP - 首次内容绘制
// 衡量浏览器首次渲染内容的时间
// 目标:< 1.8s

import { onFCP } from 'web-vitals';

onFCP((metric) => {
  console.log('FCP:', metric.value);
});

// 优化FCP
// 1. 减少阻塞渲染的资源
// 2. 内联关键CSS
// 3. 优化关键渲染路径
// 4. 使用预加载

// 内联关键CSS
<style>
  /* 关键CSS */
  body { margin: 0; padding: 0; }
  .header { background: #333; color: #fff; }
</style>
<link rel="preload" href="/styles/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">

2.2 TTI (Time to Interactive)

JavaScript
// TTI - 可交互时间
// 衡量页面完全可交互的时间
// 目标:< 3.8s
// 注意:TTI 是 Lighthouse 实验室指标,web-vitals 库不提供 TTI
// 可通过 Lighthouse API 或 PerformanceObserver 测量

// 优化TTI
// 1. 减少JavaScript执行时间
// 2. 延迟加载非关键JavaScript
// 3. 优化第三方脚本
// 4. 使用代码分割

📦 代码分割与懒加载

1. 代码分割

1.1 路由级代码分割

JavaScript
// React Router代码分割
import { lazy, Suspense } from 'react';
import { BrowserRouter, Routes, Route } from 'react-router-dom';

// 懒加载组件
const Home = lazy(() => import('./pages/Home'));
const About = lazy(() => import('./pages/About'));
const Contact = lazy(() => import('./pages/Contact'));

function App() {
  return (
    <BrowserRouter>
      <Suspense fallback={<div>Loading...</div>}>
        <Routes>
          <Route path="/" element={<Home />} />
          <Route path="/about" element={<About />} />
          <Route path="/contact" element={<Contact />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
}

// Vue Router代码分割
const routes = [
  {
    path: '/',
    component: () => import('./views/Home.vue'),
  },
  {
    path: '/about',
    component: () => import('./views/About.vue'),
  },
  {
    path: '/contact',
    component: () => import('./views/Contact.vue'),
  },
];

// Angular路由懒加载
const routes: Routes = [
  {
    path: '',
    loadChildren: () => import('./home/home.module').then(m => m.HomeModule),
  },
  {
    path: 'about',
    loadChildren: () => import('./about/about.module').then(m => m.AboutModule),
  },
];

1.2 组件级代码分割

JavaScript
// React组件懒加载
import { lazy, Suspense } from 'react';

const HeavyComponent = lazy(() => import('./HeavyComponent'));

function App() {
  const [showHeavy, setShowHeavy] = useState(false);

  return (
    <div>
      <button onClick={() => setShowHeavy(true)}>
        Load Heavy Component
      </button>
      {showHeavy && (
        <Suspense fallback={<div>Loading...</div>}>
          <HeavyComponent />
        </Suspense>
      )}
    </div>
  );
}

// Vue组件懒加载
<script setup>
import { defineAsyncComponent } from 'vue';

const HeavyComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
);
</script>

<template>
  <div>
    <button @click="showHeavy = true">Load Heavy Component</button>
    <Suspense v-if="showHeavy">
      <template #default>
        <HeavyComponent />
      </template>
      <template #fallback>
        <div>Loading...</div>
      </template>
    </Suspense>
  </div>
</template>

2. 动态导入

2.1 基础动态导入

JavaScript
// 动态导入模块
button.addEventListener('click', async () => {
  const module = await import('./heavy-module.js');
  module.doSomething();
});

// 条件动态导入
async function loadFeature() {
  if (user.hasPremium) {
    const premiumFeature = await import('./premium-feature.js');
    premiumFeature.init();
  }
}

// 动态导入带参数
const loadModule = async (moduleName) => {
  const module = await import(`./modules/${moduleName}.js`);
  return module.default;
};

// 使用
loadModule('featureA').then(FeatureA => {
  new FeatureA();
});

2.2 预加载

JavaScript
// 预加载模块
const preloadModule = (modulePath) => {
  const link = document.createElement('link');
  link.rel = 'preload';
  link.as = 'script';
  link.href = modulePath;
  document.head.appendChild(link);
};

// 预加载
preloadModule('/heavy-module.js');

// 后续使用
button.addEventListener('click', async () => {
  const module = await import('/heavy-module.js');
  module.doSomething();
});

3. Tree Shaking

3.1 配置Tree Shaking

JavaScript
// Webpack配置
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,
    sideEffects: false,
  },
};

// package.json配置
{
  "sideEffects": false
}

// 或者指定有副作用的文件
{
  "sideEffects": [
    "*.css",
    "./src/polyfills.js"
  ]
}

// 使用ES模块
// 好的做法 - 支持Tree Shaking
import { debounce } from 'lodash-es';

// 不好的做法 - 不支持Tree Shaking
import _ from 'lodash';

3.2 优化导入

JavaScript
// 好的做法 - 按需导入
import { Button, Input } from 'antd';

// 不好的做法 - 导入整个库
import antd from 'antd';

// 好的做法 - 使用Tree Shaking
import { map, filter, reduce } from 'lodash-es';

// 不好的做法 - 导入整个lodash
import _ from 'lodash';

🚀 缓存策略

1. 浏览器缓存

1.1 HTTP缓存

JavaScript
// 服务端设置缓存头
// Express.js示例
app.use(express.static('public', {
  maxAge: '1y', // 1年
  etag: true,
  lastModified: true,
}));

// Nginx配置
location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
  expires 1y;
  add_header Cache-Control "public, immutable";
}

// 缓存策略
// 1. 静态资源:长期缓存(1年)
// 2. HTML文件:不缓存或短时间缓存
// 3. API响应:根据内容设置缓存时间
// 4. 版本化资源:使用内容哈希

1.2 Service Worker缓存

JavaScript
// 注册Service Worker
if ('serviceWorker' in navigator) {
  window.addEventListener('load', () => {
    navigator.serviceWorker.register('/sw.js').then(registration => {
      console.log('SW registered:', registration);
    }).catch(error => {
      console.log('SW registration failed:', error);
    });
  });
}

// Service Worker实现
const CACHE_NAME = 'v1';
const urlsToCache = [
  '/',
  '/styles/main.css',
  '/scripts/main.js',
  '/images/logo.png',
];

self.addEventListener('install', (event) => {
  event.waitUntil(
    caches.open(CACHE_NAME).then((cache) => {
      return cache.addAll(urlsToCache);
    })
  );
});

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request).then((response) => {
      if (response) {
        return response;
      }
      return fetch(event.request);
    })
  );
});

// Workbox简化Service Worker
import { precacheAndRoute } from 'workbox-precaching';
import { registerRoute } from 'workbox-routing';
import { CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';

// 预缓存静态资源
precacheAndRoute(self.__WB_MANIFEST);

// 缓存策略
registerRoute(
  ({ request }) => request.destination === 'image',
  new CacheFirst({
    cacheName: 'images',
  })
);

registerRoute(
  ({ request }) => request.destination === 'script',
  new StaleWhileRevalidate({
    cacheName: 'scripts',
  })
);

2. 应用缓存

2.1 LocalStorage缓存

JavaScript
// 缓存数据
function cacheData(key, data, ttl = 3600000) {
  const item = {
    data,
    timestamp: Date.now(),
    ttl,
  };
  localStorage.setItem(key, JSON.stringify(item));
}

// 获取缓存数据
function getCachedData(key) {
  const item = localStorage.getItem(key);
  if (!item) return null;

  const { data, timestamp, ttl } = JSON.parse(item);
  const isExpired = Date.now() - timestamp > ttl;

  if (isExpired) {
    localStorage.removeItem(key);
    return null;
  }

  return data;
}

// 使用示例
async function fetchUsers() {
  const cached = getCachedData('users');
  if (cached) {
    return cached;
  }

  const response = await fetch('/api/users');
  const users = await response.json();
  cacheData('users', users, 600000); // 缓存10分钟
  return users;
}

2.2 IndexedDB缓存

JavaScript
// 使用IndexedDB
import { openDB } from 'idb';

async function initDB() {
  return openDB('my-app', 1, {
    upgrade(db) {
      if (!db.objectStoreNames.contains('users')) {
        db.createObjectStore('users', { keyPath: 'id' });
      }
    },
  });
}

async function cacheUsers(users) {
  const db = await initDB();
  const tx = db.transaction('users', 'readwrite');
  const store = tx.objectStore('users');

  for (const user of users) {
    await store.put(user);
  }

  await tx.done;
}

async function getCachedUsers() {
  const db = await initDB();
  return db.getAll('users');
}

🎨 渲染性能优化

1. DOM优化

1.1 减少重排和重绘

JavaScript
// 不好的做法 - 多次修改DOM
const element = document.getElementById('myElement');
element.style.width = '100px';
element.style.height = '100px';
element.style.backgroundColor = 'red';

// 好的做法 - 批量修改
const element = document.getElementById('myElement');
element.style.cssText = 'width: 100px; height: 100px; background-color: red;';

// 或者使用类
element.className = 'styled-element';

// 使用DocumentFragment批量添加元素
const fragment = document.createDocumentFragment();

for (let i = 0; i < 1000; i++) {
  const li = document.createElement('li');
  li.textContent = `Item ${i}`;
  fragment.appendChild(li);
}

document.getElementById('list').appendChild(fragment);

1.2 虚拟DOM优化

JavaScript
// React - 使用key优化列表
function TodoList({ todos }) {
  return (
    <ul>
      {todos.map(todo => (  // map转换每个元素;filter筛选;reduce累积
        <li key={todo.id}>
          {todo.text}
        </li>
      ))}
    </ul>
  );
}

// 使用React.memo避免不必要的渲染
const TodoItem = React.memo(function TodoItem({ todo }) {
  return <li>{todo.text}</li>;
});

// 使用useMemo缓存计算结果
function ExpensiveComponent({ data }) {
  const processedData = useMemo(() => {
    return data.map(item => ({
      ...item,  // ...展开运算符:展开数组/对象
      value: item.value * 2,
    }));
  }, [data]);

  return <div>{processedData.map(item => <div key={item.id}>{item.value}</div>)}</div>;
}

2. CSS优化

2.1 CSS选择器优化

CSS
/* 不好的做法 - 过深的嵌套 */
.container .sidebar .menu .item .link {
  color: blue;
}

/* 好的做法 - 扁平化选择器 */
.menu-link {
  color: blue;
}

/* 不好的做法 - 使用通配符 */
* {
  margin: 0;
  padding: 0;
}

/* 好的做法 - 具体指定 */
body, h1, h2, h3, p {
  margin: 0;
  padding: 0;
}

2.2 CSS动画优化

CSS
/* 使用transform和opacity进行动画 */
.animated-element {
  transition: transform 0.3s ease, opacity 0.3s ease;
}

.animated-element:hover {
  transform: scale(1.1);
  opacity: 0.8;
}

/* 不好的做法 - 使用left/top进行动画 */
.bad-animation {
  transition: left 0.3s ease, top 0.3s ease;
}

/* 使用will-change提示浏览器 */
.will-change-element {
  will-change: transform, opacity;
}

/* 使用GPU加速 */
.gpu-accelerated {
  transform: translateZ(0);
  backface-visibility: hidden;
}

3. 图片优化

3.1 响应式图片

HTML
<!-- 使用srcset提供不同尺寸的图片 -->
<img
  src="image-small.jpg"
  srcset="
    image-small.jpg 400w,
    image-medium.jpg 800w,
    image-large.jpg 1200w
  "
  sizes="(max-width: 600px) 400px, (max-width: 1200px) 800px, 1200px"
  alt="Description"
>

<!-- 使用picture元素提供不同格式 -->
<picture>
  <source srcset="image.webp" type="image/webp">
  <source srcset="image.jpg" type="image/jpeg">
  <img src="image.jpg" alt="Description">
</picture>

<!-- 懒加载图片 -->
<img
  src="placeholder.jpg"
  data-src="actual-image.jpg"
  loading="lazy"
  alt="Description"
  class="lazy-load"
>

3.2 图片压缩和格式

JavaScript
// 使用sharp库压缩图片
const sharp = require('sharp');

async function optimizeImage(inputPath, outputPath) {
  await sharp(inputPath)
    .resize(800, 600, {
      fit: 'inside',
      withoutEnlargement: true,
    })
    .webp({ quality: 80 })
    .toFile(outputPath);
}

// 使用现代图片格式
// WebP - 比JPEG小25-35%
// AVIF - 比WebP小50%
// 使用picture元素提供降级方案

🌐 网络性能优化

1. HTTP优化

1.1 HTTP/2和HTTP/3

JavaScript
// 启用HTTP/2
// Nginx配置
listen 443 ssl http2;

// 启用HTTP/3
// Nginx配置
listen 443 quic reuseport;

// 使用HTTP/2 Server Push
// Nginx配置
http {
  server {
    location / {
      http2_push /style.css;
      http2_push /script.js;
    }
  }
}

1.2 预连接

HTML
<!-- 预连接到重要域名 -->
<link rel="preconnect" href="https://api.example.com">
<link rel="preconnect" href="https://cdn.example.com">

<!-- DNS预解析 -->
<link rel="dns-prefetch" href="https://example.com">

<!-- 预加载关键资源 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/styles/main.css" as="style">
<link rel="preload" href="/scripts/main.js" as="script">

2. 资源优化

2.1 资源压缩

JavaScript
// Webpack配置压缩
module.exports = {
  optimization: {
    minimize: true,
    minimizer: [
      new TerserPlugin({
        terserOptions: {
          compress: {
            drop_console: true,
          },
        },
      }),
      new CssMinimizerPlugin(),
    ],
  },
};

// Gzip压缩
const compression = require('compression');
app.use(compression());

// Brotli压缩(Node.js 11.7+ 内置 zlib.brotliCompress)
// 使用 shrink-ray-current 中间件同时支持 Gzip 和 Brotli
const shrinkRay = require('shrink-ray-current');
app.use(shrinkRay());
// 或在 Nginx 层配置:brotli on; brotli_types text/html text/css application/javascript;

2.2 CDN加速

JavaScript
// 使用CDN加速静态资源
const CDN_URL = 'https://cdn.example.com';

// 在HTML中使用
<link rel="stylesheet" href="${CDN_URL}/styles/main.css">
<script src="${CDN_URL}/scripts/main.js"></script>

// 在代码中使用
const imageUrl = `${CDN_URL}/images/logo.png`;

📈 性能监控

1. Web Vitals监控

JavaScript
// 监控Core Web Vitals(web-vitals v4+ API)
import { onCLS, onINP, onLCP } from 'web-vitals';

function sendToAnalytics(metric) {
  // 发送到分析服务
  analytics.track('Web Vitals', {
    name: metric.name,
    value: metric.value,
    id: metric.id,
  });
}

onCLS(sendToAnalytics);
onINP(sendToAnalytics);
onLCP(sendToAnalytics);

// 自定义性能指标
function measureCustomMetric() {
  const start = performance.now();

  // 执行操作
  doSomething();

  const end = performance.now();
  const duration = end - start;

  analytics.track('Custom Metric', { duration });
}

2. 性能分析工具

2.1 Lighthouse

JavaScript
// 使用Lighthouse CLI
lighthouse https://example.com --output html --output-path report.html

// 使用Lighthouse API
// ⚠️ 注意:Lighthouse 12+ 已改用 ESM 导入:import lighthouse from 'lighthouse';
// 且 startFlow / navigation 模式取代了直接调用 lighthouse(url, options)。
// 以下为旧版 API 写法,仅供参考。详见:https://github.com/GoogleChrome/lighthouse/blob/main/docs/user-flows.md
const lighthouse = require('lighthouse');
const chromeLauncher = require('chrome-launcher');

async function runLighthouse(url) {  // async定义异步函数;await等待Promise完成
  const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] });  // await等待异步操作完成
  const options = {
    logLevel: 'info',
    output: 'html',
    onlyCategories: ['performance'],
    port: chrome.port,
  };

  const runnerResult = await lighthouse(url, options);
  await chrome.kill();

  return runnerResult;
}

runLighthouse('https://example.com').then(results => {
  console.log('Performance score:', results.lhr.categories.performance.score * 100);
});

2.2 Chrome DevTools

JavaScript
// 使用Performance API(Navigation Timing Level 2)
// 测量页面加载时间
window.addEventListener('load', () => {
  const [navEntry] = performance.getEntriesByType('navigation');  // 解构赋值:从对象/数组提取值
  if (navEntry) {
    const pageLoadTime = navEntry.loadEventEnd - navEntry.startTime;
    console.log('Page load time:', pageLoadTime);
  }
});

// 使用User Timing API
performance.mark('startOperation');
// 执行操作
performance.mark('endOperation');
performance.measure('operation', 'startOperation', 'endOperation');

const measure = performance.getEntriesByName('operation')[0];
console.log('Operation duration:', measure.duration);

📝 练习题

1. 基础题

题目1:实现一个懒加载组件

JavaScript
import { lazy, Suspense } from 'react';

// 实现懒加载组件
const LazyComponent = lazy(() => {  // 箭头函数:简洁的函数语法
  // 动态导入组件
});

function App() {
  return (
    <div>
      {/* 使用懒加载组件 */}
    </div>
  );
}

2. 进阶题

题目2:实现一个缓存工具

JavaScript
// 实现缓存工具
class Cache {
  constructor() {
    // 初始化缓存
  }

  set(key, value, ttl) {
    // 设置缓存
  }

  get(key) {
    // 获取缓存
  }

  clear() {
    // 清除缓存
  }
}

// 使用示例
const cache = new Cache();  // const不可重新赋值;let块级作用域变量
cache.set('key', 'value', 60000); // 缓存60秒
console.log(cache.get('key'));

3. 面试题

题目3:解释重排和重绘的区别

JavaScript
// 答案要点:
// 1. 重排:元素位置、大小变化,需要重新计算布局
// 2. 重绘:元素外观变化,不需要重新计算布局
// 3. 重排必定导致重绘,重绘不一定导致重排
// 4. 优化方法:
//    - 批量修改DOM
//    - 使用DocumentFragment
//    - 使用transform和opacity进行动画
//    - 避免使用table布局

🎯 本章总结

本章节全面介绍了前端性能优化的各种技术和策略,包括性能指标、代码分割、缓存策略、渲染优化、网络优化、性能监控等。关键要点:

  1. 性能指标:理解Core Web Vitals和其他性能指标
  2. 代码分割:掌握路由级和组件级代码分割
  3. 缓存策略:掌握浏览器缓存、Service Worker缓存、应用缓存
  4. 渲染优化:掌握DOM优化、CSS优化、图片优化
  5. 网络优化:掌握HTTP优化、资源优化、CDN加速
  6. 性能监控:掌握性能监控工具和分析方法

下一步将深入学习前端工程化技术。