引言

在当今的Web应用中,性能优化已经成为提升用户体验的关键因素。研究表明,加载时间每增加1秒,用户流失率就会显著上升。本文将从多个维度深入探讨前端性能优化策略,帮助你构建出响应迅速、运行流畅的Web应用。

性能指标与测量

Core Web Vitals

LCP(Largest Contentful Paint)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
// 测量LCP
import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals'

getLCP(console.log)
getFID(console.log)
getCLS(console.log)

// 使用Performance Observer
const observer = new PerformanceObserver((list) => {
  const entries = list.getEntries()
  const lastEntry = entries[entries.length - 1]
  console.log('LCP:', lastEntry.startTime)
})

observer.observe({ entryTypes: ['largest-contentful-paint'] })

FID(First Input Delay)

1
2
3
4
5
6
7
8
// 监听FID
const fidObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log('FID:', entry.processingStart - entry.startTime)
  }
})

fidObserver.observe({ entryTypes: ['first-input'] })

CLS(Cumulative Layout Shift)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// 监听CLS
let clsValue = 0
const clsObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    if (!entry.hadRecentInput) {
      clsValue += entry.value
    }
  }
  console.log('CLS:', clsValue)
})

clsObserver.observe({ entryTypes: ['layout-shift'] })

性能监控工具

Lighthouse自动化测试

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 使用Lighthouse CI
const lighthouse = require('lighthouse')
const chromeLauncher = require('chrome-launcher')

async function runLighthouse(url) {
  const chrome = await chromeLauncher.launch({ chromeFlags: ['--headless'] })
  const options = { logLevel: 'info', output: 'json', port: chrome.port }

  const runnerResult = await lighthouse(url, options)

  await chrome.kill()
  return runnerResult.lhr
}

自定义性能监控

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class PerformanceMonitor {
  constructor() {
    this.metrics = {}
    this.observers = []
  }

  startTiming(name) {
    performance.mark(`${name}-start`)
  }

  endTiming(name) {
    performance.mark(`${name}-end`)
    performance.measure(name, `${name}-start`, `${name}-end`)

    const measure = performance.getEntriesByName(name)[0]
    this.metrics[name] = measure.duration
    return measure.duration
  }

  observeResourceTiming() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        console.log(`${entry.name}: ${entry.duration}ms`)
      }
    })

    observer.observe({ entryTypes: ['resource'] })
    this.observers.push(observer)
  }
}

资源加载优化

图片优化

WebP格式支持

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
// Picture元素实现响应式图片
<picture>
  <source srcset="image.webp" type="image/webp">
  <source srcset="image.jpg" type="image/jpeg">
  <img src="image.jpg" alt="描述" loading="lazy">
</picture>

// 动态检测WebP支持
function supportsWebP() {
  return new Promise(resolve => {
    const webP = new Image()
    webP.onload = webP.onerror = () => {
      resolve(webP.height === 2)
    }
    webP.src = 'data:image/webp;base64,UklGRjoAAABXRUJQVlA4IC4AAACyAgCdASoCAAIALmk0mk0iIiIiIgBoSygABc6WWgAA/veff/0PP8bA//LwYAAA'
  })
}

图片懒加载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// Intersection Observer实现懒加载
class LazyLoader {
  constructor() {
    this.observer = new IntersectionObserver(
      this.handleIntersection.bind(this),
      { rootMargin: '50px' }
    )
  }

  observe(element) {
    this.observer.observe(element)
  }

  handleIntersection(entries) {
    entries.forEach(entry => {
      if (entry.isIntersecting) {
        const img = entry.target
        img.src = img.dataset.src
        img.srcset = img.dataset.srcset
        this.observer.unobserve(img)
      }
    })
  }
}

// 使用
const lazyLoader = new LazyLoader()
document.querySelectorAll('img[data-src]').forEach(img => {
  lazyLoader.observe(img)
})

脚本加载优化

