BodySense 性能优化策略

BodySense 项目的性能优化策略:前端懒加载、后端缓存、数据库优化、AI 服务优化、性能监控。

#type / concept #status / growing #tech / dev #tech / ai

[!info] related notes

BodySense 性能优化策略

性能目标

指标目标说明
首屏加载 (FCP)< 1.5s首次内容绘制
最大内容绘制 (LCP)< 2.5s最大内容元素渲染
首次输入延迟 (FID)< 100ms用户交互响应
累积布局偏移 (CLS)< 0.1布局稳定性
API 响应时间 P95< 500ms后端 API
LLM 响应时间 P95< 5sAI 服务

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:

  1. 代码分割:路由级懒加载,减少首屏 JS 体积
  2. 图片优化:懒加载、WebP 格式、CDN 分发
  3. 虚拟滚动:长列表优化
  4. 状态优化:Zustand 选择性订阅,避免不必要重渲染
  5. 请求优化:TanStack Query 缓存、staleTime 配置

Q: 后端怎么优化 API 响应时间?

A:

  1. 数据库优化:索引、查询优化、连接池
  2. 缓存策略:Redis 缓存热点数据
  3. 异步处理:非关键路径异步执行
  4. 并发控制:限制并发数,防止过载

Q: AI 服务怎么优化响应速度?

A:

  1. 流式响应:减少首字延迟
  2. 缓存:缓存确定性查询结果
  3. 并发控制:限制并发 LLM 调用
  4. 模型选择:简单任务用小模型

常见错误

过度优化

// ❌ 过度缓存,数据一致性问题
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)
}
创建于 2026/6/25 更新于 2026/6/25