性能优化

前端性能优化

核心优化方向(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/Brotli压缩
// nginx配置
gzip on;
gzip_types text/plain text/css application/json application/javascript;
gzip_min_length 1000;

// Webpack配置
const CompressionPlugin = require('compression-webpack-plugin');
module.exports = {
plugins: [
new CompressionPlugin({
algorithm: 'gzip',
test: /\.(js|css|html|svg)$/,
threshold: 10240, // 10KB以上才压缩
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
// 1. 选择合适的图片格式
// - JPEG:照片、复杂图像
// - PNG:透明图、简单图标
// - WebP:现代浏览器首选(体积小30%)
// - SVG:矢量图、图标
// - Base64:小图标内联(<10KB)

// 2. 图片懒加载
<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));
};

// 3. 响应式图片
<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
// 1. 路由懒加载
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>
);
}

// 2. 动态导入
button.addEventListener('click', async () => {
const module = await import('./heavy-module.js');
module.doSomething();
});

// 3. Webpack代码分割
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';

// ES6模块支持Tree Shaking
// package.json
{
"sideEffects": false // 标记无副作用,可安全删除未使用代码
}

1.5 预加载策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- 1. DNS预解析 -->
<link rel="dns-prefetch" href="https://cdn.example.com">

<!-- 2. 预连接 -->
<link rel="preconnect" href="https://api.example.com">

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

<!-- 4. 预获取下一页资源 -->
<link rel="prefetch" href="next-page.js">

<!-- 5. 预渲染下一页 -->
<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;';

// ✅ 好:使用class
element.className = 'large-box';

// ✅ 好:离线操作DOM
element.style.display = 'none';
// 进行大量DOM操作
element.style.display = 'block';

// ✅ 好:使用DocumentFragment
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);

2.2 使用transform代替定位

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);
};
}

// 使用场景:搜索框输入、窗口resize
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
// ❌ 不好:使用setTimeout
function animate() {
element.style.left = element.offsetLeft + 1 + 'px';
setTimeout(animate, 16); // 约60fps
}

// ✅ 好:使用requestAnimationFrame
function animate() {
element.style.left = element.offsetLeft + 1 + 'px';
requestAnimationFrame(animate);
}
requestAnimationFrame(animate);

// 更好:使用transform + CSS动画
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
// 1. 合并文件
// - CSS Sprites(雪碧图)
// - 合并JS/CSS文件
// - 使用字体图标(iconfont)

// 2. 内联小资源
// - 小图片转Base64
// - 关键CSS内联
<style>
/* 首屏关键CSS */
</style>

// 3. 使用HTTP/2
// - 多路复用,一个连接并发多个请求
// - 头部压缩
// - 服务器推送

3.2 CDN加速

1
2
3
4
5
6
7
8
9
// 静态资源使用CDN
<script src="https://cdn.jsdelivr.net/npm/vue@3/dist/vue.global.js"></script>

// 配置多个CDN域名(突破浏览器并发限制)
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>

<!-- CSS放head,JS放body底部 -->

4. 缓存优化

4.1 HTTP缓存策略

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 1. 强缓存(不请求服务器)
Cache-Control: max-age=31536000 // 1年
Expires: Wed, 21 Oct 2025 07:28:00 GMT

// 2. 协商缓存(询问服务器)
// ETag方式
ETag: "33a64df551425fcc55e4d42a148795d9f25f89d4"
If-None-Match: "33a64df551425fcc55e4d42a148795d9f25f89d4"

// Last-Modified方式
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
If-Modified-Since: Wed, 21 Oct 2024 07:28:00 GMT

// 3. 缓存策略建议
// - HTML: no-cache(协商缓存)
// - CSS/JS: max-age=31536000 + 文件名hash
// - 图片: max-age=31536000

4.2 本地存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 1. LocalStorage(5MB,持久化)
localStorage.setItem('user', JSON.stringify(userData));
const user = JSON.parse(localStorage.getItem('user'));

// 2. SessionStorage(5MB,会话级)
sessionStorage.setItem('token', 'xxx');

// 3. IndexedDB(大容量,异步)
const request = indexedDB.open('myDB', 1);
request.onsuccess = (event) => {
const db = event.target.result;
// 存储大量数据
};

// 4. Service Worker缓存
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
// 1. 内存缓存
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;
}

// 2. 使用SWR策略(stale-while-revalidate)
// 先返回缓存,后台更新
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 = {
// 1. 生产模式
mode: 'production',

// 2. 代码分割
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
priority: 10
}
}
},
// 提取runtime代码
runtimeChunk: 'single'
},

// 3. 压缩
optimization: {
minimize: true,
minimizer: [
new TerserPlugin({
parallel: true, // 多进程压缩
terserOptions: {
compress: {
drop_console: true // 删除console
}
}
})
]
},

// 4. 缓存
cache: {
type: 'filesystem'
},

// 5. 排除不需要的依赖
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

// webpack配置
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
// 1. LCP(Largest Contentful Paint)最大内容绘制
// 目标:< 2.5s
// 优化:优化图片、减少阻塞、使用CDN