非阻塞脚本加载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
<!-- 异步加载 -->
<script src="app.js" async></script>

<!-- 延迟加载 -->
<script src="analytics.js" defer></script>

<!-- 动态加载 -->
<script>
  function loadScript(src, async = true, defer = false) {
    const script = document.createElement('script')
    script.src = src
    script.async = async
    script.defer = defer

    return new Promise((resolve, reject) => {
      script.onload = resolve
      script.onerror = reject
      document.head.appendChild(script)
    })
  }

  // 按需加载
  document.addEventListener('DOMContentLoaded', () => {
    loadScript('/js/analytics.js', true, true)
  })
</script>

模块预加载

1
2
3
4
5
6
7
<!-- 预加载关键资源 -->
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/js/critical.js" as="script">

<!-- 预取可能需要的资源 -->
<link rel="prefetch" href="/js/next-page.js">
<link rel="dns-prefetch" href="//api.example.com">

CSS优化

关键CSS内联

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// 提取关键CSS
const criticalCSS = `
  body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }
  .header { background: #fff; padding: 1rem; }
`

// 内联到HTML
document.head.insertAdjacentHTML('beforeend',
  `<style>${criticalCSS}</style>`
)

CSS异步加载

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<!-- 防阻塞性CSS加载 -->
<link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
<noscript><link rel="stylesheet" href="styles.css"></noscript>

<!-- 或者使用JavaScript -->
<script>
  function loadCSS(href) {
    const link = document.createElement('link')
    link.rel = 'stylesheet'
    link.href = href
    document.head.appendChild(link)
  }

  // 在交互后加载非关键CSS
  window.addEventListener('load', () => {
    loadCSS('/styles/non-critical.css')
  })
</script>

代码优化

JavaScript优化

防抖与节流

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 防抖
function debounce(func, wait) {
  let timeout
  return function executedFunction(...args) {
    const later = () => {
      clearTimeout(timeout)
      func(...args)
    }
    clearTimeout(timeout)
    timeout = setTimeout(later, wait)
  }
}

// 节流
function throttle(func, limit) {
  let inThrottle
  return function(...args) {
    if (!inThrottle) {
      func.apply(this, args)
      inThrottle = true
      setTimeout(() => inThrottle = false, limit)
    }
  }
}

// 使用示例
const searchInput = document.getElementById('search')
searchInput.addEventListener('input', debounce(handleSearch, 300))

window.addEventListener('scroll', throttle(handleScroll, 100))

虚拟滚动

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
class VirtualList {
  constructor(container, itemHeight, renderItem) {
    this.container = container
    this.itemHeight = itemHeight
    this.renderItem = renderItem
    this.visibleItems = []
    this.startIndex = 0
    this.endIndex = 0

    this.init()
  }

  init() {
    this.container.style.overflowY = 'auto'
    this.container.style.height = '400px'

    // 创建占位元素
    this.placeholder = document.createElement('div')
    this.container.appendChild(this.placeholder)

    this.container.addEventListener('scroll',
      this.handleScroll.bind(this)
    )
  }

  handleScroll() {
    const scrollTop = this.container.scrollTop
    const containerHeight = this.container.clientHeight

    this.startIndex = Math.floor(scrollTop / this.itemHeight)
    this.endIndex = Math.min(
      this.startIndex + Math.ceil(containerHeight / this.itemHeight) + 1,
      this.items.length - 1
    )

    this.renderVisibleItems()
  }

  renderVisibleItems() {
    // 清理旧元素
    this.visibleItems.forEach(el => el.remove())
    this.visibleItems = []

    // 渲染可见项
    for (let i = this.startIndex; i <= this.endIndex; i++) {
      const item = this.renderItem(this.items[i], i)
      item.style.position = 'absolute'
      item.style.top = `${i * this.itemHeight}px`
      this.container.appendChild(item)
      this.visibleItems.push(item)
    }

    // 更新占位元素高度
    this.placeholder.style.height = `${this.items.length * this.itemHeight}px`
  }

