Performance Troubleshooting

性能问题排查实战手册

14个全链路性能问题 × 从发现到修复到验证 × 覆盖前端/后端/数据库/基础设施。每个问题都有完整的发现→排查→原理→修复→验证流程,以电商小程序为业务场景贯穿全程。

📝 14 个性能问题 🐛 前端/后端/DB/Redis/Infra 🔧 完整排查工具链 ⏱️ 约 90 分钟阅读
FCP慢 RT飙高 慢查询 Redis命中率 GC频繁 线程池满 渲染卡顿 CDN回源 大文件上传 WebSocket断连 N+1查询 网络抖动 磁盘IO瓶颈 内存泄漏
P1
页面FCP慢 — 首屏白屏时间过长
严重程度:高 发生频率:高频 排查难度:中 修复成本:低
电商小程序首页打开后白屏长达 3.2s,用户以为页面挂了直接退出。Lighthouse 评分 Performance 只有 38 分。运营反馈:首页跳出率从 25% 飙升到 58%。

🔍 发现过程 — Lighthouse + 用户反馈

运营同学在周会上反馈:小程序首页跳出率异常升高。技术团队用 Lighthouse 跑了一遍首页:

Chrome DevTools — Lighthouse Report
38
Performance
45
FCP
62
LCP
28
TTI
12
SI
关键指标:
FCP (First Contentful Paint): 3.2s — 红色(差)
LCP (Largest Contentful Paint): 4.8s — 橙色(需改进)
TTI (Time to Interactive): 6.1s — 红色(差)
阻塞渲染的资源:4 个 CSS、3 个 JS(未加 async/defer)

🔧 排查步骤 — Chrome DevTools Network 面板

打开 DevTools → Network 面板,刷新首页,查看资源加载瀑布流:

Chrome DevTools — Network Waterfall
NameSizeTimeWaterfall
index.html12 KB200ms
vendor.js1.2 MB2.8s
app.js380 KB1.2s
main.css89 KB800ms
lodash.js540 KB1.5s
moment.js310 KB900ms

分析发现的问题:

1. vendor.js 1.2MB 未拆分,加载 2.8s,阻塞渲染

2. lodash 全量引入 540KB,实际只用了 3 个函数

3. moment.js 310KB,包含全部 locale 文件

4. CSS/JS 未加 async/defer,全部阻塞渲染

5. 没有 gzip/brotli 压缩,没有开启 CDN

💡 原理讲解 — 为什么 FCP 会慢?

FCP 慢的本质:关键渲染路径上的资源太重

浏览器从拿到 HTML 到渲染出第一个内容(FCP),要经历:① 下载 HTML → ② 下载 CSS/JS(阻塞渲染)→ ③ 解析编译 JS → ④ 构建 DOM + CSSOM → 渲染树 → 绘制

如果 JS 太大(vendor.js 1.2MB),下载和编译都要好几秒,FCP 自然慢。关键渲染路径上的每个阻塞资源都会拖慢首屏。

技术原理:浏览器渲染流程 — HTML 解析 → 遇到 CSS 会阻塞渲染(等 CSSOM)→ 遇到没有 async/defer 的 JS 会同时阻塞解析和渲染。关键渲染路径上的资源越多越重,FCP 越慢。

🛠️ 修复方案

❌ 修复前
// Vite 默认打包,所有依赖打进单个 vendor // vite.config.ts(默认配置,未做代码分割) export default defineConfig({}) // main.js — 全量引入 lodash 和 moment import _ from 'lodash' import moment from 'moment' import 'moment/locale/zh-cn' // index.html — JS 放 head,阻塞渲染 <script src="/vendor.js"></script> <script src="/app.js"></script>
✅ 修复后
// Vite 代码分割 + 按需加载 // vite.config.ts export default defineConfig({ build: { rollupOptions: { output: { manualChunks: { vendor: ['vue', 'vue-router'], elementPlus: ['element-plus'] } } } } }) // 按需引入 lodash 函数 import debounce from 'lodash/debounce' import throttle from 'lodash/throttle' // 用 dayjs 替代 moment(2KB vs 310KB) import dayjs from 'dayjs' // index.html — JS 加 defer <script defer src="/vendor.js"></script> <script defer src="/app.js"></script>
/* 额外优化:Nginx 开启 gzip + 静态资源上 CDN */ # nginx.conf gzip on; gzip_types text/css application/javascript application/json; gzip_min_length 1024; # 静态资源设置长期缓存 location /static/ { expires 365d; add_header Cache-Control "public, immutable"; }
✅ 验证结果
指标修复前修复后提升
FCP3.2s0.8s↓ 75%
LCP4.8s1.6s↓ 67%
TTI6.1s2.3s↓ 62%
vendor.js 体积1.2MB280KB (gzip)↓ 77%
Lighthouse 评分3892↑ 142%
首页跳出率58%22%↓ 62%
P2
接口RT飙高 — 响应时间从200ms飙到3s
严重程度:高 发生频率:间歇性 排查难度:高 修复成本:中
电商小程序「下单」接口 /api/order/create 的平均 RT 从平时的 200ms 突然飙到 3s。Grafana 监控告警触发,高峰期大量用户反馈"下单转圈很久"。

🔍 发现过程 — Grafana 告警

🚨 Grafana Alert — /api/order/create RT 异常
14:0014:1014:2014:3014:4014:5015:00
P99 RT: 3.2s
P95 RT: 2.1s
P50 RT: 680ms
QPS: 320

🔧 排查步骤 — Arthas 追踪慢方法

用 Arthas 在线追踪接口调用链,找到耗时最长的环节:

Arthas — trace 在线追踪
$ trace com.milktea.controller.OrderController createOrder -n 5 `---[3021ms] com.milktea.controller.OrderController:createOrder() +---[45ms] com.milktea.service.OrderService:validateOrder() +---[1850ms] com.milktea.service.InventoryService:checkStock() +---[890ms] com.milktea.service.CouponService:validateCoupon() +---[120ms] com.milktea.service.OrderService:createOrder() `---[56ms] com.milktea.dao.OrderDao:insert() $ trace com.milktea.service.InventoryService checkStock -n 5 `---[1830ms] com.milktea.service.InventoryService:checkStock() +---[1700ms] com.milktea.service.InventoryService:getStockFromCache() `---[12ms] com.milktea.service.InventoryService:queryDB() ⚠️ 发现根因:getStockFromCache() 耗时 1700ms! → 原因:Redis 连接池配置过小(maxActive=8),高峰期等待连接超时