// 2. FID(First Input Delay)首次输入延迟
// 目标:< 100ms
// 优化:减少JS执行时间、代码分割

// 3. CLS(Cumulative Layout Shift)累积布局偏移
// 目标:< 0.1
// 优化:图片设置宽高、避免动态插入内容

// 监控代码
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
// FCP(First Contentful Paint)首次内容绘制
// TTI(Time to Interactive)可交互时间
// TTFB(Time to First Byte)首字节时间

// 使用Performance API监控
window.addEventListener('load', () => {
const timing = performance.timing;

// DNS查询时间
const dns = timing.domainLookupEnd - timing.domainLookupStart;

// TCP连接时间
const tcp = timing.connectEnd - timing.connectStart;

// 请求时间
const request = timing.responseEnd - timing.requestStart;

// 白屏时间
const blank = timing.domLoading - timing.fetchStart;

// DOM解析时间
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: 首屏加载优化有哪些方案?

  1. 路由懒加载,只加载首屏需要的代码
  2. 图片懒加载,首屏外图片延迟加载
  3. 关键CSS内联,减少阻塞
  4. SSR服务端渲染,直接返回HTML
  5. 骨架屏,提升用户体验
  6. 使用CDN加速静态资源
  7. 开启Gzip压缩
  8. 预加载关键资源(preload)

Q2: 重排和重绘如何优化?

减少重排:

  • 批量修改DOM(DocumentFragment)
  • 使用class代替逐条修改样式
  • 使用transform代替top/left
  • 避免频繁读取offsetTop等属性
  • 将DOM离线后修改(display:none)

减少重绘:

  • 使用CSS3动画(GPU加速)
  • 避免频繁修改颜色、背景
  • 使用will-change提示浏览器

Q3: 图片优化有哪些方案?

  1. 选择合适格式(WebP > JPEG/PNG)
  2. 图片压缩(TinyPNG、ImageOptim)
  3. 响应式图片(srcset、picture)
  4. 懒加载(Intersection Observer)
  5. 雪碧图(CSS Sprites)
  6. Base64内联小图标
  7. CDN + 参数裁剪
  8. 使用SVG代替位图图标

Q4: Webpack如何优化打包速度?

  1. 使用cache缓存
  2. 多进程打包(thread-loader)
  3. 缩小构建范围(exclude、include)
  4. DllPlugin预编译第三方库
  5. 使用更快的loader(esbuild-loader)
  6. 开启持久化缓存
  7. 升级到Webpack 5

Q5: 如何监控前端性能?

  1. Performance API获取性能数据
  2. PerformanceObserver监听性能指标
  3. 上报到监控平台(Sentry、阿里云ARMS)
  4. Lighthouse自动化测试
  5. Chrome DevTools性能分析
  6. 真实用户监控(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
// .eslintrc.js
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']
}
};

// package.json
{
"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
// .prettierrc.js
module.exports = {
printWidth: 100,
tabWidth: 2,
useTabs: false,
semi: true,
singleQuote: true,
trailingComma: 'es5',
bracketSpacing: true,
arrowParens: 'avoid'
};

// package.json
{
"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
// .stylelintrc.js
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 + lint-staged
// package.json
{
"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
// commitlint.config.js
module.exports = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
[
'feat', // 新功能
'fix', // 修复bug
'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
// webpack.config.js
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: [
// JS/TS
{
test: /\.(js|jsx|ts|tsx)$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: {
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript'
]
}
}
},
// CSS
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'postcss-loader'
]
},
// 图片
{
test: /\.(png|jpg|gif|svg)$/,
type: 'asset',
parser: {
dataUrlCondition: {
maxSize: 10 * 1024 // 10KB以下转base64
}
}
}
]
},

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
// vite.config.js
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
// babel.config.js
module.exports = {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage', // 按需引入polyfill
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
// postcss.config.js
module.exports = {
plugins: {
'autoprefixer': {}, // 自动添加浏览器前缀
'postcss-pxtorem': { // px转rem
rootValue: 16,
propList: ['*']
},
'cssnano': { // 压缩CSS
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
// 1. IIFE(立即执行函数)
(function() {
var module = {};
// ...
window.myModule = module;
})();

// 2. CommonJS(Node.js)
// 导出
module.exports = { name: 'module' };
exports.name = 'module';

// 导入
const module = require('./module');

// 3. AMD(RequireJS)
define(['jquery'], function($) {
return { name: 'module' };
});

// 4. ES6 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
// CommonJS
// - 运行时加载(动态)
// - 值拷贝
// - 同步加载
const module = require('./module');
if (condition) {
const module2 = require('./module2'); // 可以条件加载
}

// ES Module
// - 编译时加载(静态)
// - 值引用
// - 异步加载
// - 支持Tree Shaking
import module from './module';
// import module2 from './module2'; // 不能条件加载,必须顶层

// 动态导入
if (condition) {
import('./module2').then(module => {
// 使用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
// sum.js
export function sum(a, b) {
return a + b;
}

// sum.test.js
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);
});
});

// React组件测试
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
// API测试
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
// cypress/integration/login.spec.js
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
// package.json
{
"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
}
}
}
}