  setItems(items) {
    this.items = items
    this.handleScroll()
  }
}

内存管理

避免内存泄漏

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class ComponentManager {
  constructor() {
    this.components = new Map()
    this.observers = []
  }

  addComponent(id, component) {
    this.components.set(id, component)

    // 添加清理函数
    component.cleanup = () => {
      // 清理事件监听器
      component.element?.removeEventListener()

      // 清理定时器
      if (component.timer) {
        clearInterval(component.timer)
      }

      // 清理观察者
      component.observer?.disconnect()
    }
  }

  removeComponent(id) {
    const component = this.components.get(id)
    if (component && typeof component.cleanup === 'function') {
      component.cleanup()
    }
    this.components.delete(id)
  }

  cleanup() {
    this.components.forEach((_, id) => {
      this.removeComponent(id)
    })
    this.observers.forEach(observer => observer.disconnect())
  }
}

// 页面卸载时清理
window.addEventListener('beforeunload', () => {
  componentManager.cleanup()
})

缓存策略

HTTP缓存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// Service Worker缓存
const CACHE_NAME = 'app-v1'
const urlsToCache = [
  '/',
  '/js/main.js',
  '/css/main.css',
  '/images/logo.png'
]

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

self.addEventListener('fetch', (event) => {
  event.respondWith(
    caches.match(event.request)
      .then((response) => {
        // 缓存命中
        if (response) {
          return response
        }

        // 网络请求
        return fetch(event.request)
          .then((response) => {
            // 检查是否有效响应
            if (!response || response.status !== 200 || response.type !== 'basic') {
              return response
            }

            // 克隆响应
            const responseToCache = response.clone()

            caches.open(CACHE_NAME)
              .then((cache) => {
                cache.put(event.request, responseToCache)
              })

            return response
          })
      })
  )
})

浏览器缓存

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// IndexedDB缓存大数据
class DataCache {
  constructor(dbName = 'AppCache', storeName = 'data') {
    this.dbName = dbName
    this.storeName = storeName
  }

  async init() {
    return new Promise((resolve, reject) => {
      const request = indexedDB.open(this.dbName, 1)

      request.onerror = () => reject(request.error)
      request.onsuccess = () => {
        this.db = request.result
        resolve()
      }

      request.onupgradeneeded = (event) => {
        const db = event.target.result
        if (!db.objectStoreNames.contains(this.storeName)) {
          db.createObjectStore(this.storeName, { keyPath: 'key' })
        }
      }
    })
  }

  async set(key, data, ttl = 3600000) { // 默认1小时
    const transaction = this.db.transaction([this.storeName], 'readwrite')
    const store = transaction.objectStore(this.storeName)

    const item = {
      key,
      data,
      timestamp: Date.now(),
      ttl
    }

    return new Promise((resolve, reject) => {
      const request = store.put(item)
      request.onsuccess = () => resolve()
      request.onerror = () => reject(request.error)
    })
  }

  async get(key) {
    const transaction = this.db.transaction([this.storeName], 'readonly')
    const store = transaction.objectStore(this.storeName)

    return new Promise((resolve, reject) => {
      const request = store.get(key)
      request.onsuccess = () => {
        const item = request.result
        if (!item) {
          resolve(null)
          return
        }

        // 检查是否过期
        if (Date.now() - item.timestamp > item.ttl) {
          this.delete(key)
          resolve(null)
          return
        }

        resolve(item.data)
      }
      request.onerror = () => reject(request.error)
    })
  }

  async delete(key) {
    const transaction = this.db.transaction([this.storeName], 'readwrite')
    const store = transaction.objectStore(this.storeName)

    return new Promise((resolve, reject) => {
      const request = store.delete(key)
      request.onsuccess = () => resolve()
      request.onerror = () => reject(request.error)
    })
  }
}

渲染优化