分析链路:

1. checkStock() 耗时 1850ms — 是主要瓶颈

2. validateCoupon() 耗时 890ms — 远程调用慢

3. 追踪 checkStock 内部:getStockFromCache 就占了 1700ms

4. 根因:Redis 连接池太小,高峰期获取连接排队等 1700ms

💡 原理讲解 — 连接池耗尽为什么这么慢?

连接池耗尽原理:Redis 连接池 maxActive=8,高峰期 50 个并发请求同时竞争连接。每个请求必须等前面的释放连接才能拿到,排队等待时间 1700ms。

技术原理:Redis 连接池(Lettuce/Jedis)配置过小时,当并发请求超过连接数,后续线程会阻塞等待连接释放。如果连接池 maxActive=8,而 QPS=320,平均每个请求占用连接 5ms,那理论最大吞吐量 = 8 / 0.005 = 1600 QPS。但如果个别慢请求(大 key)占着连接不放,实际吞吐量远低于理论值。

🛠️ 修复方案

❌ 修复前
// application.yml — Redis 连接池过小 spring: redis: lettuce: pool: max-active: 8 max-idle: 4 max-wait: 3000ms min-idle: 2 // 库存查询没有本地缓存 public Integer getStock(Long skuId) { return redisTemplate.opsForValue() .get("stock:" + skuId); }
✅ 修复后
// application.yml — 扩大连接池 spring: redis: lettuce: pool: max-active: 64 max-idle: 32 max-wait: 500ms min-idle: 16 // 增加 Caffeine 本地缓存(1秒过期) @Cacheable(value = "stock", cacheManager = "localCacheManager") public Integer getStock(Long skuId) { return redisTemplate.opsForValue() .get("stock:" + skuId); }
✅ 验证结果
指标修复前修复后提升
P99 RT3.2s180ms↓ 94%
P95 RT2.1s120ms↓ 94%
checkStock 耗时1850ms35ms↓ 98%
Redis 连接池利用率100%(满)45%↓ 55%
下单成功率78%99.5%↑ 27%
P3
数据库慢查询 — 高峰期SQL执行时间暴增
严重程度:高 发生频率:高峰期必现 排查难度:中 修复成本:低
电商小程序「商品列表」页面在午高峰(11:30-13:00)加载极慢,MySQL 慢查询日志显示 SELECT * FROM product WHERE category_id = ? AND status = 1 ORDER BY sales_count DESC 单次执行 2.8s。

🔍 发现过程 — MySQL 慢查询日志

MySQL Slow Query Log
# Time: 2026-06-10T12:15:32.456789Z # User@Host: milktea_app[www] @ db-master [10.0.1.50] # Query_time: 2.834512 Lock_time: 0.000123 Rows_sent: 50 Rows_examined: 1,580,000 SELECT * FROM milk_tea_product WHERE category_id = 5 AND status = 1 ORDER BY sales_count DESC LIMIT 50 OFFSET 0; ⚠️ 扫描了 158 万行,只返回 50 行!典型的全表扫描

🔧 排查步骤 — EXPLAIN 分析执行计划

DataGrip — EXPLAIN 分析
🗄️ milktea_prod — milk_tea_product
EXPLAIN SELECT * FROM milk_tea_product WHERE category_id = 5 AND status = 1 ORDER BY sales_count DESC LIMIT 50;
📋 Execution Plan (1 row)
idselect_typetabletypepossible_keyskeyrowsExtra
1 SIMPLE milk_tea_product ALL idx_category NULL 1,580,000 Using where; Using filesort
EXPLAIN 结果分析:
type: ALL — 全表扫描,没有走索引
key: NULL — MySQL 优化器选择了不使用索引
rows: 1,580,000 — 扫描了全表 158 万行
Using filesort — 额外的文件排序,效率极低
原因:虽然有 idx_category 索引,但 ORDER BY 用了 sales_count,索引无法覆盖排序,MySQL 选择全表扫描 + filesort
💡 原理讲解 — 为什么索引没生效?

filesort 原理:索引 idx_category_id 只能快速定位 category_id 匹配的行,但 ORDER BY sales_count 排序无法利用该索引。MySQL 只能把所有匹配行取出到内存(或临时文件)中,再做一次排序 —— 这就是 filesort。

技术原理:MySQL 索引是 B+ 树结构。查询条件用 category_id,排序用 sales_count,这两个字段不在同一个索引上,MySQL 无法利用索引的有序性完成排序,只能做 filesort。解决方案是创建联合索引 (category_id, status, sales_count),让索引同时覆盖 WHERE 过滤和 ORDER BY 排序。

🛠️ 修复方案

❌ 修复前
-- 只有单列索引,无法覆盖排序 CREATE INDEX idx_category ON milk_tea_product(category_id); -- 查询扫描 158 万行 + filesort SELECT * FROM milk_tea_product WHERE category_id = 5 AND status = 1 ORDER BY sales_count DESC LIMIT 50;
✅ 修复后
-- 创建联合索引覆盖 WHERE + ORDER BY CREATE INDEX idx_cat_status_sales ON milk_tea_product( category_id, status, sales_count DESC ); -- 走联合索引,扫描行数大幅减少 SELECT id, name, price, cover_img FROM milk_tea_product WHERE category_id = 5 AND status = 1 ORDER BY sales_count DESC LIMIT 50;
✅ 验证结果

EXPLAIN 修复后:

type: range — 走了索引范围扫描
key: idx_cat_status_sales — 使用了新索引
rows: 320 — 只扫描 320 行(原来 158 万行)
Extra: Backward index scan — 利用索引逆序,无需 filesort

指标修复前修复后提升
SQL 执行时间2.8s12ms↓ 99.6%
扫描行数1,580,000320↓ 99.98%
filesort消除
P4
Redis命中率低 — 大量请求绕过缓存打DB
严重程度:高 发生频率:持续性 排查难度:中 修复成本:低
电商小程序商品详情页,Redis 缓存命中率只有 23%。大量请求穿透缓存直接打到 MySQL,数据库 CPU 飙到 85%。午高峰期间 DB 连接池偶尔耗尽,导致部分请求 500。

🔍 发现过程 — Grafana Redis 监控面板

