前端性能优化实战:我踩过的坑和总结的经验
前言:性能优化是前端开发的永恒话题。从最初盲目地"压缩代码"到后来系统地分析和优化,今天把这些经验整理出来,希望能帮你少走一些弯路。
性能优化的核心思路
在开始之前,先明确一个原则:先测量,再优化。
1 2 3 4 5
| - Chrome DevTools Lighthouse - WebPageTest - Chrome Performance 面板 - bundle-analyzer (打包分析)
|
我的优化流程:
- 用 Lighthouse 跑分,找出瓶颈
- 用 Performance 面板分析运行时性能
- 用 bundle-analyzer 分析包体积
- 针对性优化
- 再次测量验证效果
一、加载性能优化
技巧 1:代码分割的正确姿势
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import { ComponentA } from './components' import { ComponentB } from './components' import { ComponentC } from './components'
const Home = lazy(() => import('./pages/Home')) const About = lazy(() => import('./pages/About')) const Dashboard = lazy(() => import('./pages/Dashboard'))
const HeavyComponent = lazy(() => import('./components/HeavyComponent'))
<Suspense fallback={<Loading />}> <HeavyComponent /> </Suspense>
|
实际效果:首屏包体积从 2MB 降到 400KB
技巧 2:图片优化的组合拳
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
| <img src="image-800.jpg" srcSet="image-400.jpg 400w, image-800.jpg 800w, image-1200.jpg 1200w" sizes="(max-width: 600px) 400px, (max-width: 1000px) 800px, 1200px" alt="描述" />
<img src="placeholder.jpg" data-src="actual-image.jpg" loading="lazy" alt="描述" />
<picture> <source srcSet="image.avif" type="image/avif" /> <source srcSet="image.webp" type="image/webp" /> <img src="image.jpg" alt="描述" loading="lazy" /> </picture>
|
我用的图片优化工具:
- XnConvert(批量转换/压缩,适合做 AVIF/WebP 等格式输出)
- Squoosh.app(在线压缩)
- imagemin(构建时压缩)
- Cloudinary/Imgix(CDN 优化)
技巧 3:预加载关键资源
1 2 3 4 5 6 7 8 9 10 11 12
| <link rel="dns-prefetch" href="//cdn.example.com" />
<link rel="preconnect" href="https://api.example.com" />
<link rel="preload" href="/fonts/main.woff2" as="font" type="font/woff2" crossorigin /> <link rel="preload" href="/css/critical.css" as="style" />
<link rel="prefetch" href="/next-page.js" />
|
使用场景:
preload:当前页面必需的资源
prefetch:下一页可能需要的资源
preconnect:提前建立连接
技巧 4:Tree Shaking 最大化
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import _ from 'lodash' _.debounce(func, 300)
import debounce from 'lodash/debounce'
import { debounce } from 'throttle-debounce'
{ "sideEffects": false }
|
二、运行时性能优化
技巧 5:避免不必要的重渲染
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
| const Parent = () => { const [count, setCount] = useState(0) return ( <div> <button onClick={() => setCount(c => c + 1)}>{count}</button> <Child data={someObject} /> {/* someObject 每次都是新的 */} </div> ) }
const Child = React.memo(({ data, onAction }) => { console.log('Child rendered') return <div>{data.name}</div> })
const Parent = () => { const [count, setCount] = useState(0) const data = useMemo(() => ({ name: 'John' }), []) const onAction = useCallback(() => {}, []) return ( <div> <button onClick={() => setCount(c => c + 1)}>{count}</button> <Child data={data} onAction={onAction} /> </div> ) }
|
技巧 6:虚拟列表处理大数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
{items.map(item => <Item key={item.id} data={item} />)}
import { FixedSizeList } from 'react-window'
const VirtualList = ({ items }) => ( <FixedSizeList height={600} itemCount={items.length} itemSize={50} width="100%" > {({ index, style }) => ( <div style={style}> <Item data={items[index]} /> </div> )} </FixedSizeList> )
|
性能对比:
- 直接渲染:10000 条 → 5-10 秒,内存 200MB+
- 虚拟列表:10000 条 → <100ms,内存 20MB
技巧 7:防抖和节流
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
|
<input onChange={(e) => search(e.target.value)} />
const debouncedSearch = useMemo( () => debounce((value) => { api.search(value) }, 300), [] ) <input onChange={(e) => debouncedSearch(e.target.value)} />
const throttledHandler = useMemo( () => throttle(() => { handleScroll() }, 100), [] ) window.addEventListener('scroll', throttledHandler)
|
技巧 8:Web Worker 处理计算密集型任务
1 2 3 4 5 6 7 8 9 10 11 12 13
| const worker = new Worker('./worker.js')
worker.postMessage(data) worker.onmessage = (e) => { console.log('计算结果:', e.data) }
self.onmessage = (e) => { const result = heavyComputation(e.data) self.postMessage(result) }
|
适用场景:
三、缓存策略
技巧 9:HTTP 缓存配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { expires 1y; add_header Cache-Control "public, immutable"; }
location ~* \.html$ { expires -1; add_header Cache-Control "no-cache, no-store, must-revalidate"; }
location /api/ { add_header Cache-Control "no-cache"; }
|
技巧 10:Service 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
| import { registerRoute } from 'workbox-routing' import { CacheFirst, NetworkFirst } from 'workbox-strategies' import { ExpirationPlugin } from 'workbox-expiration'
registerRoute( /\.(?:png|jpg|jpeg|svg|gif|css|js)$/, new CacheFirst({ cacheName: 'static-resources', plugins: [ new ExpirationPlugin({ maxEntries: 60, maxAgeSeconds: 30 * 24 * 60 * 60, }), ], }) )
registerRoute( /\/api\//, new NetworkFirst({ cacheName: 'api-cache', networkTimeoutSeconds: 3, }) )
|
四、构建优化
技巧 11:Webpack/Vite 配置优化
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
| module.exports = { optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendors: { test: /[\\/]node_modules[\\/]/, name: 'vendors', chunks: 'all', }, }, }, runtimeChunk: 'single', }, mode: 'production', terserOptions: { compress: { drop_console: true, drop_debugger: true, }, }, }
|
技巧 12:分析打包体积
1 2 3 4 5 6 7 8 9 10 11 12 13
| npm install --save-dev webpack-bundle-analyzer
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
plugins: [ new BundleAnalyzerPlugin({ analyzerMode: 'static', // 生成报告文件 }), ]
|
我优化过的案例:
- moment.js → dayjs(200KB → 2KB)
- lodash 全量 → 按需(70KB → 5KB)
- 多个图表库 → 统一用 ECharts(减少重复)
五、性能监控
技巧 13:核心性能指标监控
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| import { onCLS, onFID, onFCP, onLCP, onTTFB } from 'web-vitals'
onCLS(console.log) onFID(console.log) onFCP(console.log) onLCP(console.log) onTTFB(console.log)
function sendToAnalytics(metric) { const body = { name: metric.name, value: metric.value, delta: metric.delta, rating: metric.rating, id: metric.id, navigationType: metric.navigationType, url: window.location.href, } navigator.sendBeacon('/analytics', JSON.stringify(body)) }
|
技巧 14:错误监控
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
| window.addEventListener('error', (event) => { reportError({ type: 'js-error', message: event.message, filename: event.filename, lineno: event.lineno, colno: event.colno, stack: event.error?.stack, }) })
window.addEventListener('unhandledrejection', (event) => { reportError({ type: 'promise-rejection', reason: event.reason, }) })
class ErrorBoundary extends React.Component { componentDidCatch(error, errorInfo) { reportError({ type: 'react-error', error, componentStack: errorInfo.componentStack, }) } render() { return this.props.children } }
|
性能优化清单
加载性能
- 代码分割(路由级别 + 组件级别)
- 图片优化(格式、尺寸、懒加载)
- 资源预加载(preload/prefetch)
- Tree Shaking
- CDN 加速
- Gzip/Brotli 压缩
运行时性能
- 避免不必要的重渲染
- 大数据用虚拟列表
- 防抖节流
- Web Worker 处理重计算
- 使用 CSS transform 代替位置变化
缓存
- HTTP 缓存配置
- Service Worker
- 本地存储策略
监控
总结
性能优化是一个持续的过程,不是一蹴而就的。我的建议:
- 先测量:用数据说话,不要凭感觉
- 抓重点:优先优化影响最大的瓶颈
- 渐进式:不要一次性做太多,逐步验证
- 监控:优化后要持续监控,防止回退
性能优化的本质:在用户体验和开发成本之间找到平衡点。
以上就是我在性能优化方面的一些经验总结。如果你有任何问题或更好的优化技巧,欢迎交流讨论!