
前言
上个月,我们的产品被反馈"页面加载太慢"。用户在3G网络下需要等待8秒才能看到内容。
经过一个月的优化,我们把首屏加载时间从8秒降到了1.2秒。这篇文章分享我们的优化实践。
一、性能指标体系
1.1 核心Web指标(Core Web Vitals)
javascript
// LCP(Largest Contentful Paint)- 最大内容绘制// 目标:2.5秒内const observer = new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { console.log('LCP:', entry.renderTime || entry.loadTime); });});observer.observe({entryTypes: ['largest-contentful-paint']});// FID(First Input Delay)- 首次输入延迟// 目标:100毫秒内new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { console.log('FID:', entry.processingDuration); });}).observe({entryTypes: ['first-input']});// CLS(Cumulative Layout Shift)- 累积布局偏移// 目标:0.1以下let clsValue = 0;new PerformanceObserver((list) => { list.getEntries().forEach((entry) => { if (!entry.hadRecentInput) { clsValue += entry.value; console.log('CLS:', clsValue); } });}).observe({entryTypes: ['layout-shift']});
1.2 性能指标采集
javascript
// 采集所有关键指标const getMetrics = () => { const navigation = performance.getEntriesByType('navigation')[0]; const paint = performance.getEntriesByType('paint'); return { // 时间相关指标 DNS: navigation.domainLookupEnd - navigation.domainLookupStart, TCP: navigation.connectEnd - navigation.connectStart, TTFB: navigation.responseStart - navigation.requestStart, // 首字节时间 DomReady: navigation.domInteractive - navigation.fetchStart, LoadComplete: navigation.loadEventEnd - navigation.fetchStart, // 渲染相关指标 FP: paint.find(p => p.name === 'first-paint')?.startTime, // 首次绘制 FCP: paint.find(p => p.name === 'first-contentful-paint')?.startTime, // 首次内容绘制 // 资源加载 Resources: performance.getEntriesByType('resource').length, };};console.log(getMetrics());
二、网络优化
2.1 HTTP/2与CDN
javascript
// 配置CDN的关键资源const cdnConfig = { // 使用CDN加速静态资源 images: 'https://cdn.example.com/images', scripts: 'https://cdn.example.com/scripts', styles: 'https://cdn.example.com/styles',};// 使用HTTP/2的服务器推送// 在服务器侧配置const http2Push = { '/index.html': [ '/styles/main.css', '/scripts/app.js', '/fonts/roboto.woff2', ]};
2.2 资源压缩
bash
# Gzip压缩gzip -9 static/bundle.js -c > static/bundle.js.gz# Brotli压缩(更高的压缩率)brotli -q 11 static/bundle.js -o static/bundle.js.br# WebP图片格式cwebp original.jpg -o optimized.webp -q 80
2.3 缓存策略
javascript
// 服务器配置缓存头const express = require('express');const app = express();// 不变资源:带hash的JS、CSS、图片app.use('/static', (req, res, next) => { res.set('Cache-Control', 'public, max-age=31536000, immutable'); next();});// HTML文件:始终检查新版本app.get('/', (req, res) => { res.set('Cache-Control', 'no-cache, must-revalidate'); res.sendFile('index.html');});// API响应:短期缓存app.get('/api/*', (req, res) => { res.set('Cache-Control', 'private, max-age=300'); // 5分钟 next();});
三、代码分割与懒加载
3.1 Webpack代码分割
javascript
// webpack.config.jsmodule.exports = { optimization: { splitChunks: { chunks: 'all', cacheGroups: { // 分离第三方库 vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10, reuseExistingChunk: true, }, // 分离公共模块 common: { minChunks: 2, priority: 5, reuseExistingChunk: true, } } } }};
3.2 动态导入与懒加载
javascript
// React懒加载示例import React, { Suspense, lazy } from 'react';const Dashboard = lazy(() => import('./Dashboard'));const Settings = lazy(() => import('./Settings'));function App() { return ( <Suspense fallback={<Loading />}> <Router> <Route path="/dashboard" component={Dashboard} /> <Route path="/settings" component={Settings} /> </Router> </Suspense> );}
3.3 路由懒加载
javascript
// Vue路由懒加载const router = new VueRouter({ routes: [ { path: '/home', component: () => import('./views/Home.vue') }, { path: '/about', component: () => import('./views/About.vue') }, { path: '/products', component: () => import('./views/Products.vue') } ]});
四、图片优化
4.1 响应式图片
html
<!-- 使用srcset适配不同屏幕 --><img src="image.jpg" srcset=" image-320w.jpg 320w, image-640w.jpg 640w, image-1280w.jpg 1280w " sizes="(max-width: 320px) 280px, (max-width: 640px) 600px, 1200px" alt="Responsive image"/><!-- 使用picture元素支持多种格式 --><picture> <source srcset="image.webp" type="image/webp" /> <source srcset="image.jpg" type="image/jpeg" /> <img src="image.jpg" alt="Fallback" /></picture>
4.2 图片懒加载
javascript
// 使用Intersection Observer懒加载const imageObserver = new IntersectionObserver((entries, observer) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; img.classList.add('loaded'); observer.unobserve(img); } });});document.querySelectorAll('img[data-src]').forEach(img => { imageObserver.observe(img);});
html
<!-- HTML使用 --><img data-src="https://example.com/image.jpg" class="lazy-image" />
4.3 图片服务化
javascript
// 使用图片服务API进行动态裁剪和压缩const getOptimizedImage = (url, width, height, quality = 80) => { return `https://image-service.example.com/resize?url=${url}&w=${width}&h=${height}&q=${quality}`;};// 使用示例const optimized = getOptimizedImage('original.jpg', 400, 300, 75);// https://image-service.example.com/resize?url=original.jpg&w=400&h=300&q=75
五、JavaScript优化
5.1 删除未使用代码
javascript
// webpack.config.js 配置Tree Shakingmodule.exports = { mode: 'production', // 自动启用Tree Shaking optimization: { usedExports: true, sideEffects: false }};// package.json 标记无副作用{ "sideEffects": false}
5.2 异步脚本加载
html
<!-- async:立即下载,下载完成后立即执行 --><script async src="analytics.js"></script><!-- defer:等待HTML解析完成后执行 --><script defer src="app.js"></script><!-- 动态加载脚本 --><script> if (condition) { const script = document.createElement('script'); script.src = 'optional-feature.js'; document.body.appendChild(script); }</script>
5.3 防抖和节流
javascript
// 防抖:延迟执行const debounce = (func, wait) => { let timeout; return function executedFunction(...args) { const later = () => { clearTimeout(timeout); func(...args); }; clearTimeout(timeout); timeout = setTimeout(later, wait); };};// 搜索输入防抖const handleSearch = debounce((query) => { fetch(`/api/search?q=${query}`);}, 300);// 节流:定期执行const throttle = (func, limit) => { let inThrottle; return function(...args) { if (!inThrottle) { func.apply(this, args); inThrottle = true; setTimeout(() => inThrottle = false, limit); } };};// 滚动事件节流window.addEventListener('scroll', throttle(() => { console.log('Scroll event');}, 100));
六、CSS优化
6.1 关键CSS内联
html
<!-- 内联关键CSS,提高首屏速度 --><head> <style> /* 首屏关键样式 */ body { margin: 0; font-family: -apple-system; } .header { background: #fff; padding: 20px; } .hero { height: 500px; background: url('hero.jpg'); } </style> <!-- 异步加载剩余样式 --> <link rel="preload" href="styles.css" as="style" onload="this.onload=null;this.rel='stylesheet'" /></head>
6.2 CSS-in-JS优化
javascript
// 使用样式缓存import styled from 'styled-components';const Button = styled.button` background: #007bff; color: white; padding: 10px 20px;`;// 避免在render中创建样式// ❌ 不好const Component = () => { const styles = { color: 'red' }; // 每次都重新创建 return <div style={styles}>Content</div>;};// ✅ 好const styles = { color: 'red' };const Component = () => <div style={styles}>Content</div>;
七、渲染性能优化
7.1 虚拟滚动
javascript
// React虚拟滚动示例import { FixedSizeList } from 'react-window';const VirtualList = ({ items }) => ( <FixedSizeList height={600} itemCount={items.length} itemSize={35} width="100%" > {({ index, style }) => ( <div style={style}> {items[index].name} </div> )} </FixedSizeList>);
7.2 requestAnimationFrame优化动画
javascript
// ❌ 低效:setInterval更新DOMsetInterval(() => { element.style.left = parseInt(element.style.left) + 1 + 'px';}, 16);// ✅ 优化:使用requestAnimationFramelet lastTime = 0;const animate = (currentTime) => { const deltaTime = currentTime - lastTime; lastTime = currentTime; element.style.transform = `translateX(${deltaTime}px)`; requestAnimationFrame(animate);};requestAnimationFrame(animate);
7.3 Web Workers处理复杂计算
javascript
// main.jsconst worker = new Worker('worker.js');// 发送数据给Workerworker.postMessage({ cmd: 'calculate', data: [1, 2, 3, 4, 5]});// 接收Worker的结果worker.onmessage = (e) => { console.log('Result:', e.data);};// worker.jsself.onmessage = (e) => { if (e.data.cmd === 'calculate') { const result = e.data.data.reduce((a, b) => a + b); self.postMessage(result); }};
八、字体优化
8.1 字体加载策略
css
/* 使用font-display优化字体加载 */@font-face { font-family: 'Roboto'; src: url('/fonts/roboto.woff2') format('woff2'); font-display: swap; /* 立即使用后备字体,加载完成后替换 */}/* 其他选项: auto: 浏览器默认行为 block: 等待字体,最多3秒 swap: 立即使用后备,加载完成后替换 fallback: 100ms内加载不了就用后备 optional: 加载失败则使用后备*/
8.2 子集字体
html
<!-- 只加载所需的字符 --><link href="https://fonts.googleapis.com/css2?family=Roboto:display=swap&subset=latin-ext" rel="stylesheet"/>
九、监控与分析
9.1 性能监控
javascript
// 上报性能数据到服务器const reportPerformance = () => { if (!window.performance) return; const perfData = performance.timing; const pageLoadTime = perfData.loadEventEnd - perfData.navigationStart; const metrics = { pageLoadTime, dns: perfData.domainLookupEnd - perfData.domainLookupStart, tcp: perfData.connectEnd - perfData.connectStart, request: perfData.responseStart - perfData.requestStart, render: perfData.domInteractive - perfData.domLoading, dom: perfData.domComplete - perfData.domLoading, load: perfData.loadEventEnd - perfData.loadEventStart, }; // 上报到服务器 navigator.sendBeacon('/api/metrics', JSON.stringify(metrics));};window.addEventListener('load', reportPerformance);
9.2 使用性能分析工具
bash
# Lighthouse CLIlighthouse https://example.com --view# WebPageTest# 访问 https://www.webpagetest.org/# Chrome DevTools# 打开Chrome DevTools > Performance标签# 录制页面加载过程
十、全球团队技术交流
我们公司的前端团队分布在全球各地,每周进行性能优化的技术分享会议。会议中既有中文讲解,也有英文Q&A。为了确保语言不成为障碍,我们使用**同言翻译(Transync AI)**提供实时同声传译,让团队中的每个成员都能准确理解优化细节和最佳实践,显著提升了跨语言技术交流的效率。
十一、性能优化检查清单
- 测量并分析当前性能指标
- 启用Gzip和Brotli压缩
- 配置CDN和缓存策略
- 实现代码分割和路由懒加载
- 优化图片,使用WebP格式
- 内联关键CSS
- 删除未使用的JavaScript
- 实现虚拟滚动处理大列表
- 优化字体加载
- 设置性能监控和告警
十二、优化前后对比
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 首屏加载 | 8.0s | 1.2s | 85% |
| 首次交互 | 5.2s | 0.8s | 85% |
| 总体大小 | 2.5MB | 450KB | 82% |
| Lighthouse | 35分 | 92分 | 163% |
| 3G首屏 | 18s | 3.5s | 81% |
十三、常见误区
javascript
// ❌ 误区1:压缩后的代码不需要再优化// 实际上,代码压缩只是第一步,还需要分割、缓存、懒加载等// ❌ 误区2:所有资源都应该async加载// 实际上,关键资源应该内联或预加载,非关键资源才延迟加载// ❌ 误区3:缓存时间越长越好// 实际上,需要平衡新版本发布和用户体验// ✅ 正确:采用多维度优化策略// 网络 -> 资源 -> 渲染 -> 执行
十四、推荐工具与资源
Chrome DevTools Performance - 性能分析Lighthouse - 性能审计WebPageTest - 在线性能测试Bundle Analyzer - 包体积分析Speed Curve - 性能监控
十五、结语
前端性能优化是一个系统工程,需要从多个维度入手。不是一次性优化,而是持续改进的过程。
核心思路:测量 → 分析 → 优化 → 验证 → 监控
希望这篇文章能帮助你的产品提升用户体验。欢迎在评论区分享你的优化经验!