📊 Grafana — Redis Monitoring Dashboard
Keyspace Hit Rate: 23%
Commands/s: 8,500
Memory Used: 1.2GB / 4GB
Evicted Keys: 142,000
🚨 告警:Redis keyspace hit_rate < 50% 已持续 2 小时
关联告警:MySQL CPU 使用率 > 85%,活跃连接数 > 150(max=200)

🔧 排查步骤 — Redis CLI 分析

Terminal — Redis 排查
$ redis-cli info stats | grep keyspace keyspace_hits:12834 keyspace_misses:43021 命中率 = 12834 / (12834 + 43021) = 22.9% $ redis-cli info memory | grep used_memory_human used_memory_human:1.2G $ redis-cli --scan --pattern "product:*" | head -20 product:detail:1001 product:detail:1002 ...(只有 200 个 key) $ redis-cli config get maxmemory-policy 1) "maxmemory-policy" 2) "volatile-lru" ⚠️ 发现问题: 1. 商品 key 只有 200 个,但有 10000+ 商品 → 大量未缓存 2. 使用 volatile-lru 但大部分 key 没设 TTL → 永不过期占内存 3. 缓存代码没有"回写"机制 → 缓存未命中时不写入缓存

分析结果:

1. 只缓存了 200/10000 的商品 — 缓存覆盖率只有 2%

2. 缓存代码逻辑:只有缓存命中时返回,未命中时直接查 DB 但不回写

3. 部分缓存 key 没设过期时间,旧数据永远占着内存

💡 原理讲解 — 缓存穿透 vs 缓存击穿 vs 缓存雪崩

缓存三大问题原理:

缓存穿透:查询不存在的数据(如传入不存在的商品ID),缓存中没有,每次都打到 DB。防护:布隆过滤器或缓存空值。

缓存击穿:热点 key 过期的瞬间,大量并发请求同时打到 DB。防护:互斥锁或永不过期+异步刷新。

缓存雪崩:大量 key 同时过期(如启动时批量设了相同 TTL),DB 瞬间承压。防护:TTL 加随机值,错开过期时间。

本次问题属于:缓存回写缺失 + 覆盖率低。大量请求根本没走缓存。

🛠️ 修复方案

❌ 修复前
// 缓存未命中时不回写 public Product getProduct(Long id) { Product p = redisTemplate.opsForValue() .get("product:" + id); if (p != null) return p; // 未命中直接查DB,不回写缓存! return productDao.selectById(id); }
✅ 修复后
// 缓存未命中时查DB + 回写 + 防穿透 public Product getProduct(Long id) { String key = "product:" + id; Product p = redisTemplate.opsForValue() .get(key); if (p != null) return p; // 查DB p = productDao.selectById(id); if (p != null) { // 回写缓存,TTL 30分钟 + 随机偏移防雪崩 long ttl = 1800 + RandomUtil.randomInt(300); redisTemplate.opsForValue() .set(key, p, ttl, TimeUnit.SECONDS); } else { // 防穿透:缓存空值,TTL 5分钟 redisTemplate.opsForValue() .set(key, "NULL", 300, SECONDS); } return p; }
✅ 验证结果
指标修复前修复后提升
Redis 缓存命中率23%96%↑ 317%
缓存 key 数量20010,200↑ 5000%
MySQL QPS(读)4,500320↓ 93%
MySQL CPU85%18%↓ 79%
商品详情页 RT450ms25ms↓ 94%
P5
JVM GC频繁 — Young GC每秒一次,STW导致接口卡顿
严重程度:高 发生频率:持续性 排查难度:高 修复成本:中
电商订单服务 JVM 频繁 GC,每秒触发一次 Young GC,每次 STW(Stop-The-World)暂停 30-50ms。接口 P99 延迟出现锯齿状毛刺,用户偶尔感受到"卡一下"。

🔍 发现过程 — GC 日志分析

GC Log — Young GC 频率分析
# JVM 启动参数 java -Xms512m -Xmx512m -XX:+UseG1GC -Xloggc:/var/log/gc.log -jar milktea-order.jar # GC 日志(截取部分) [2026-06-10T12:00:01.123] GC pause (G1 Evacuation Pause) (young) [Eden: 256.0M(256.0M)->0.0B(230.0M) Survivors: 0.0B->26.0M Heap: 350.0M(512.0M)->280.0M(512.0M)] [Times: user=0.08 sys=0.01, real=0.04 secs] [2026-06-10T12:00:01.583] GC pause (G1 Evacuation Pause) (young) ← 460ms后又一次GC! [2026-06-10T12:00:02.090] GC pause (G1 Evacuation Pause) (young) ← 507ms后又一次GC! [2026-06-10T12:00:02.543] GC pause (G1 Evacuation Pause) (young) ⚠️ 平均每 500ms 一次 Young GC,每次 STW 40ms 问题:堆内存太小(512M),Eden 区只有 256M,频繁填满触发 GC

分析:

1. 堆内存只有 512M,高峰期不够用

2. Eden 区 256M,每秒新对象约 500MB → 每 0.5s 填满一次 Eden

3. 每次 Young GC 暂停 40ms → 接口 P99 有 40ms 毛刺

4. 对象晋升到 Old 区过快 → 即将触发 Full GC

💡 原理讲解 — 为什么 GC 频繁导致卡顿?

房间整理类比:你的桌面(Eden 区)很小,随手放东西(新对象创建),桌面满了就要快速扫一眼:有用的收进抽屉(Survivor 区),没用的扔垃圾桶(Young GC),整个过程 1 秒搞定。抽屉里很久没清理的东西最终搬进储物间(Old Gen)。但储物间也满了,你要彻底翻箱倒柜整理整个房间(Full GC),要 10 分钟,整理期间谁也不能用这个房间(STW)。桌面越小,清理越频繁,别人等得越久。

技术原理:JVM 堆分为 Eden + Survivor + Old。新对象分配在 Eden,Eden 满了触发 Young GC(STW)。如果堆太小或分配速率太高,GC 频率就高。G1 GC 的 STW 时间与存活对象数量成正比,频繁 GC 意味着频繁暂停业务线程。

🛠️ 修复方案