减少重绘和重排

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 批量DOM操作
class BatchDOMUpdate {
  constructor() {
    this.pendingUpdates = []
    this.isScheduled = false
  }

  add(update) {
    this.pendingUpdates.push(update)
    this.schedule()
  }

  schedule() {
    if (this.isScheduled) return

    this.isScheduled = true
    requestAnimationFrame(() => {
      this.flush()
      this.isScheduled = false
    })
  }

  flush() {
    // 使用DocumentFragment批量更新
    const fragment = document.createDocumentFragment()

    this.pendingUpdates.forEach(update => {
      if (typeof update === 'function') {
        update(fragment)
      }
    })

    if (fragment.children.length > 0) {
      document.body.appendChild(fragment)
    }

    this.pendingUpdates = []
  }
}

// 使用示例
const batchUpdate = new BatchDOMUpdate()

// 批量添加元素
for (let i = 0; i < 1000; i++) {
  batchUpdate.add((fragment) => {
    const div = document.createElement('div')
    div.textContent = `Item ${i}`
    fragment.appendChild(div)
  })
}

CSS动画优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
/* 使用transform和opacity */
.optimized-animation {
  will-change: transform, opacity;
  transform: translateZ(0); /* 触发硬件加速 */
  backface-visibility: hidden;
}

/* 避免布局抖动 */
.layout-stable {
  position: absolute;
  width: 100px;
  height: 100px;
  transform: translateX(0);
  transition: transform 0.3s ease-out;
}

.layout-stable.move {
  transform: translateX(100px);
}

Web Workers优化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
// 主线程
class WorkerPool {
  constructor(workerScript, poolSize = 4) {
    this.workers = []
    this.taskQueue = []
    this.poolSize = poolSize

    this.initWorkers(workerScript)
  }

  initWorkers(workerScript) {
    for (let i = 0; i < this.poolSize; i++) {
      const worker = new Worker(workerScript)
      worker.addEventListener('message', this.handleWorkerMessage.bind(this))
      this.workers.push({
        worker,
        busy: false
      })
    }
  }

  execute(data) {
    return new Promise((resolve, reject) => {
      this.taskQueue.push({ data, resolve, reject })
      this.processQueue()
    })
  }

  processQueue() {
    if (this.taskQueue.length === 0) return

    const availableWorker = this.workers.find(w => !w.busy)
    if (!availableWorker) return

    const task = this.taskQueue.shift()
    availableWorker.busy = true
    availableWorker.currentTask = task

    availableWorker.worker.postMessage(task.data)
  }

  handleWorkerMessage(event) {
    const worker = this.workers.find(w => w.worker === event.target)
    if (worker && worker.busy) {
      const { resolve } = worker.currentTask
      resolve(event.data)
      worker.busy = false
      worker.currentTask = null

      this.processQueue()
    }
  }
}

// Worker脚本 (worker.js)
self.addEventListener('message', (event) => {
  const { data } = event

  // 执行重计算任务
  const result = heavyComputation(data)

  // 返回结果
  self.postMessage(result)
})

function heavyComputation(data) {
  // CPU密集型计算
  return data.reduce((acc, item) => acc + item * item, 0)
}

网络优化

HTTP/2和HTTP/3

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// 服务器推送关键资源
// 在服务器配置中
res.push('/css/critical.css')
res.push('/js/main.js')

// 使用连接复用
const fetchWithHttp2 = async (url) => {
  const response = await fetch(url, {
    // HTTP/2自动复用连接
    keepalive: true
  })
  return response.json()
}

数据压缩

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 启用Brotli压缩
// .htaccess配置
<IfModule mod_brotli.c>
    AddOutputFilterByType DEFLATE application/json
    AddOutputFilterByType DEFLATE application/javascript
    AddOutputFilterByType DEFLATE text/css
    AddOutputFilterByType DEFLATE text/html
</IfModule>

