BodySense 性能优化策略
BodySense 项目的性能优化策略:前端懒加载、后端缓存、数据库优化、AI 服务优化、性能监控。
#type / concept
#status / growing
#tech / dev
#tech / ai
[!info] related notes
- 知识地图: BodySense 项目 MOC
- 缓存: Redis 缓存实践
- 数据库: 数据库设计
BodySense 性能优化策略
性能目标
| 指标 | 目标 | 说明 |
|---|---|---|
| 首屏加载 (FCP) | < 1.5s | 首次内容绘制 |
| 最大内容绘制 (LCP) | < 2.5s | 最大内容元素渲染 |
| 首次输入延迟 (FID) | < 100ms | 用户交互响应 |
| 累积布局偏移 (CLS) | < 0.1 | 布局稳定性 |
| API 响应时间 P95 | < 500ms | 后端 API |
| LLM 响应时间 P95 | < 5s | AI 服务 |
1. 前端优化
代码分割
// routes/index.tsx
import { lazy, Suspense } from 'react';
import { Routes, Route } from 'react-router-dom';
// 懒加载页面组件
const DashboardPage = lazy(() => import('@/features/dashboard/pages/DashboardPage'));
const ConsultationPage = lazy(() => import('@/features/consultation/pages/ConsultationPage'));
const ProfilePage = lazy(() => import('@/features/profile/pages/ProfilePage'));
const TrainingPage = lazy(() => import('@/features/training/pages/TrainingPage'));
function Loading() {
return (
<div className="flex items-center justify-center h-screen">
<Spinner className="h-8 w-8" />
</div>
);
}
export function AppRoutes() {
return (
<Suspense fallback={<Loading />}>
<Routes>
<Route path="/dashboard" element={<DashboardPage />} />
<Route path="/consultation/:id" element={<ConsultationPage />} />
<Route path="/profile" element={<ProfilePage />} />
<Route path="/training" element={<TrainingPage />} />
</Routes>
</Suspense>
);
}
组件懒加载
// features/consultation/pages/ConsultationPage.tsx
import { lazy, Suspense } from 'react';
// 重型组件懒加载
const SymptomVisualization = lazy(() =>
import('../components/SymptomVisualization')
);
const TrainingPlanGenerator = lazy(() =>
import('../components/TrainingPlanGenerator')
);
export function ConsultationPage() {
return (
<div className="grid grid-cols-2 gap-4">
<ChatPanel />
<div>
<Suspense fallback={<Skeleton className="h-64" />}>
<SymptomVisualization />
</Suspense>
<Suspense fallback={<Skeleton className="h-48" />}>
<TrainingPlanGenerator />
</Suspense>
</div>
</div>
);
}
图片优化
// components/OptimizedImage.tsx
import { useState, useRef, useEffect } from 'react';
interface OptimizedImageProps {
src: string;
alt: string;
width: number;
height: number;
}
export function OptimizedImage({ src, alt, width, height }: OptimizedImageProps) {
const [isLoaded, setIsLoaded] = useState(false);
const imgRef = useRef<HTMLImageElement>(null);
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
imgRef.current?.setAttribute('src', src);
observer.disconnect();
}
},
{ rootMargin: '100px' }
);
if (imgRef.current) {
observer.observe(imgRef.current);
}
return () => observer.disconnect();
}, [src]);
return (
<div className="relative" style={{ width, height }}>
{!isLoaded && (
<Skeleton className="absolute inset-0" />
)}
<img
ref={imgRef}
alt={alt}
width={width}
height={height}
loading="lazy"
onLoad={() => setIsLoaded(true)}
className={`transition-opacity ${isLoaded ? 'opacity-100' : 'opacity-0'}`}
/>
</div>
);
}
虚拟滚动
// components/VirtualizedMessageList.tsx
import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';
interface Message {
id: string;
content: string;
role: 'user' | 'assistant';
}
interface VirtualizedMessageListProps {
messages: Message[];
}
export function VirtualizedMessageList({ messages }: VirtualizedMessageListProps) {
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: messages.length,
getScrollElement: () => parentRef.current,
estimateSize: () => 80,
overscan: 5,
});
return (
<div ref={parentRef} className="h-[600px] overflow-auto">
<div
style={{
height: `${virtualizer.getTotalSize()}px`,
position: 'relative',
}}
>
{virtualizer.getVirtualItems().map((virtualItem) => (
<div
key={virtualItem.key}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: `${virtualItem.size}px`,
transform: `translateY(${virtualItem.start}px)`,
}}
>
<MessageItem message={messages[virtualItem.index]} />
</div>
))}
</div>
</div>
);
}
状态优化
// 避免不必要的重渲染
const useAuthStore = create<AuthState>()((set, get) => ({
user: null,
accessToken: null,
isAuthenticated: false,
}));
// ✅ 选择性订阅
const user = useAuthStore((state) => state.user);
// ❌ 订阅整个 store
const store = useAuthStore();
请求优化
// hooks/useOptimizedQuery.ts
import { useQuery } from '@tanstack/react-query';
export function useOptimizedQuery<T>(
key: string[],
fetcher: () => Promise<T>,
options?: {
staleTime?: number;
cacheTime?: number;
refetchOnWindowFocus?: boolean;
}
) {
return useQuery({
queryKey: key,
queryFn: fetcher,
staleTime: options?.staleTime ?? 5 * 60 * 1000, // 5 分钟
cacheTime: options?.cacheTime ?? 10 * 60 * 1000, // 10 分钟
refetchOnWindowFocus: options?.refetchOnWindowFocus ?? false,
});
}
// 使用
const { data: sessions } = useOptimizedQuery(
['sessions'],
() => api.get('/api/v1/consultation/sessions'),
{ staleTime: 2 * 60 * 1000 } // 2 分钟
);
2. 后端优化
连接池配置
// database/postgres.go
func Connect(cfg DatabaseConfig) (*gorm.DB, error) {
db, err := gorm.Open(postgres.Open(cfg.DSN), &gorm.Config{})
if err != nil {
return nil, err
}
sqlDB, err := db.DB()
if err != nil {
return nil, err
}
// 连接池配置
sqlDB.SetMaxOpenConns(25) // 最大打开连接数
sqlDB.SetMaxIdleConns(10) // 最大空闲连接数
sqlDB.SetConnMaxLifetime(5 * time.Minute) // 连接最大生命周期
sqlDB.SetConnMaxIdleTime(1 * time.Minute) // 空闲连接最大生命周期
return db, nil
}
Redis 缓存策略
// cache/session.go
type SessionCache struct {
redis *redis.Client
ttl time.Duration
}
func (c *SessionCache) Get(ctx context.Context, sessionID string) (*Session, error) {
key := fmt.Sprintf("session:%s", sessionID)
data, err := c.redis.Get(ctx, key).Bytes()
if err == redis.Nil {
return nil, nil // 缓存未命中
}
if err != nil {
return nil, err
}
var session Session
if err := json.Unmarshal(data, &session); err != nil {
return nil, err
}
return &session, nil
}
func (c *SessionCache) Set(ctx context.Context, session *Session) error {
key := fmt.Sprintf("session:%s", session.ID)
data, err := json.Marshal(session)
if err != nil {
return err
}
return c.redis.Set(ctx, key, data, c.ttl).Err()
}
func (c *SessionCache) Delete(ctx context.Context, sessionID string) error {
key := fmt.Sprintf("session:%s", sessionID)
return c.redis.Del(ctx, key).Err()
}
缓存穿透防护
// cache/session.go
func (c *SessionCache) GetOrFetch(
ctx context.Context,
sessionID string,
fetcher func() (*Session, error),
) (*Session, error) {
// 1. 尝试从缓存获取
session, err := c.Get(ctx, sessionID)
if err != nil {
return nil, err
}
if session != nil {
return session, nil
}
// 2. 缓存未命中,从数据库获取
session, err = fetcher()
if err != nil {
return nil, err
}
// 3. 如果不存在,缓存空值(防穿透)
if session == nil {
c.redis.Set(ctx, fmt.Sprintf("session:%s", sessionID), "null", 1*time.Minute)
return nil, nil
}
// 4. 缓存结果
c.Set(ctx, session)
return session, nil
}
查询优化
// repository/session.go
func (r *SessionRepository) ListWithMessages(
ctx context.Context,
userID uuid.UUID,
limit int,
) ([]Session, error) {
var sessions []Session
// 一次查询获取会话和消息(避免 N+1)
err := r.db.WithContext(ctx).
Preload("Messages", func(db *gorm.DB) *gorm.DB {
return db.Order("created_at ASC").Limit(100)
}).
Where("user_id = ?", userID).
Order("created_at DESC").
Limit(limit).
Find(&sessions).Error
return sessions, err
}
批量操作
// repository/symptom.go
func (r *SymptomRepository) BatchCreate(ctx context.Context, symptoms []Symptom) error {
// 批量插入,减少数据库往返
return r.db.WithContext(ctx).CreateInBatches(symptoms, 100).Error
}
异步处理
// service/consultation.go
func (s *ConsultationService) ProcessMessage(
ctx context.Context,
sessionID string,
message string,
writer *sse.Writer,
) error {
// 1. 保存用户消息(同步)
if err := s.saveUserMessage(ctx, sessionID, message); err != nil {
return err
}
// 2. 异步处理 AI 响应
go func() {
err := s.generateAIResponse(ctx, sessionID, message, writer)
if err != nil {
writer.WriteEvent("error", gin.H{"message": "processing failed"})
}
writer.WriteEvent("done", nil)
}()
return nil
}
3. 数据库优化
索引优化
-- 复合索引覆盖常见查询
CREATE INDEX idx_sessions_user_created
ON consultation_sessions(user_id, created_at DESC);
-- 部分索引(只索引活跃会话)
CREATE INDEX idx_sessions_active
ON consultation_sessions(user_id)
WHERE status = 'in_progress';
-- 覆盖索引(查询只需要索引列)
CREATE INDEX idx_messages_session_id
ON consultation_messages(session_id)
INCLUDE (content, created_at);
查询分析
-- 分析查询计划
EXPLAIN ANALYZE
SELECT s.*, json_agg(m.*) as messages
FROM consultation_sessions s
LEFT JOIN consultation_messages m ON s.id = m.session_id
WHERE s.user_id = 'user-123'
GROUP BY s.id
ORDER BY s.created_at DESC
LIMIT 20;
分区表
-- 按时间分区消息表
CREATE TABLE consultation_messages (
id UUID,
session_id UUID,
content TEXT,
created_at TIMESTAMP
) PARTITION BY RANGE (created_at);
-- 创建分区
CREATE TABLE messages_2026_06 PARTITION OF consultation_messages
FOR VALUES FROM ('2026-06-01') TO ('2026-07-01');
CREATE TABLE messages_2026_07 PARTITION OF consultation_messages
FOR VALUES FROM ('2026-07-01') TO ('2026-08-01');
读写分离
// database/replica.go
type Database struct {
primary *gorm.DB
replica *gorm.DB
}
func (d *Database) Read() *gorm.DB {
return d.replica
}
func (d *Database) Write() *gorm.DB {
return d.primary
}
// 使用
func (r *SessionRepository) GetByID(ctx context.Context, id uuid.UUID) (*Session, error) {
var session Session
err := r.db.Read().WithContext(ctx).Where("id = ?", id).First(&session).Error
return &session, err
}
func (r *SessionRepository) Create(ctx context.Context, session *Session) error {
return r.db.Write().WithContext(ctx).Create(session).Error
}
4. AI 服务优化
LLM 请求优化
# ai/provider.py
class LLMProvider:
def __init__(self):
self.client = AsyncOpenAI()
self.cache = {} # 简单缓存
async def chat(
self,
messages: list[dict],
model: str = "gpt-4o-mini",
temperature: float = 0.7,
) -> str:
# 1. 检查缓存(对于确定性查询)
cache_key = self._get_cache_key(messages)
if cache_key in self.cache:
return self.cache[cache_key]
# 2. 流式请求(减少首字延迟)
response = await self.client.chat.completions.create(
model=model,
messages=messages,
temperature=temperature,
stream=True,
)
# 3. 累积响应
content = ""
async for chunk in response:
if chunk.choices[0].delta.content:
content += chunk.choices[0].delta.content
# 4. 缓存结果
if self._should_cache(messages):
self.cache[cache_key] = content
return content
Embedding 优化
# ai/embedding.py
class EmbeddingService:
def __init__(self):
self.model = SentenceTransformer('all-MiniLM-L6-v2')
self.cache = {}
async def generate(self, text: str) -> list[float]:
# 检查缓存
if text in self.cache:
return self.cache[text]
# 批量生成(如果有多条)
embedding = self.model.encode(text).tolist()
# 缓存
self.cache[text] = embedding
return embedding
async def batch_generate(self, texts: list[str]) -> list[list[float]]:
# 批量生成,减少 API 调用
embeddings = self.model.encode(texts).tolist()
# 缓存
for text, embedding in zip(texts, embeddings):
self.cache[text] = embedding
return embeddings
RAG 优化
# ai/rag.py
class RAGService:
async def search(
self,
query: str,
limit: int = 5,
use_cache: bool = True,
) -> list[SearchResult]:
# 1. 检查缓存
cache_key = f"rag:{hash(query)}"
if use_cache:
cached = await self.redis.get(cache_key)
if cached:
return json.loads(cached)
# 2. 生成 query embedding
query_embedding = await self.embedding_service.generate(query)
# 3. 向量搜索(使用索引)
results = await self.vector_store.search(
embedding=query_embedding,
limit=limit * 2, # 多取一些,后续重排
)
# 4. 意图感知重排
reranked = self.reranker.rerank(results, query)
# 5. 截取 top-k
final_results = reranked[:limit]
# 6. 缓存结果
if use_cache:
await self.redis.setex(
cache_key,
300, # 5 分钟
json.dumps(final_results),
)
return final_results
并发控制
# ai/concurrency.py
import asyncio
class ConcurrencyLimiter:
def __init__(self, max_concurrent: int = 10):
self.semaphore = asyncio.Semaphore(max_concurrent)
async def run(self, coro):
async with self.semaphore:
return await coro
# 使用
limiter = ConcurrencyLimiter(max_concurrent=10)
async def process_message(message: str):
return await limiter.run(llm.chat(message))
5. 缓存策略
缓存层次
┌─────────────────────────────────────────────────────────┐
│ 浏览器缓存 │
│ - Service Worker │
│ - HTTP 缓存头 │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ CDN 缓存 │
│ - 静态资源 │
│ - API 响应(可选) │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ Redis 缓存 │
│ - 会话数据 │
│ - 用户信息 │
│ - RAG 搜索结果 │
└─────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────┐
│ 数据库 │
│ - 持久化存储 │
└─────────────────────────────────────────────────────────┘
HTTP 缓存头
// handler/static.go
func StaticFiles(r *gin.Engine) {
// 静态资源缓存
r.Static("/assets", "./dist/assets")
r.StaticFS("/assets", http.Dir("./dist/assets"))
// API 响应缓存
r.GET("/api/v1/health", func(c *gin.Context) {
c.Header("Cache-Control", "public, max-age=60") // 1 分钟
c.JSON(200, gin.H{"status": "ok"})
})
}
Redis 缓存配置
// cache/config.go
type CacheConfig struct {
SessionTTL time.Duration
UserTTL time.Duration
RAGResultTTL time.Duration
BlacklistTTL time.Duration
}
var DefaultCacheConfig = CacheConfig{
SessionTTL: 30 * time.Minute,
UserTTL: 15 * time.Minute,
RAGResultTTL: 5 * time.Minute,
BlacklistTTL: 7 * 24 * time.Hour, // 7 天
}
6. 性能监控
Web Vitals
// utils/webVitals.ts
import { onCLS, onFID, onLCP } from 'web-vitals';
function reportMetric(metric: any) {
console.log(metric);
// 发送到监控服务
fetch('/api/v1/metrics/web-vitals', {
method: 'POST',
body: JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
}),
});
}
onCLS(reportMetric);
onFID(reportMetric);
onLCP(reportMetric);
性能计时
// middleware/performance.go
func PerformanceMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
start := time.Now()
c.Next()
duration := time.Since(start)
// 记录慢请求
if duration > 1*time.Second {
logger.Warn("slow request",
slog.String("method", c.Request.Method),
slog.String("path", c.FullPath()),
slog.Duration("duration", duration),
)
}
// 记录性能指标
httpRequestDuration.WithLabelValues(
c.Request.Method,
c.FullPath(),
).Observe(duration.Seconds())
}
}
7. 性能测试
负载测试
// k6/load-test.js
import http from 'k6/http';
import { check, sleep } from 'k6';
export const options = {
stages: [
{ duration: '2m', target: 100 }, // 2 分钟内增加到 100 用户
{ duration: '5m', target: 100 }, // 保持 100 用户 5 分钟
{ duration: '2m', target: 0 }, // 2 分钟内降到 0
],
thresholds: {
http_req_duration: ['p(95)<500'], // 95% 请求 < 500ms
http_req_failed: ['rate<0.01'], // 错误率 < 1%
},
};
export default function () {
const res = http.get('http://localhost:8080/api/v1/health');
check(res, {
'status is 200': (r) => r.status === 200,
});
sleep(1);
}
压力测试
# k6 压力测试
k6 run --vus 100 --duration 5m k6/load-test.js
# 输出
# http_req_duration..............: avg=150ms min=50ms med=120ms max=800ms p(90)=250ms p(95)=350ms
# http_req_failed................: 0.00% ✓ 0 ✗ 10000
常见面试问题
Q: 你的前端性能优化策略是什么?
A:
- 代码分割:路由级懒加载,减少首屏 JS 体积
- 图片优化:懒加载、WebP 格式、CDN 分发
- 虚拟滚动:长列表优化
- 状态优化:Zustand 选择性订阅,避免不必要重渲染
- 请求优化:TanStack Query 缓存、staleTime 配置
Q: 后端怎么优化 API 响应时间?
A:
- 数据库优化:索引、查询优化、连接池
- 缓存策略:Redis 缓存热点数据
- 异步处理:非关键路径异步执行
- 并发控制:限制并发数,防止过载
Q: AI 服务怎么优化响应速度?
A:
- 流式响应:减少首字延迟
- 缓存:缓存确定性查询结果
- 并发控制:限制并发 LLM 调用
- 模型选择:简单任务用小模型
常见错误
过度优化
// ❌ 过度缓存,数据一致性问题
func (r *Repo) Get(ctx context.Context, id string) (*Entity, error) {
// 总是从缓存获取,不检查数据库
return r.cache.Get(ctx, id)
}
// ✅ 缓存 + 数据库
func (r *Repo) Get(ctx context.Context, id string) (*Entity, error) {
// 1. 先查缓存
entity, _ := r.cache.Get(ctx, id)
if entity != nil {
return entity, nil
}
// 2. 缓存未命中,查数据库
entity, err := r.db.Get(ctx, id)
if err != nil {
return nil, err
}
// 3. 写入缓存
r.cache.Set(ctx, entity)
return entity, nil
}
忽略 N+1 查询
// ❌ N+1 查询
sessions, _ := repo.List(ctx, userID)
for _, session := range sessions {
messages, _ := messageRepo.ListBySession(ctx, session.ID)
session.Messages = messages
}
// ✅ 预加载
sessions, _ := repo.ListWithMessages(ctx, userID)
不监控性能
// ❌ 不记录性能指标
func (h *Handler) Process(c *gin.Context) {
result, _ := h.service.Process(c.Request.Context())
c.JSON(200, result)
}
// ✅ 记录性能指标
func (h *Handler) Process(c *gin.Context) {
start := time.Now()
result, _ := h.service.Process(c.Request.Context())
duration := time.Since(start)
httpRequestDuration.WithLabelValues("process").Observe(duration.Seconds())
c.JSON(200, result)
}