❌ 修复前
# 堆内存只有 512M -Xms512m -Xmx512m -XX:+UseG1GC # 没有设置 G1 Region 大小 # 没有设置最大 GC 暂停目标 Young GC 频率:每 500ms 一次 STW 时间:40ms/次 Full GC 频率:每 30 分钟一次
✅ 修复后
# 扩大堆内存 + G1 调优 -Xms2g -Xmx2g -XX:+UseG1GC # 目标最大暂停时间 50ms -XX:MaxGCPauseMillis=50 # G1 Region 大小 4MB -XX:G1HeapRegionSize=4m # GC 线程数(4核 CPU) -XX:ParallelGCThreads=4 # 大对象阈值 1MB -XX:G1HeapRegionSize=4m Young GC 频率:每 8-10s 一次 STW 时间:15ms/次 Full GC:基本消除
✅ 验证结果
指标修复前修复后提升
Young GC 频率2次/s0.1次/s↓ 95%
GC STW 时间40ms/次15ms/次↓ 63%
Full GC 频率2次/h0次(连续72h无FGC)消除
接口 P99 RT850ms120ms↓ 86%
RT 毛刺频繁(每秒)基本消除消除
P6
线程池满 — Tomcat线程池耗尽,新请求被拒绝
严重程度:严重 发生频率:高峰期 排查难度:中 修复成本:低
电商小程序在午高峰突然出现大量 503 错误,用户无法下单。Nginx 日志显示后端返回 503,Tomcat 日志报 java.util.concurrent.RejectedExecutionException

🔍 发现过程 — Nginx 503 + Tomcat 日志

Nginx Error Log → Tomcat 日志
# Nginx access.log — 大量 503 10.0.1.5 - - [10/Jun/2026:12:15:32] "POST /api/order/create" 503 0.001 10.0.1.6 - - [10/Jun/2026:12:15:32] "POST /api/order/create" 503 0.001 10.0.1.7 - - [10/Jun/2026:12:15:32] "GET /api/product/list" 503 0.001 # Tomcat catalina.out java.util.concurrent.RejectedExecutionException: Task java.util.concurrent.FutureTask@5a3b7c rejected from java.util.concurrent.ThreadPoolExecutor@8f2d1e [Running, pool size = 200, active threads = 200, queued tasks = 100, completed tasks = 45230] ⚠️ 线程池 200 个线程全忙,队列 100 满了,新请求被拒绝!

🔧 排查步骤 — jstack 线程 Dump

jstack — Thread Dump 分析
$ jstack $(pgrep -f milktea-order) | grep -A 3 "http-nio" "http-nio-8080-exec-1" #120 daemon prio=5 os_prio=0 java.lang.Thread.State: WAITING (on object monitor) at java.lang.Object.wait(Native Method) at com.milktea.service.PayService.callPayment(PayService.java:45) - waiting to pay-gateway response... timeout=30s # 统计:200 个 Tomcat 线程中有 156 个在等待支付网关响应! $ jstack $(pgrep -f milktea-order) | grep "http-nio" | wc -l 200 $ jstack $(pgrep -f milktea-order) | grep "PayService" | wc -l 156 ⚠️ 根因:支付网关响应慢(30s 超时),156 个线程被卡住!
💡 原理讲解 — 线程池为什么被耗尽?

线程池耗尽原理:Tomcat 线程池 maxThreads=200,其中 156 个线程被阻塞在等待支付网关响应(超时 30s)。真正能处理新请求的线程只剩 44 个,新的请求进来就排队溢出,触发拒绝策略返回 503。

技术原理:Tomcat 用线程池处理 HTTP 请求。当所有线程都被阻塞在慢操作(同步调用外部支付网关)上,新请求进不来。解决方案不是简单增大线程池(线程也是资源),而是把同步阻塞调用改成异步非阻塞,让线程不被长时间占用。

🛠️ 修复方案

❌ 修复前
// 同步调用支付网关,30s 超时 public PayResult createOrder(Order order) { // 同步阻塞,线程被卡住 30s PayResult result = payService .callPayment(order, 30s); return orderDao.insert(order); } // server.xml — 默认线程池 <Connector port="8080" maxThreads="200" />
✅ 修复后
// 异步调用 + 缩短超时 + 熔断 @HystrixCommand( fallbackMethod = "createOrderFallback", commandProperties = { @HystrixProperty( name="execution.isolation.thread .timeoutInMilliseconds", value="3000"), @HystrixProperty( name="circuitBreaker.requestVolume .threshold", value="20") } ) public CompletableFuture<Order> createOrderAsync(Order order) { return CompletableFuture.supplyAsync(() -> payService.callPayment(order, 3s), separatePayThreadPool); }
✅ 验证结果
指标修复前修复后提升
503 错误率12%0.01%↓ 99.9%
Tomcat 活跃线程200/200(满)45/200↓ 78%
支付调用线程占用156 线程×30s独立线程池×3s隔离
高峰期吞吐量80 QPS520 QPS↑ 550%
P7
前端渲染卡顿 — 大列表(10000+条)渲染导致页面卡死
严重程度:高 发生频率:操作必现 排查难度:低 修复成本:低
电商管理后台「订单列表」页面,查询一个月内的全部订单有 12,000 条,直接渲染到 DOM。页面打开后滚动严重卡顿,Chrome DevTools Performance 面板显示单帧渲染耗时 800ms+(正常应 < 16ms)。

🔍 发现过程 — Chrome DevTools Performance

Chrome DevTools — Performance 面板
Rendering Stats (滚动期间)
Frames: 1.2 FPS(目标 60 FPS)
Frame Duration: 800ms(目标 < 16ms)
Scripting: 450ms | Rendering: 280ms | Painting: 70ms
DOM 节点统计
DOM Nodes: 84,000
Document: 1 | Nodes: 84,000
Layout Trees: 42 | Compositing Reason: 太多

🔧 排查步骤 — Elements 面板 + 代码审查

问题代码:一次性将 12,000 条订单全部渲染为 DOM 节点。每个订单行有 ~7 个 DOM 节点(tr、td×5、span),共 84,000 个 DOM 节点。浏览器需要进行大量的 Layout + Paint 计算,滚动时每帧都要重新计算。
// ❌ 问题代码:直接渲染全部数据 <table> <tr v-for="order in orders"> <!-- 12000 条全渲染 --> <td>{{ order.id }}</td> <td>{{ order.productName }}</td> <td>{{ order.amount }}</td> <td>{{ order.status }}</td> <td>{{ order.createdAt }}</td> </tr> </table>
💡 原理讲解 — 为什么大列表会卡?

DOM 节点过多导致卡顿原理:12,000 条订单 = 12,000 个 DOM 节点。每次滚动时浏览器要重新计算这些节点的位置(Layout Reflow)。但用户屏幕实际只能显示 ~20 条,其余 11,980 个节点完全不可见却在拖累渲染性能。

虚拟列表原理:只渲染可视区域内的 DOM 节点(~20 个),加上上下各 5 个缓冲节点,总共只有 30 个 DOM 节点。滚动时动态替换可视区域内容,DOM 总数恒定,性能恒定。

🛠️ 修复方案

// ✅ 使用虚拟列表(vue-virtual-scroller) <RecycleScroller :items="orders" :item-size=48 key-field="id" v-slot="{ item }"> <OrderRow :order="item" /> </RecycleScroller> // ✅ 或者分页加载 + Intersection Observer <div v-for="order in visibleOrders"> <OrderRow :order="order" /> </div> <div ref="sentinel"></div> <!-- 触发加载的哨兵元素 -->
✅ 验证结果
指标修复前修复后提升
DOM 节点数84,00030↓ 99.96%
帧率(FPS)1.2 FPS60 FPS↑ 4900%
首次渲染时间4.5s120ms↓ 97%
内存占用580MB65MB↓ 89%
滚动流畅度严重卡顿丝滑
P8
CDN缓存未命中 — 静态资源每次都回源
严重程度:中 发生频率:持续性 排查难度:低 修复成本:低
电商小程序 H5 页面加载慢,CDN 命中率只有 15%。每个用户请求都回源到 Nginx 服务器,导致源站带宽被打满(1Gbps 跑到 850Mbps),静态资源加载时间 800ms+。

🔍 发现过程 — CDN 控制台 + cURL 分析

Terminal — CDN 缓存分析
$ curl -I https://cdn.milktea.com/static/js/app.abc123.js HTTP/2 200 cache-control: no-cache expires: Thu, 01 Jan 1970 00:00:00 GMT x-cache: MISS content-type: application/javascript $ curl -I https://cdn.milktea.com/static/img/logo.png HTTP/2 200 cache-control: no-cache x-cache: MISS ⚠️ 所有静态资源都返回 no-cache + X-Cache: MISS! 每次请求都回源,CDN 完全没缓存
💡 原理讲解 — CDN 缓存为什么没生效?

CDN 回源率高的原理:CDN 边缘节点本应缓存静态资源,用户就近获取。但如果源站返回的响应头设置了 Cache-Control: no-cache 或 TTL 太短,边缘节点不敢缓存,每次请求都要回源到 Nginx,CDN 等于白部署了。

技术原理:CDN 节点根据源站返回的 Cache-Control 头决定是否缓存。no-cache 表示每次都要验证,no-store 表示完全不缓存。静态资源(JS/CSS/图片)应该设置长期缓存 + 文件名 hash 实现缓存更新。

🛠️ 修复方案

# ✅ Nginx 配置 — 静态资源设置长期缓存 location /static/ { alias /var/www/static/; expires 365d; add_header Cache-Control "public, max-age=31536000, immutable"; add_header X-Cache-Config "cdn-static"; } # HTML 文件不缓存(引导加载最新 hash 的 JS/CSS) location / { try_files $uri /index.html; add_header Cache-Control "no-cache"; } # Vite 配置文件名 hash(默认开启) // vite.config.ts export default defineConfig({ build: { rollupOptions: { output: { entryFileNames: 'assets/[name].[hash].js', chunkFileNames: 'assets/[name].[hash].js', assetFileNames: 'assets/[name].[hash][extname]' } } } })
✅ 验证结果
指标修复前修复后提升
CDN 命中率15%98%↑ 553%
静态资源加载时间800ms35ms↓ 96%
源站带宽利用率85%8%↓ 91%
源站 QPS5,000300↓ 94%
P9
大文件上传慢 — 100MB文件上传超时
严重程度:中 发生频率:上传时必现 排查难度:低 修复成本:中
电商运营后台上传产品宣传视频(100MB MP4),上传到 80% 时超时失败。Nginx 配置 client_max_body_size 10m,请求体超过限制直接被 Nginx 拒绝。即使改大限制,单次上传 100MB 在弱网环境下也很容易超时。

🔍 发现过程 — Nginx 错误日志

Nginx Error Log
2026/06/10 14:32:15 [error] 1234#0: *5678 client intended to send too large body: 104857600 bytes, client: 10.0.1.5, server: admin.milktea.com, request: "POST /api/upload/video HTTP/1.1", host: "admin.milktea.com", referrer: "https://..." ⚠️ 100MB > Nginx client_max_body_size (10MB) → 413 Request Entity Too Large

🔧 排查步骤 — 分析上传链路瓶颈

浏览器
100MB 文件
Nginx
限制 10MB
❌ 413
Spring Boot
超时 30s
❌ Timeout
OSS
存储服务

瓶颈分析:

1. Nginx client_max_body_size 10m → 100MB 直接被拒绝

2. 即使放大限制,单次上传 100MB 在 4G 网络下要 25s+,容易超时

3. 上传失败后从头重传,没有断点续传

4. 没有上传进度反馈,用户体验差

💡 原理讲解 — 分片上传为什么更可靠?

分片上传原理:整文件上传 100MB,任何一个分片失败就要从头来。分片上传把 100MB 拆成 20 个 5MB 分片,每个分片独立上传,失败只需重传那一片。还可以多分片并发上传,大幅缩短总时间。

技术原理:大文件分片上传将文件切成固定大小的块(如 5MB/片),每片独立上传,失败只需重传该分片。所有分片上传完成后,服务端合并文件。还可以用文件内容 hash 做秒传(文件已存在则跳过上传)。

🛠️ 修复方案 — 分片上传