// 前端压缩数据
function compressData(data) {
  const jsonString = JSON.stringify(data)

  // 如果浏览器支持CompressionStream
  if ('CompressionStream' in window) {
    const stream = new CompressionStream('gzip')
    const writer = stream.writable.getWriter()
    const reader = stream.readable.getReader()

    writer.write(new TextEncoder().encode(jsonString))
    writer.close()

    return new Response(stream.readable).arrayBuffer()
  }

  return new TextEncoder().encode(jsonString)
}

性能监控与告警

Real User Monitoring (RUM)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
class RUMMonitor {
  constructor(apiEndpoint) {
    this.apiEndpoint = apiEndpoint
    this.metrics = {}
    this.init()
  }

  init() {
    // 监控页面加载
    window.addEventListener('load', () => {
      this.collectPageMetrics()
    })

    // 监控用户交互
    this.monitorUserInteractions()

    // 监控资源加载
    this.monitorResourceLoading()
  }

  collectPageMetrics() {
    const navigation = performance.getEntriesByType('navigation')[0]

    this.metrics = {
      dns: navigation.domainLookupEnd - navigation.domainLookupStart,
      tcp: navigation.connectEnd - navigation.connectStart,
      ssl: navigation.secureConnectionStart > 0 ?
        navigation.connectEnd - navigation.secureConnectionStart : 0,
      ttfb: navigation.responseStart - navigation.requestStart,
      download: navigation.responseEnd - navigation.responseStart,
      domParse: navigation.domContentLoadedEventStart - navigation.responseEnd,
      domReady: navigation.domContentLoadedEventEnd - navigation.domContentLoadedEventStart,
      loadComplete: navigation.loadEventEnd - navigation.loadEventStart
    }

    this.sendMetrics()
  }

  monitorUserInteractions() {
    ['click', 'touchstart'].forEach(eventType => {
      document.addEventListener(eventType, (event) => {
        const startTime = performance.now()

        requestAnimationFrame(() => {
          const responseTime = performance.now() - startTime

          if (responseTime > 100) { // 超过100ms的交互
            this.recordSlowInteraction({
              type: eventType,
              target: event.target.tagName,
              responseTime
            })
          }
        })
      })
    })
  }

  monitorResourceLoading() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (entry.duration > 1000) { // 超过1秒的资源
          this.recordSlowResource({
            name: entry.name,
            duration: entry.duration,
            size: entry.transferSize
          })
        }
      }
    })

    observer.observe({ entryTypes: ['resource'] })
  }

  async sendMetrics() {
    try {
      await fetch(this.apiEndpoint, {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json'
        },
        body: JSON.stringify({
          url: window.location.href,
          userAgent: navigator.userAgent,
          timestamp: Date.now(),
          metrics: this.metrics
        })
      })
    } catch (error) {
      console.error('Failed to send metrics:', error)
    }
  }

  recordSlowInteraction(data) {
    this.sendMetrics({
      type: 'slow_interaction',
      ...data
    })
  }

  recordSlowResource(data) {
    this.sendMetrics({
      type: 'slow_resource',
      ...data
    })
  }
}

// 初始化监控
const rumMonitor = new RUMMonitor('/api/analytics/performance')

总结

前端性能优化是一个持续的过程,需要从多个维度进行考虑:

加载性能优化:

  • 图片优化和懒加载
  • 资源预加载和预取
  • 代码分割和按需加载
  • 缓存策略优化

运行时性能优化:

  • 减少重绘和重排
  • 优化JavaScript执行
  • 使用Web Workers处理重任务
  • 实现虚拟滚动等优化技术

网络优化:

  • 启用HTTP/2和压缩
  • CDN加速
  • 资源合并和压缩
  • 减少HTTP请求数

监控与测量:

  • Core Web Vitals监控
  • Real User Monitoring
  • 性能告警机制
  • 持续性能优化

通过系统性的性能优化,我们可以显著提升用户体验,让网站真正实现"快如闪电"的加载速度。


相关资源: