性能是用户体验的核心。一个性能优秀的应用能显著提升用户满意度和转化率。本文将系统性地介绍前端性能优化的各个方面。

性能指标体系

核心Web指标(Core Web Vitals)

 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
"""
Google核心Web指标:
┌────────────────────────────────────────────────────────────────┐
│ 1. LCP (Largest Contentful Paint) - 最大内容绘制               │
│    - 目标:< 2.5秒                                              │
│    - 衡量:主要内容加载速度                                    │
│    - 影响:用户感知的加载速度                                  │
├────────────────────────────────────────────────────────────────┤
│ 2. INP (Interaction to Next Paint) - 交互到下次绘制           │
│    - 目标:< 200ms                                             │
│    - 衡量:页面响应速度                                        │
│    - 影响:用户交互体验                                        │
├────────────────────────────────────────────────────────────────┤
│ 3. CLS (Cumulative Layout Shift) - 累积布局偏移               │
│    - 目标:< 0.1                                              │
│    - 衡量:视觉稳定性                                         │
│    - 影响:意外的布局变化                                      │
└────────────────────────────────────────────────────────────────┘
"""

class PerformanceMetrics:
    """性能指标追踪"""

    def track_lcp(self):
        """
        追踪最大内容绘制
        """
        return """
// 在页面中添加追踪代码
<script>
new PerformanceObserver((entryList) => {
  const entries = entryList.getEntries();
  const lastEntry = entries[entries.length - 1];

  // 发送到分析服务
  analytics.track('LCP', {
    value: lastEntry.renderTime || lastEntry.loadTime,
    element: lastEntry.element?.tagName
  });
}).observe({entryTypes: ['largest-contentful-paint']});
</script>
        """

    def track_cls(self):
        """
        追踪累积布局偏移
        """
        return """
<script>
let clsValue = 0;
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    if (!entry.hadRecentInput) {
      clsValue += entry.value;
      analytics.track('CLS', {value: clsValue});
    }
  }
}).observe({entryTypes: ['layout-shift']});
</script>
        """

    def track_inp(self):
        """
        追踪交互响应
        """
        return """
<script>
new PerformanceObserver((entryList) => {
  for (const entry of entryList.getEntries()) {
    analytics.track('INP', {
      value: entry.duration,
      target: entry.target?.tagName
    });
  }
}).observe({entryTypes: ['event']});
</script>
        """

资源加载优化

关键渲染路径优化

 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
<!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="https://cdn.example.com">
    <link rel="dns-prefetch" href="https://api.example.com">

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

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

    <!-- 4. 关键CSS内联 -->
    <style>
        /* 首屏关键样式 */
        body { margin: 0; font-family: system-ui; }
        .header { display: flex; justify-content: space-between; }
        .hero { min-height: 400px; }
    </style>

    <!-- 5. 其余CSS异步加载 -->
    <link rel="preload" href="/styles/main.css" as="style" onload="this.onload=null;this.rel='stylesheet'">
    <noscript><link rel="stylesheet" href="/styles/main.css"></noscript>

    <!-- 6. 预获取下一页资源 -->
    <link rel="prefetch" href="/page2.html">
    <link rel="prefetch" href="/images/page2-hero.jpg">

    <!-- 7. 预渲染可能访问的页面 -->
    <link rel="prerender" href="/next-page.html">
</head>
<body>
    <!-- 内容 -->

    <!-- 8. JavaScript延迟加载 -->
    <script defer src="/scripts/main.js"></script>

    <!-- 9. 非关键JS异步加载 -->
    <script async src="/scripts/analytics.js"></script>

    <!-- 10. 动态导入非关键模块 -->
    <script type="module">
        // 只在需要时加载
        const dialog = await import('/components/dialog.js');
    </script>
</body>
</html>

代码分割策略

 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
/**
 * Webpack代码分割配置
 */
module.exports = {
    optimization: {
        splitChunks: {
            chunks: 'all',
            cacheGroups: {
                // 第三方库单独打包
                vendor: {
                    test: /[\\/]node_modules[\\/]/,
                    name: 'vendors',
                    priority: 10
                },
                // 公共代码提取
                common: {
                    minChunks: 2,
                    priority: 5,
                    reuseExistingChunk: true
                },
                // 框架单独打包
                framework: {
                    test: /[\\/]node_modules[\\/](react|react-dom|vue)[\\/]/,
                    name: 'framework',
                    priority: 20
                }
            }
        },
        // 运行时代码单独打包
        runtimeChunk: {
            name: 'runtime'
        }
    }
};

/**
 * 路由级代码分割(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 Dashboard = lazy(() => import('./pages/Dashboard'));

// 加载状态组件
const LoadingFallback = () => (
    <div className="loading">
        <div className="spinner"></div>
        <p>加载中...</p>
    </div>
);

function App() {
    return (
        <BrowserRouter>
            <Suspense fallback={<LoadingFallback />}>
                <Routes>
                    <Route path="/" element={<Home />} />
                    <Route path="/about" element={<About />} />
                    <Route path="/dashboard" element={<Dashboard />} />
                </Routes>
            </Suspense>
        </BrowserRouter>
    );
}

/**
 * 组件级代码分割
 */
import { lazy } from 'react';

// 重型组件延迟加载
const HeavyChart = lazy(() =>
    import('./HeavyChart')
);

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

function Dashboard() {
    const [showChart, setShowChart] = useState(false);

    return (
        <div>
            <button onClick={() => setShowChart(true)}>
                显示图表
            </button>

            {showChart && (
                <Suspense fallback={<div>加载图表...</div>}>
                    <HeavyChart />
                </Suspense>
            )}
        </div>
    );
}

渲染性能优化

重排与重绘优化

 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
/**
 * 减少重排(Reflow)和重绘(Repaint)
 */

/* 1. 使用transform和opacity代替位置变化 */

/* 不好的做法 - 触发重排 */
.bad {
    position: absolute;
    left: 100px;
    top: 100px;
    transition: left 0.3s, top 0.3s;
}

.bad:hover {
    left: 200px;  /* 触发重排 */
    top: 200px;
}

/* 好的做法 - 只触发合成 */
.good {
    position: absolute;
    left: 100px;
    top: 100px;
    transform: translate(0, 0);
    transition: transform 0.3s;
    will-change: transform;  /* 提示浏览器优化 */
}

.good:hover {
    transform: translate(100px, 100px);  /* GPU加速 */
}

/* 2. 避免布局抖动 */
.layout-stable {
    /* 为图片预留空间,防止加载后布局变化 */
    aspect-ratio: 16 / 9;
    min-height: 300px;
    background-color: #f0f0f0;
}

/* 3. 使用contain属性 */
.isolated {
    /* 告诉浏览器这个元素的渲染独立于其他部分 */
    contain: layout style paint;
}

/* 4. 批量DOM修改 */
.batch-update {
    /* 使用类名切换代替多次style修改 */
}
 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
/**
 * JavaScript性能优化:减少DOM操作
 */

// 不好的做法:多次DOM操作
function updateListBad(items) {
    const list = document.getElementById('list');

    items.forEach(item => {
        // 每次循环都触发重排
        const li = document.createElement('li');
        li.textContent = item;
        list.appendChild(li);
    });
}

// 好的做法:使用DocumentFragment批量操作
function updateListGood(items) {
    const list = document.getElementById('list');
    const fragment = document.createDocumentFragment();

    items.forEach(item => {
        const li = document.createElement('li');
        li.textContent = item;
        fragment.appendChild(li);  // 不触发重排
    });

    list.appendChild(fragment);  // 只触发一次重排
}

// 更好的做法:使用innerHTML一次性更新
function updateListBetter(items) {
    const list = document.getElementById('list');

    const html = items.map(item => `<li>${item}</li>`).join('');

    list.innerHTML = html;  // 只触发一次重排和重绘
}

// 最佳做法:使用虚拟DOM
function updateListBest(items) {
    // React/Vue等框架会自动优化DOM操作
    return (
        <ul>
            {items.map(item => (
                <li key={item.id}>{item.text}</li>
            ))}
        </ul>
    );
}

/**
 * 读写分离:减少强制同步布局
 */
function layoutThrashingBad() {
    const elements = document.querySelectorAll('.item');

    elements.forEach(el => {
        // 读
        const height = el.offsetHeight;
        // 写 - 触发重排
        el.style.height = height + 10 + 'px';
        // 读 - 强制同步布局
        const width = el.offsetWidth;
        // 写 - 再次触发重排
        el.style.width = width + 10 + 'px';
    });
}

function layoutThrashingGood() {
    const elements = document.querySelectorAll('.item');

    // 批量读取
    const heights = Array.from(elements).map(el => el.offsetHeight);
    const widths = Array.from(elements).map(el => el.offsetWidth);

    // 批量写入
    elements.forEach((el, i) => {
        el.style.height = heights[i] + 10 + 'px';
        el.style.width = widths[i] + 10 + 'px';
    });
}

虚拟滚动

 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
/**
 * 虚拟滚动:只渲染可见区域的列表项
 */
class VirtualList {
    constructor(options) {
        this.container = options.container;
        this.itemHeight = options.itemHeight;
        this.totalItems = options.totalItems;
        this.renderItem = options.renderItem;

        this.visibleCount = Math.ceil(
            this.container.clientHeight / this.itemHeight
        ) + 2;  // 缓冲区

        this.scrollTop = 0;

        this.init();
    }

    init() {
        this.container.style.overflow = 'auto';
        this.container.style.position = 'relative';

        // 创建容器
        this.content = document.createElement('div');
        this.content.style.height = `${this.totalItems * this.itemHeight}px`;
        this.content.style.position = 'relative';

        this.viewport = document.createElement('div');
        this.viewport.style.position = 'absolute';
        this.viewport.style.top = '0';
        this.viewport.style.left = '0';
        this.viewport.style.right = '0';

        this.content.appendChild(this.viewport);
        this.container.appendChild(this.content);

        // 监听滚动
        this.container.addEventListener('scroll', () => {
            this.onScroll();
        });

        // 初始渲染
        this.render();
    }

    onScroll() {
        this.scrollTop = this.container.scrollTop;
        requestAnimationFrame(() => this.render());
    }

    render() {
        const startIndex = Math.floor(this.scrollTop / this.itemHeight);
        const endIndex = Math.min(
            startIndex + this.visibleCount,
            this.totalItems
        );

        const offsetY = startIndex * this.itemHeight;
        this.viewport.style.transform = `translateY(${offsetY}px)`;

        // 清空并重新渲染可见项
        this.viewport.innerHTML = '';

        for (let i = startIndex; i < endIndex; i++) {
            const item = this.renderItem(i);
            item.style.height = `${this.itemHeight}px`;
            item.style.position = 'absolute';
            item.style.top = `${(i - startIndex) * this.itemHeight}px`;
            item.style.left = '0';
            item.style.right = '0';
            this.viewport.appendChild(item);
        }
    }
}

// 使用示例
const virtualList = new VirtualList({
    container: document.getElementById('list-container'),
    itemHeight: 50,
    totalItems: 10000,
    renderItem: (index) => {
        const div = document.createElement('div');
        div.textContent = `Item ${index}`;
        div.className = 'list-item';
        return div;
    }
});

网络优化

HTTP/2和HTTP/3优化

 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
/**
 * 服务器推送(HTTP/2 Server Push)
 */
// Nginx配置示例
server {
    listen 443 ssl http2;

    location / {
        http2_push /styles/main.css;
        http2_push /scripts/main.js;
        http2_push /fonts/main.woff2;
    }
}

/**
 * 资源优先级设置
 */
// 通过Fetch Priority API控制资源加载优先级
fetch('/api/data', {
    priority: 'high'  // 'high', 'low', 'auto'
});

// 图片加载优先级
<img src="hero.jpg" fetchpriority="high" alt="Hero">
<img src="icon.png" fetchpriority="low" alt="Icon">

/**
 * 预连接优化
 */
// HTML中预连接
<link rel="preconnect" href="https://api.example.com" crossorigin>
<link rel="dns-prefetch" href="https://cdn.example.com">

// Service Worker中预连接
self.addEventListener('install', (event) => {
    event.waitUntil(
        caches.open('v1').then((cache) => {
            return cache.addAll([
                '/',
                '/styles/main.css',
                '/scripts/main.js'
            ]);
        })
    );
});

图片优化

 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
<!-- 1. 响应式图片 -->
<picture>
    <!-- 现代格式 -->
    <source
        srcset="hero.webp"
        type="image/webp">
    <source
        srcset="hero.avif"
        type="image/avif">

    <!-- 降级方案 -->
    <img
        src="hero.jpg"
        alt="Hero image"
        loading="lazy"
        decoding="async"
        width="1200"
        height="600">
</picture>

<!-- 2. srcset响应式 -->
<img
    srcset="small.jpg 400w,
            medium.jpg 800w,
            large.jpg 1200w"
    sizes="(max-width: 600px) 400px,
           (max-width: 1200px) 800px,
           1200px"
    src="medium.jpg"
    alt="Responsive image"
    loading="lazy">

<!-- 3. 懒加载 -->
<img
    data-src="image.jpg"
    class="lazyload"
    alt="Lazy loaded image">

<script>
// 原生懒加载(如果浏览器支持)
if ('loading' in HTMLImageElement.prototype) {
    const images = document.querySelectorAll('img[loading="lazy"]');
    images.forEach(img => {
        img.src = img.dataset.src;
    });
} else {
    // 降级到Intersection Observer
    const imageObserver = new IntersectionObserver((entries) => {
        entries.forEach(entry => {
            if (entry.isIntersecting) {
                const img = entry.target;
                img.src = img.dataset.src;
                img.classList.remove('lazyload');
                imageObserver.unobserve(img);
            }
        });
    });

    document.querySelectorAll('img.lazyload').forEach(img => {
        imageObserver.observe(img);
    });
}
</script>
 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
/**
 * 图片压缩和格式转换
 */
const sharp = require('sharp');

async function optimizeImage(inputPath, outputPath) {
    await sharp(inputPath)
        // 调整尺寸
        .resize(1200, null, {
            withoutEnlargement: true,
            fit: 'inside'
        })
        // 转换为WebP(更小的文件大小)
        .webp({
            quality: 80,
            effort: 4
        })
        // 生成多份不同质量
        .toFile(outputPath);

    // 同时生成AVIF格式(如果浏览器支持)
    await sharp(inputPath)
        .resize(1200, null, {
            withoutEnlargement: true
        })
        .avif({
            quality: 65,
            effort: 4
        })
        .toFile(outputPath.replace('.webp', '.avif'));
}

// 批量优化
async function optimizeImages(inputDir, outputDir) {
    const images = glob.sync(`${inputDir}/*.{jpg,png}`);

    for (const image of images) {
        const basename = path.basename(image, path.extname(image));
        await optimizeImage(
            image,
            `${outputDir}/${basename}.webp`
        );
    }
}

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
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
/**
 * 防抖(Debounce):延迟执行,只在最后一次触发后执行
 */
function debounce(func, wait) {
    let timeout;

    return function executedFunction(...args) {
        const later = () => {
            clearTimeout(timeout);
            func(...args);
        };

        clearTimeout(timeout);
        timeout = setTimeout(later, wait);
    };
}

// 使用场景:搜索输入
const searchInput = document.getElementById('search');

const handleSearch = debounce((query) => {
    fetch(`/api/search?q=${query}`)
        .then(res => res.json())
        .then(results => displayResults(results));
}, 300);

searchInput.addEventListener('input', (e) => {
    handleSearch(e.target.value);
});

/**
 * 节流(Throttle):固定时间间隔执行
 */
function throttle(func, limit) {
    let inThrottle;

    return function(...args) {
        if (!inThrottle) {
            func.apply(this, args);
            inThrottle = true;
            setTimeout(() => inThrottle = false, limit);
        }
    };
}

// 使用场景:滚动事件
const handleScroll = throttle(() => {
    const scrollTop = window.pageYOffset;
    updateHeaderPosition(scrollTop);
}, 100);

window.addEventListener('scroll', handleScroll);

/**
 * requestAnimationFrame节流
 */
function rafThrottle(func) {
    let rafId = null;

    return function(...args) {
        if (rafId === null) {
            rafId = requestAnimationFrame(() => {
                func.apply(this, args);
                rafId = null;
            });
        }
    };
}

// 使用场景:动画
const handleMouseMove = rafThrottle((x, y) => {
    updateCursorPosition(x, y);
});

document.addEventListener('mousemove', (e) => {
    handleMouseMove(e.clientX, e.clientY);
});

Web Worker计算密集任务

 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
/**
 * 主线程代码
 */
// main.js
const worker = new Worker('worker.js');

// 发送数据给Worker
worker.postMessage({
    type: 'process',
    data: largeDataSet
});

// 接收Worker结果
worker.onmessage = (e) => {
    const { type, result } = e.data;

    if (type === 'done') {
        displayResults(result);
    } else if (type === 'progress') {
        updateProgress(result);
    }
};

/**
 * Worker线程代码
 */
// worker.js
self.onmessage = (e) => {
    const { type, data } = e.data;

    if (type === 'process') {
        processData(data);
    }
};

function processData(data) {
    const total = data.length;
    let processed = 0;

    for (let i = 0; i < total; i++) {
        // 执行计算密集任务
        const result = complexCalculation(data[i]);

        processed++;

        // 定期发送进度更新
        if (i % 100 === 0) {
            self.postMessage({
                type: 'progress',
                result: processed / total
            });
        }
    }

    // 发送最终结果
    self.postMessage({
        type: 'done',
        result: processed
    });
}

/**
 * 使用OffscreenCanvas在Worker中渲染
 */
// main.js
const canvas = document.getElementById('canvas');
const offscreen = canvas.transferControlToOffscreen();

const worker = new Worker('canvas-worker.js');
worker.postMessage({ canvas: offscreen }, [offscreen]);

// canvas-worker.js
self.onmessage = (e) => {
    const canvas = e.data.canvas;
    const ctx = canvas.getContext('2d');

    function render() {
        ctx.fillStyle = 'blue';
        ctx.fillRect(0, 0, canvas.width, canvas.height);

        requestAnimationFrame(render);
    }

    render();
};

总结

前端性能优化的核心要点:

  1. 加载性能:减少资源大小和请求数量
  2. 渲染性能:减少重排重绘,使用GPU加速
  3. 运行时性能:优化JavaScript执行,避免长任务
  4. 网络性能:利用HTTP/2,优化资源加载顺序
  5. 监控测量:持续跟踪Core Web Vitals指标

性能优化是持续的过程,需要根据实际数据不断调整和改进。