❌ 修复前
// 整文件上传,100MB 一次性发送 const formData = new FormData(); formData.append('file', file); // 100MB axios.post('/api/upload', formData); # Nginx 只允许 10MB client_max_body_size 10m;
✅ 修复后
// 分片上传(每片 5MB) const CHUNK_SIZE = 5 * 1024 * 1024; const chunks = Math.ceil(file.size / CHUNK_SIZE); for (let i = 0; i < chunks; i++) { const chunk = file.slice( i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE); await uploadChunk(chunk, i, fileHash); } # Nginx 允许分片大小即可 client_max_body_size 10m;
✅ 验证结果
指标修复前修复后提升
上传成功率30%(常超时)99.9%↑ 233%
100MB 上传时间超时失败25s(并发分片)可用
失败恢复从头重传只重传失败分片
进度展示精确到分片级
秒传支持hash 匹配秒传
P10
WebSocket连接断开 — 消息推送服务频繁断连重连
严重程度:高 发生频率:频繁 排查难度:中 修复成本:中
电商小程序「订单实时推送」功能,配送员端 WebSocket 平均每 3 分钟断连一次。断连期间配送员收不到新订单通知,导致履约延迟。客户端日志显示大量 WebSocket connection to 'wss://...' failed: 错误。

🔍 发现过程 — 客户端日志 + 服务端日志

浏览器 Console — WebSocket 日志
[12:00:01] WebSocket connected to wss://push.milktea.com/ws [12:00:05] Received: {"type":"new_order","orderId":"ORD20260001"} [12:00:32] Received: {"type":"order_update","status":"making"} [12:03:15] WebSocket connection to 'wss://push.milktea.com/ws' failed: Error during WebSocket handshake: net::ERR_CONNECTION_TIMED_OUT [12:03:16] Attempting reconnect... (1/5) [12:03:21] Attempting reconnect... (2/5) [12:03:26] WebSocket reconnected [12:03:28] Received: {"type":"new_order","orderId":"ORD20260002"} [12:06:30] WebSocket connection failed again! ⚠️ 平均每 3 分钟断连一次,断连期间消息丢失

🔧 排查步骤 — Nginx 配置 + 网络分析

Nginx Config — WebSocket 相关配置
upstream websocket { server 10.0.1.10:8080; } server { location /ws { proxy_pass http://websocket; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; # ❌ 缺少以下关键配置: # proxy_read_timeout ← 默认60s,超时断连! # proxy_send_timeout # 没有心跳保活机制 } } 根因:Nginx proxy_read_timeout 默认 60s, 如果 60s 内没有数据传输,Nginx 主动断开连接。 加上没有心跳,3分钟空闲后连接必断。
💡 原理讲解 — WebSocket 为什么会断?

WebSocket 断连原理:WebSocket 是长连接,但如果 60 秒内没有数据传输,中间的代理/负载均衡器(Nginx)会认为连接已经空闲,主动断开。解法:客户端定时发心跳包(ping),保持连接活跃。

技术原理:Nginx 作为反向代理转发 WebSocket 连接时,proxy_read_timeout 默认 60s。如果这段时间内没有数据从上游服务器读取到,Nginx 会关闭连接。解决方案:① 增大超时时间 ② 客户端/服务端定期发送心跳 ping/pong 保活 ③ 客户端实现指数退避重连。

🛠️ 修复方案

# ✅ Nginx 配置 — WebSocket 长连接优化 location /ws { proxy_pass http://websocket; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; # 读取超时设为 1 小时 proxy_read_timeout 3600s; proxy_send_timeout 3600s; }
// ✅ 客户端心跳 + 指数退避重连 class WSClient { connect() { this.ws = new WebSocket('wss://push.milktea.com/ws'); // 每 30s 发一次心跳 this.heartbeat = setInterval(() => { this.ws.send(JSON.stringify({type:'ping'})); }, 30000); } reconnect() { // 指数退避:1s, 2s, 4s, 8s, 16s const delay = Math.min(1000 * Math.pow(2, retryCount), 16000); setTimeout(() => this.connect(), delay); } }
✅ 验证结果
指标修复前修复后提升
平均连接时长3 分钟12+ 小时↑ 24000%
断连频率20次/小时0.1次/小时↓ 99.5%
消息到达率85%99.9%↑ 17%
重连延迟5-30s1-2s(退避重连)↓ 80%
P11
N+1查询问题 — JPA/MyBatis循环查关联数据
严重程度:高 发生频率:每次请求必现 排查难度:低 修复成本:低
电商小程序「我的订单」页面,查询 20 条订单需要执行 21 条 SQL(1 查订单 + 20 查商品详情)。接口 RT 800ms,MySQL 慢查询日志里大量 SELECT * FROM product WHERE id = ? 的单条查询。

🔍 发现过程 — MySQL 慢查询 + SQL 审计

MySQL General Log — SQL 审计
2026-06-10 14:20:01 Query: SELECT * FROM orders WHERE user_id = 10086 ORDER BY created_at DESC LIMIT 20 2026-06-10 14:20:01 Query: SELECT * FROM product WHERE id = 2001 2026-06-10 14:20:01 Query: SELECT * FROM product WHERE id = 2002 2026-06-10 14:20:01 Query: SELECT * FROM product WHERE id = 2003 2026-06-10 14:20:02 Query: SELECT * FROM product WHERE id = 2004 ... (重复 20 次) ⚠️ 1 + 20 = 21 条 SQL!经典的 N+1 问题 每条 SQL 虽然只要 30ms,但 21 条串行执行 = 630ms+

🔧 排查步骤 — 代码审查

// ❌ 问题代码:循环中查关联数据 public List<OrderVO> getMyOrders(Long userId) { // 第 1 条 SQL:查 20 条订单 List<Order> orders = orderMapper.selectByUserId(userId); List<OrderVO> result = new ArrayList<>(); for (Order order : orders) { // 每个订单查一次商品 → N 条 SQL! Product p = productMapper.selectById(order.getProductId()); result.add(OrderVO.build(order, p)); } return result; }
💡 原理讲解 — 什么是 N+1 查询?

N+1 查询原理:先查 20 条订单(1 次 SQL),然后循环中每条订单各查一次商品详情(20 次 SQL),总共 21 次。改为批量查询:收集所有 product_id,一条 SELECT * FROM product WHERE id IN (...) 查出全部,再在内存中关联,只需 2 次 SQL。

技术原理:ORM 框架(JPA/MyBatis)默认懒加载关联对象,访问关联属性时触发额外 SQL。在循环中访问 N 条记录的关联对象,就产生 N 条额外 SQL。修复方式:JOIN 查询、批量查询、MyBatis 的 association/collection 嵌套映射、JPA 的 @EntityGraph

🛠️ 修复方案

// ✅ 修复:先批量查商品,再内存关联 public List<OrderVO> getMyOrders(Long userId) { // 第 1 条 SQL:查 20 条订单 List<Order> orders = orderMapper.selectByUserId(userId); // 收集所有 productId Set<Long> productIds = orders.stream() .map(Order::getProductId) .collect(Collectors.toSet()); // 第 2 条 SQL:批量查所有商品(1条SQL代替20条) Map<Long, Product> productMap = productMapper .selectBatchIds(productIds).stream() .collect(Collectors.toMap(Product::getId, Function.identity())); return orders.stream() .map(o -> OrderVO.build(o, productMap.get(o.getProductId()))) .collect(Collectors.toList()); }
-- ✅ 或者直接 JOIN 查询(更高效) SELECT o.id, o.order_no, o.amount, o.status, p.id AS product_id, p.name, p.price, p.cover_img FROM orders o INNER JOIN product p ON o.product_id = p.id WHERE o.user_id = #{userId} ORDER BY o.created_at DESC LIMIT 20;
✅ 验证结果
指标修复前修复后提升
SQL 执行次数21 条2 条(或 1 条 JOIN)↓ 90-95%
接口 RT800ms45ms↓ 94%
DB 连接占用时间630ms15ms↓ 98%
网络往返次数21 次1-2 次↓ 90-95%
P12
网络延迟抖动 — 跨机房调用RT不稳定
严重程度:中 发生频率:间歇性 排查难度:高 修复成本:中
电商小程序的支付服务部署在北京机房,但订单服务在上海机房。跨机房调用支付接口 RT 极不稳定:正常 50ms,偶尔飙到 500ms 甚至 2s。高峰期出现大量支付超时,用户投诉"支付转圈很久"。

🔍 发现过程 — APM 链路追踪

SkyWalking — 链路追踪详情
Trace ID: trace-abc-123-def
Span 明细:
服务操作耗时状态
order-servicecreateOrder()12ms
order→pay (跨机房)callPayment()1,850ms⚠ 不稳定
pay-serviceprocess()35ms
跨机房网络 RT:min=48ms, avg=120ms, max=1850ms, P99=680ms
抖动范围极大!目标 RT 稳定性 < 100ms

🔧 排查步骤 — ping + traceroute + mtr

Terminal — 网络诊断
$ mtr --report 10.0.2.100 # 上海→北京 HOST: order-sh-01 Loss% Snt Last Avg Best Wrst 1. 10.0.1.1 0.0% 50 1ms 1ms 0ms 2ms 2. sh-core-router 0.0% 50 5ms 4ms 3ms 8ms 3. bj-backbone-link 2.0% 50 85ms 45ms 20ms 320ms 4. bj-datacenter-gw 3.0% 50 150ms 52ms 22ms 850ms 5. 10.0.2.100 (目标) 3.0% 50 180ms 55ms 25ms 1850ms ⚠️ 第3-4跳丢包 2-3%,延迟波动 25ms~1850ms 跨机房公网链路质量不稳定!
💡 原理讲解 — 跨机房网络为什么抖动?

跨机房网络抖动原理:上海和北京机房之间走的是公网链路,经过多个路由跳转。公网延迟受拥塞、丢包影响,不像同机房内网稳定(<1ms)。高峰期公网带宽紧张时,延迟从 25ms 飙到 1850ms,丢包率 3%。解法:专线/VPN + 超时降级 + 本地缓存。

技术原理:跨机房走公网/VPN,网络质量受运营商链路影响。高延迟+丢包导致 TCP 重传,RT 进一步恶化。解决方案:① 专线/高速通道(稳定低延迟)② 同机房部署 ③ 重试+超时+降级策略 ④ 异步化调用。

🛠️ 修复方案

❌ 修复前
// 同步调用,超时30s,无重试 PayResult result = payClient .callPayment(order); // 30s超时 // 部署架构: 上海机房: order-service → 公网 → 北京机房: pay-service RT: 25ms~1850ms, 丢包3%
✅ 修复后
// 超时3s + 重试2次 + 降级 @Retryable(maxAttempts = 3, backoff = @Backoff(delay=200,multiplier=2)) public PayResult pay(PayReq req) { return payClient.call(req, 3s); } // 部署优化: 上海机房: order-service + pay-service (同机房调用 RT < 2ms)
✅ 验证结果
指标修复前修复后提升
支付调用 P99 RT680ms45ms↓ 93%
RT 波动范围25ms~1850ms8ms~65ms稳定
丢包率3%0%消除
支付超时率5%0.01%↓ 99.8%
P13
磁盘IO瓶颈 — 日志写入太频繁导致IO wait高
严重程度:中高 发生频率:持续性 排查难度:中 修复成本:低
电商订单服务器 iowait 长期 25%+,高峰期达到 45%。日志文件每天写入 50GB,每个请求打印 10+ 行日志(包括完整请求体和响应体)。磁盘 IO 成为系统瓶颈,影响所有依赖磁盘的操作。

🔍 发现过程 — top + iostat 监控

Terminal — 系统监控
$ top %Cpu(s): 35.2 us, 12.1 sy, 0.0 ni, 25.8 wa, 0.0 hi, 0.2 si ↑ iowait 25.8%!非常高 $ iostat -x 1 3 Device rrqm/s wrqm/s r/s w/s rMB/s wMB/s await vda 0.0 45.2 5.0 850.0 0.1 48.5 32.5 ⚠️ 写入 850次/s,48.5MB/s,平均等待 32.5ms $ du -sh /var/log/milktea/ 52GB /var/log/milktea/ $ ls -lh /var/log/milktea/ | head -5 -rw-r--r-- 1 root root 4.2G Jun 10 14:30 milktea-order.log -rw-r--r-- 1 root root 3.8G Jun 10 14:30 milktea-order.log.1 -rw-r--r-- 1 root root 4.5G Jun 10 14:30 access.log

🔧 排查步骤 — 分析日志写入模式

// ❌ 问题代码:每个请求打印大量日志 log.info("收到订单请求: {}", JSON.toJSONString(request)); // 完整请求体 log.info("校验参数: {}", JSON.toJSONString(params)); log.info("查询商品: skuId={}, result={}", skuId, JSON.toJSONString(product)); log.info("查询用户: uid={}, result={}", uid, JSON.toJSONString(user)); log.info("计算价格: {}", JSON.toJSONString(priceDetail)); log.info("创建订单: {}", JSON.toJSONString(order)); log.info("发送消息: {}", JSON.toJSONString(message)); log.info("订单响应: {}", JSON.toJSONString(response)); // 完整响应体 // 每个请求 10+ 行 INFO 日志,高峰期 300 QPS = 3000+ 行/s
💡 原理讲解 — 为什么日志写入会导致 IO 瓶颈?

日志 IO 瓶颈原理:每个请求打印 10+ 行完整请求体/响应体日志,高峰期 300 QPS × 10 行 = 每秒 3000 行写入。磁盘写入速度跟不上,iowait 飙到 45%,所有依赖磁盘的操作(包括数据库写入)都被拖慢。解法:异步日志(AsyncAppender)+ 采样 + 分级。

技术原理:日志框架(Logback/Log4j2)默认同步写入磁盘。高频写入产生大量小 IO 操作,机械磁盘(或云盘)随机写入性能有限。同步日志意味着业务线程要等 IO 完成,iowait 高 = CPU 在等磁盘 = 整体吞吐量下降。

🛠️ 修复方案

❌ 修复前
# logback.xml — 同步写入 + 无滚动策略 <appender name="FILE" class="ch.qos.logback.core.FileAppender"> <file>milktea.log</file> <!-- 无 AsyncAppender --> <!-- 无 SizeAndTimeBasedRollingPolicy --> </appender> // 日志级别:全部 INFO <root level="INFO"> iowait: 25.8% 磁盘写入: 850次/s, 48.5MB/s 日志量: 50GB/天
✅ 修复后
# logback.xml — 异步写入 + 滚动策略 <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender"> <queueSize>2048</queueSize> <discardingThreshold>0</discardingThreshold> <appender-ref ref="ROLL_FILE" /> </appender> # 业务代码改为 DEBUG 级别 <logger name="com.milktea" level="WARN" /> iowait: 3.2% 磁盘写入: 120次/s, 2.1MB/s 日志量: 3GB/天
✅ 验证结果
指标修复前修复后提升
CPU iowait25.8%3.2%↓ 88%
磁盘写入速率48.5MB/s2.1MB/s↓ 96%
日志量/天50GB3GB↓ 94%
接口 P99 RT350ms85ms↓ 76%
磁盘利用率92%35%↓ 62%
P14
内存泄漏(浏览器) — SPA长时间运行后页面越来越慢
严重程度:中高 发生频率:长时间使用后 排查难度:高 修复成本:低
电商管理后台(SPA 应用),工作人员每天要开 8-10 小时。使用 2-3 小时后页面越来越慢,最终浏览器标签页占用 2.5GB 内存,滚动卡顿,点击延迟 3-5s。必须刷新页面才能恢复。Chrome 任务管理器显示该标签页内存持续增长。

🔍 发现过程 — Chrome Task Manager + Memory 面板

Chrome Task Manager — 内存监控
ProcessMemoryCPU趋势
电商管理后台2,540 MB45%↑ 持续增长
刚打开时180 MB5%正常
内存增长曲线:
启动 180MB → 1小时 450MB → 2小时 980MB → 3小时 2.5GB → 页面卡死
每小时增长约 300-500MB,典型的内存泄漏

🔧 排查步骤 — Chrome DevTools Memory 面板(Heap Snapshot)

Chrome DevTools — Memory Heap Snapshot 对比
Heap Snapshot 对比(Snapshot 1 vs Snapshot 2,间隔 30 分钟)
ConstructorAddedDeletedDeltaRetained Size
WebSocket+1860+186280 MB
Event Listener+12,4000+12,40085 MB
closure+3,200150+3,05042 MB
Object (orderData)+8,5000+8,50065 MB
泄漏点分析:
① WebSocket:创建了 186 个但 0 个被回收 → 没有关闭旧连接
② Event Listener:12,400 个新增 → 没有移除旧的监听器
③ orderData:8,500 个对象 → 订单数据只加不清,列表无限增长

🔧 问题代码定位

// ❌ 泄漏点 1:WebSocket 重复创建,不关闭旧连接 function connectWS() { const ws = new WebSocket(url); // 每次调用都新建,旧的没关闭! ws.onmessage = (e) => { ... }; } setInterval(connectWS, 60000); // 每分钟创建一个新的! // ❌ 泄漏点 2:Event Listener 只添加不移除 mounted() { window.addEventListener('resize', this.onResize); // 加了 } // beforeDestroy() 缺少 removeEventListener! // ❌ 泄漏点 3:订单数据只加不清 data() { return { allOrders: [] } // 无限 push,从不清除 } methods: { fetchOrders() { api.getOrders().then(res => { this.allOrders.push(...res.data); // 永远只加不减! }); } }
💡 原理讲解 — 浏览器内存泄漏的常见原因

前端内存泄漏原理:SPA 不刷新页面,每次路由切换/数据更新都在分配内存。如果组件销毁时没有清理定时器、事件监听器、闭包引用,这些内存永远无法被 GC 回收。长时间运行后内存持续增长,最终页面卡死。排查工具:Chrome DevTools Memory 面板 → Heap Snapshot 对比。

技术原理:JavaScript 使用垃圾回收(GC)自动管理内存。但如果有引用链从根对象(window/global)可达,GC 就不会回收。常见泄漏:① 全局变量/闭包引用 ② 未移除的事件监听器 ③ 未关闭的 WebSocket/setInterval ④ DOM 引用未释放 ⑤ 组件销毁时未清理定时器和监听器。

🛠️ 修复方案

// ✅ 修复 1:WebSocket 单例 + 关闭旧连接 let wsInstance = null; function connectWS() { if (wsInstance) wsInstance.close(); // 先关闭旧的 wsInstance = new WebSocket(url); } // ✅ 修复 2:组件销毁时移除监听器 mounted() { window.addEventListener('resize', this.onResize); }, beforeDestroy() { window.removeEventListener('resize', this.onResize); // ← 加上! clearInterval(this.timer); // 清除定时器 } // ✅ 修复 3:限制订单列表长度 fetchOrders() { api.getOrders().then(res => { this.allOrders = res.data.slice(-500); // 只保留最近500条 }); }
✅ 验证结果
指标修复前修复后提升
内存占用(3小时后)2,540 MB210 MB↓ 92%
内存增长趋势每小时+500MB稳定(±20MB)稳定
WebSocket 实例数186(泄漏)1(单例)修复
Event Listener 数12,400(泄漏)15(正常)修复
页面流畅度(8小时后)卡死需刷新流畅如初