性能优化
发表于更新于
常见们性能优化
OHNII前端性能优化
核心优化方向(6大维度)
记忆口诀:加载 → 渲染 → 交互 → 网络 → 缓存 → 构建
1 2 3 4 5 6
| 1. 加载优化:减少资源体积,加快加载速度 2. 渲染优化:减少重排重绘,提升渲染性能 3. 交互优化:提升用户体验,减少卡顿 4. 网络优化:减少请求次数,优化传输 5. 缓存优化:合理利用缓存,减少重复请求 6. 构建优化:打包体积优化,代码分割
|
1. 加载性能优化
1.1 资源压缩
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
gzip on; gzip_types text/plain text/css application/json application/javascript; gzip_min_length 1000;
const CompressionPlugin = require('compression-webpack-plugin'); module.exports = { plugins: [ new CompressionPlugin({ algorithm: 'gzip', test: /\.(js|css|html|svg)$/, threshold: 10240, minRatio: 0.8 }) ] }
|
1.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 28 29 30 31 32 33
|
<img src="placeholder.jpg" data-src="real-image.jpg" loading="lazy" />
const lazyLoad = () => { const images = document.querySelectorAll('img[data-src]'); const observer = new IntersectionObserver((entries) => { entries.forEach(entry => { if (entry.isIntersecting) { const img = entry.target; img.src = img.dataset.src; observer.unobserve(img); } }); }); images.forEach(img => observer.observe(img)); };
<picture> <source srcset="image-large.jpg" media="(min-width: 1200px)"> <source srcset="image-medium.jpg" media="(min-width: 768px)"> <img src="image-small.jpg" alt="responsive image"> </picture>
// 4. 图片CDN + 参数裁剪 <img src="https://cdn.example.com/image.jpg?w=300&h=200&q=80" />
|
1.3 代码分割(Code Splitting)
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
| import { lazy, Suspense } from 'react';
const Home = lazy(() => import('./pages/Home')); const About = lazy(() => import('./pages/About'));
function App() { return ( <Suspense fallback={<div>Loading...</div>}> <Routes> <Route path="/" element={<Home />} /> <Route path="/about" element={<About />} /> </Routes> </Suspense> ); }
button.addEventListener('click', async () => { const module = await import('./heavy-module.js'); module.doSomething(); });
module.exports = { optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10 }, common: { minChunks: 2, priority: 5, reuseExistingChunk: true } } } } }
|
1.4 Tree Shaking(摇树优化)
1 2 3 4 5 6 7 8 9 10 11 12
|
import _ from 'lodash';
import debounce from 'lodash/debounce';
{ "sideEffects": false }
|
1.5 预加载策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <link rel="dns-prefetch" href="https://cdn.example.com">
<link rel="preconnect" href="https://api.example.com">
<link rel="preload" href="critical.css" as="style"> <link rel="preload" href="font.woff2" as="font" crossorigin>
<link rel="prefetch" href="next-page.js">
<link rel="prerender" href="next-page.html">
|
2. 渲染性能优化
2.1 减少重排(Reflow)
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
| for (let i = 0; i < 1000; i++) { element.style.width = i + 'px'; element.style.height = i + 'px'; }
element.style.cssText = 'width: 1000px; height: 1000px;';
element.className = 'large-box';
element.style.display = 'none';
element.style.display = 'block';
const fragment = document.createDocumentFragment(); for (let i = 0; i < 1000; i++) { const li = document.createElement('li'); li.textContent = i; fragment.appendChild(li); } ul.appendChild(fragment);
|
1 2 3 4 5 6 7 8 9 10 11 12
| .box { position: absolute; left: 100px; top: 100px; }
.box { transform: translate(100px, 100px); will-change: transform; }
|
2.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
| function VirtualList({ items, itemHeight, containerHeight }) { const [scrollTop, setScrollTop] = useState(0); const visibleCount = Math.ceil(containerHeight / itemHeight); const startIndex = Math.floor(scrollTop / itemHeight); const endIndex = startIndex + visibleCount; const visibleItems = items.slice(startIndex, endIndex); const offsetY = startIndex * itemHeight; return ( <div style={{ height: containerHeight, overflow: 'auto' }} onScroll={(e) => setScrollTop(e.target.scrollTop)} > <div style={{ height: items.length * itemHeight }}> <div style={{ transform: `translateY(${offsetY}px)` }}> {visibleItems.map((item, index) => ( <div key={startIndex + index} style={{ height: itemHeight }}> {item} </div> ))} </div> </div> </div> ); }
|
2.4 防抖与节流
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
| function debounce(fn, delay) { let timer = null; return function(...args) { clearTimeout(timer); timer = setTimeout(() => { fn.apply(this, args); }, delay); }; }
const handleSearch = debounce((value) => { console.log('搜索:', value); }, 300);
function throttle(fn, delay) { let lastTime = 0; return function(...args) { const now = Date.now(); if (now - lastTime >= delay) { fn.apply(this, args); lastTime = now; } }; }
const handleScroll = throttle(() => { console.log('滚动位置:', window.scrollY); }, 200);
|
2.5 requestAnimationFrame优化动画
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| function animate() { element.style.left = element.offsetLeft + 1 + 'px'; setTimeout(animate, 16); }
function animate() { element.style.left = element.offsetLeft + 1 + 'px'; requestAnimationFrame(animate); } requestAnimationFrame(animate);
element.style.transform = `translateX(${x}px)`;
|
3. 网络性能优化
3.1 减少HTTP请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
<style> </style>
|
3.2 CDN加速
1 2 3 4 5 6 7 8 9
| <script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>
const cdnDomains = [ 'https://cdn1.example.com', 'https://cdn2.example.com', 'https://cdn3.example.com' ];
|
3.3 资源优先级
1 2 3 4 5 6 7 8
| <link rel="preload" href="critical.css" as="style">
<script src="analytics.js" defer></script> <script src="non-critical.js" async></script>
|
4. 缓存优化
4.1 HTTP缓存策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| Cache-Control: max-age=31536000 Expires: Wed, 21 Oct 2025 07:28:00 GMT
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4" If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT
|
4.2 本地存储
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| localStorage.setItem('user', JSON.stringify(userData)); const user = JSON.parse(localStorage.getItem('user'));
sessionStorage.setItem('token', 'xxx');
const request = indexedDB.open('myDB', 1); request.onsuccess = (event) => { const db = event.target.result; };
self.addEventListener('fetch', (event) => { event.respondWith( caches.match(event.request).then((response) => { return response || fetch(event.request); }) ); });
|
4.3 接口缓存
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| const cache = new Map();
async function fetchWithCache(url, ttl = 60000) { const cached = cache.get(url); if (cached && Date.now() - cached.time < ttl) { return cached.data; } const data = await fetch(url).then(res => res.json()); cache.set(url, { data, time: Date.now() }); return data; }
import useSWR from 'swr';
function Profile() { const { data, error } = useSWR('/api/user', fetcher); }
|
5. 构建优化
5.1 Webpack优化
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
| module.exports = { mode: 'production', optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10 } } }, runtimeChunk: 'single' }, optimization: { minimize: true, minimizer: [ new TerserPlugin({ parallel: true, terserOptions: { compress: { drop_console: true } } }) ] }, cache: { type: 'filesystem' }, externals: { 'react': 'React', 'react-dom': 'ReactDOM' } }
|
5.2 打包体积分析
1 2 3 4 5 6 7 8 9 10 11 12 13
| npm install webpack-bundle-analyzer -D
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
module.exports = { plugins: [ new BundleAnalyzerPlugin() ] }
|
6. 性能监控指标
6.1 核心Web指标(Core Web Vitals)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
new PerformanceObserver((list) => { for (const entry of list.getEntries()) { console.log('LCP:', entry.renderTime || entry.loadTime); } }).observe({ entryTypes: ['largest-contentful-paint'] });
|
6.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 28 29 30
|
window.addEventListener('load', () => { const timing = performance.timing; const dns = timing.domainLookupEnd - timing.domainLookupStart; const tcp = timing.connectEnd - timing.connectStart; const request = timing.responseEnd - timing.requestStart; const blank = timing.domLoading - timing.fetchStart; const dom = timing.domComplete - timing.domLoading; const load = timing.loadEventEnd - timing.fetchStart; console.log({ dns, tcp, request, blank, dom, load }); });
|
7. 面试口述版本
面试官:请说说前端性能优化的方案
回答框架:
“前端性能优化可以从6个维度来考虑:
1. 加载优化
- 资源压缩:Gzip、代码压缩、图片压缩
- 代码分割:路由懒加载、动态导入
- Tree Shaking:删除未使用代码
- 预加载:dns-prefetch、preload、prefetch
2. 渲染优化
- 减少重排重绘:批量修改DOM、使用transform
- 虚拟列表:只渲染可视区域
- 防抖节流:优化高频事件
- requestAnimationFrame:优化动画
3. 网络优化
- 减少HTTP请求:合并文件、雪碧图
- CDN加速:静态资源使用CDN
- HTTP/2:多路复用、头部压缩
4. 缓存优化
- HTTP缓存:强缓存、协商缓存
- 本地存储:LocalStorage、IndexedDB
- Service Worker:离线缓存
5. 构建优化
- Webpack优化:代码分割、压缩、Tree Shaking
- 打包分析:找出体积大的模块优化
6. 监控指标
- Core Web Vitals:LCP、FID、CLS
- 使用Performance API监控性能
具体实施时,需要根据项目实际情况,使用工具分析性能瓶颈,针对性优化。”
8. 高频追问
Q1: 首屏加载优化有哪些方案?
- 路由懒加载,只加载首屏需要的代码
- 图片懒加载,首屏外图片延迟加载
- 关键CSS内联,减少阻塞
- SSR服务端渲染,直接返回HTML
- 骨架屏,提升用户体验
- 使用CDN加速静态资源
- 开启Gzip压缩
- 预加载关键资源(preload)
Q2: 重排和重绘如何优化?
减少重排:
- 批量修改DOM(DocumentFragment)
- 使用class代替逐条修改样式
- 使用transform代替top/left
- 避免频繁读取offsetTop等属性
- 将DOM离线后修改(display:none)
减少重绘:
- 使用CSS3动画(GPU加速)
- 避免频繁修改颜色、背景
- 使用will-change提示浏览器
Q3: 图片优化有哪些方案?
- 选择合适格式(WebP > JPEG/PNG)
- 图片压缩(TinyPNG、ImageOptim)
- 响应式图片(srcset、picture)
- 懒加载(Intersection Observer)
- 雪碧图(CSS Sprites)
- Base64内联小图标
- CDN + 参数裁剪
- 使用SVG代替位图图标
Q4: Webpack如何优化打包速度?
- 使用cache缓存
- 多进程打包(thread-loader)
- 缩小构建范围(exclude、include)
- DllPlugin预编译第三方库
- 使用更快的loader(esbuild-loader)
- 开启持久化缓存
- 升级到Webpack 5
Q5: 如何监控前端性能?
- Performance API获取性能数据
- PerformanceObserver监听性能指标
- 上报到监控平台(Sentry、阿里云ARMS)
- Lighthouse自动化测试
- Chrome DevTools性能分析
- 真实用户监控(RUM)
Q6: SSR和CSR的区别?如何选择?
SSR(服务端渲染):
- 优点:首屏快、SEO友好
- 缺点:服务器压力大、开发复杂
CSR(客户端渲染):
- 优点:服务器压力小、交互流畅
- 缺点:首屏慢、SEO不友好
选择建议:
- 内容型网站(新闻、博客):SSR
- 应用型网站(后台管理):CSR
- 混合方案:首屏SSR + 后续CSR(Next.js、Nuxt.js)
9. 实战优化案例
案例1:首屏加载从5s优化到1.5s
1 2 3 4 5 6 7 8 9 10 11 12
| 问题分析: - 首屏加载了所有路由代码(2MB) - 图片未压缩(单张500KB) - 未开启Gzip
优化方案: 1. 路由懒加载:减少首屏代码到500KB 2. 图片压缩+WebP:单张降到50KB 3. 开启Gzip:代码体积减少70% 4. 使用CDN:加载速度提升50%
结果:首屏时间从5s降到1.5s
|
案例2:长列表滚动卡顿优化
1 2 3 4 5 6 7 8 9
| 问题:渲染10000条数据,滚动卡顿
优化方案: 1. 虚拟列表:只渲染可视区域20条 2. 使用transform代替top定位 3. 节流滚动事件 4. 使用requestAnimationFrame
结果:滚动流畅,FPS从20提升到60
|
案例3:打包体积从5MB优化到1MB
1 2 3 4 5 6 7 8 9 10 11 12
| 问题分析: - moment.js包含所有语言包(500KB) - lodash全量引入(300KB) - 重复打包公共代码
优化方案: 1. moment.js替换为day.js(2KB) 2. lodash按需引入 3. 代码分割,提取公共代码 4. Tree Shaking删除未使用代码
结果:打包体积从5MB降到1MB
|
前端工程化
核心体系(5大支柱)
记忆口诀:规范 → 构建 → 测试 → 部署 → 监控
1 2 3 4 5
| 1. 代码规范:统一代码风格,提升代码质量 2. 构建工具:模块化、打包、优化 3. 自动化测试:保证代码质量 4. CI/CD:自动化部署流程 5. 监控体系:性能监控、错误追踪
|
1. 代码规范体系
1.1 ESLint(代码质量检查)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| module.exports = { extends: [ 'eslint:recommended', 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended' ], parser: '@typescript-eslint/parser', plugins: ['react', '@typescript-eslint'], rules: { 'no-console': 'warn', 'no-unused-vars': 'error', 'semi': ['error', 'always'], 'quotes': ['error', 'single'] } };
{ "scripts": { "lint": "eslint src --ext .js,.jsx,.ts,.tsx", "lint:fix": "eslint src --ext .js,.jsx,.ts,.tsx --fix" } }
|
1.2 Prettier(代码格式化)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| module.exports = { printWidth: 100, tabWidth: 2, useTabs: false, semi: true, singleQuote: true, trailingComma: 'es5', bracketSpacing: true, arrowParens: 'avoid' };
{ "scripts": { "format": "prettier --write \"src/**/*.{js,jsx,ts,tsx,json,css,md}\"" } }
|
1.3 Stylelint(CSS规范)
1 2 3 4 5 6 7 8 9
| module.exports = { extends: ['stylelint-config-standard'], rules: { 'color-hex-case': 'lower', 'color-hex-length': 'short', 'selector-class-pattern': '^[a-z][a-zA-Z0-9]+$' } };
|
1.4 Git Hooks(提交前检查)
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
|
{ "husky": { "hooks": { "pre-commit": "lint-staged", "commit-msg": "commitlint -E HUSKY_GIT_PARAMS" } }, "lint-staged": { "*.{js,jsx,ts,tsx}": [ "eslint --fix", "prettier --write" ], "*.{css,scss}": [ "stylelint --fix", "prettier --write" ] } }
npm install husky lint-staged -D npx husky install npx husky add .husky/pre-commit "npx lint-staged"
|
1.5 Commitlint(提交信息规范)
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
| module.exports = { extends: ['@commitlint/config-conventional'], rules: { 'type-enum': [ 2, 'always', [ 'feat', 'fix', 'docs', 'style', 'refactor', 'perf', 'test', 'chore', 'revert' ] ] } };
git commit -m "feat: 添加用户登录功能" git commit -m "fix: 修复列表页分页bug" git commit -m "docs: 更新README文档"
|
2. 构建工具体系
2.1 Webpack配置
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
| const path = require('path'); const HtmlWebpackPlugin = require('html-webpack-plugin'); const MiniCssExtractPlugin = require('mini-css-extract-plugin'); const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = { entry: './src/index.js', output: { path: path.resolve(__dirname, 'dist'), filename: '[name].[contenthash:8].js', clean: true }, module: { rules: [ { test: /\.(js|jsx|ts|tsx)$/, exclude: /node_modules/, use: { loader: 'babel-loader', options: { presets: [ '@babel/preset-env', '@babel/preset-react', '@babel/preset-typescript' ] } } }, { test: /\.css$/, use: [ MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader' ] }, { test: /\.(png|jpg|gif|svg)$/, type: 'asset', parser: { dataUrlCondition: { maxSize: 10 * 1024 } } } ] }, plugins: [ new CleanWebpackPlugin(), new HtmlWebpackPlugin({ template: './public/index.html' }), new MiniCssExtractPlugin({ filename: '[name].[contenthash:8].css' }) ], optimization: { splitChunks: { chunks: 'all', cacheGroups: { vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10 } } } }, devServer: { port: 3000, hot: true, open: true, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } };
|
2.2 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 29 30 31 32 33 34 35 36 37 38
| import { defineConfig } from 'vite'; import react from '@vitejs/plugin-react'; import path from 'path';
export default defineConfig({ plugins: [react()], resolve: { alias: { '@': path.resolve(__dirname, 'src') } }, server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } }, build: { outDir: 'dist', sourcemap: false, rollupOptions: { output: { manualChunks: { 'react-vendor': ['react', 'react-dom'], 'router': ['react-router-dom'] } } } } });
|
2.3 Babel配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| module.exports = { presets: [ [ '@babel/preset-env', { useBuiltIns: 'usage', corejs: 3, targets: { browsers: ['> 1%', 'last 2 versions', 'not dead'] } } ], '@babel/preset-react', '@babel/preset-typescript' ], plugins: [ '@babel/plugin-transform-runtime', ['import', { libraryName: 'antd', style: true }] ] };
|
2.4 PostCSS配置
1 2 3 4 5 6 7 8 9 10 11 12 13
| module.exports = { plugins: { 'autoprefixer': {}, 'postcss-pxtorem': { rootValue: 16, propList: ['*'] }, 'cssnano': { preset: 'default' } } };
|
3. 模块化方案
3.1 模块化演进
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
| (function() { var module = {}; window.myModule = module; })();
module.exports = { name: 'module' }; exports.name = 'module';
const module = require('./module');
define(['jquery'], function($) { return { name: 'module' }; });
export const name = 'module'; export default function() {}
import module from './module'; import { name } from './module';
|
3.2 CommonJS vs ES Module
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
|
const module = require('./module'); if (condition) { const module2 = require('./module2'); }
import module from './module';
if (condition) { import('./module2').then(module => { }); }
|
4. 自动化测试
4.1 单元测试(Jest)
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
| export function sum(a, b) { return a + b; }
import { sum } from './sum';
describe('sum function', () => { test('adds 1 + 2 to equal 3', () => { expect(sum(1, 2)).toBe(3); }); test('adds -1 + 1 to equal 0', () => { expect(sum(-1, 1)).toBe(0); }); });
import { render, screen, fireEvent } from '@testing-library/react'; import Button from './Button';
test('button click', () => { const handleClick = jest.fn(); render(<Button onClick={handleClick}>Click me</Button>); const button = screen.getByText('Click me'); fireEvent.click(button); expect(handleClick).toHaveBeenCalledTimes(1); });
|
4.2 集成测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| import axios from 'axios'; import MockAdapter from 'axios-mock-adapter';
const mock = new MockAdapter(axios);
test('fetch user data', async () => { mock.onGet('/api/user/1').reply(200, { id: 1, name: 'John' }); const response = await axios.get('/api/user/1'); expect(response.data.name).toBe('John'); });
|
4.3 E2E测试(Cypress)
1 2 3 4 5 6 7 8 9 10 11
| describe('Login Page', () => { it('should login successfully', () => { cy.visit('/login'); cy.get('input[name="username"]').type('admin'); cy.get('input[name="password"]').type('123456'); cy.get('button[type="submit"]').click(); cy.url().should('include', '/dashboard'); cy.contains('Welcome').should('be.visible'); }); });
|
4.4 测试覆盖率
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| { "scripts": { "test": "jest", "test:coverage": "jest --coverage" }, "jest": { "collectCoverageFrom": [ "src/**/*.{js,jsx,ts,tsx}", "!src/**/*.d.ts" ], "coverageThreshold": { "global": { "branches": 80, "functions": 80, "lines": 80, "statements": 80 } } } }
|