Files
MyClub/DOCS/PERFORMANCE_OPTIMIZATION_GUIDE.md
T
Tomáš Dvořák 12cba639b9 upload
2025-10-16 13:32:05 +02:00

15 KiB

Performance Optimization Guide

This guide provides comprehensive strategies to optimize application performance.


1. Frontend Performance

1.1 Code Splitting (IMPLEMENTED)

The App.lazy.tsx file implements route-based code splitting. To use it:

// frontend/src/index.tsx
import AppLazy from './App.lazy';

root.render(
  <React.StrictMode>
    <ErrorBoundary>
      <HelmetProvider>
        <ColorModeScript initialColorMode={theme.config.initialColorMode} />
        <AppLazy />
      </HelmetProvider>
    </ErrorBoundary>
  </React.StrictMode>
);

Expected Results:

  • Initial bundle size reduced by 60-70%
  • Faster Time to Interactive (TTI)
  • Better Lighthouse scores

1.2 Image Optimization

Server-Side Image Processing

// pkg/utils/image.go
package utils

import (
    "image"
    "image/jpeg"
    "image/png"
    "os"
    
    "github.com/nfnt/resize"
)

type ImageSize struct {
    Width  uint
    Height uint
    Name   string
}

var ThumbnailSizes = []ImageSize{
    {Width: 150, Height: 150, Name: "thumb"},
    {Width: 400, Height: 400, Name: "small"},
    {Width: 800, Height: 800, Name: "medium"},
    {Width: 1200, Height: 0, Name: "large"},
}

func GenerateThumbnails(sourcePath string, outputDir string) (map[string]string, error) {
    file, err := os.Open(sourcePath)
    if err != nil {
        return nil, err
    }
    defer file.Close()
    
    img, format, err := image.Decode(file)
    if err != nil {
        return nil, err
    }
    
    results := make(map[string]string)
    
    for _, size := range ThumbnailSizes {
        resized := resize.Resize(size.Width, size.Height, img, resize.Lanczos3)
        
        outputPath := fmt.Sprintf("%s/%s_%s.jpg", outputDir, 
            filepath.Base(sourcePath), size.Name)
        
        out, err := os.Create(outputPath)
        if err != nil {
            continue
        }
        
        if format == "png" {
            png.Encode(out, resized)
        } else {
            jpeg.Encode(out, resized, &jpeg.Options{Quality: 85})
        }
        out.Close()
        
        results[size.Name] = outputPath
    }
    
    return results, nil
}

WebP Conversion

# Install webp tools
# Ubuntu: sudo apt-get install webp
# Mac: brew install webp

# Convert images
cwebp input.jpg -q 80 -o output.webp

Frontend: Responsive Images

// components/OptimizedImage.tsx
import React from 'react';
import { Box, Image } from '@chakra-ui/react';

interface OptimizedImageProps {
  src: string;
  alt: string;
  sizes?: string;
}

export const OptimizedImage: React.FC<OptimizedImageProps> = ({ src, alt, sizes }) => {
  const basePath = src.replace(/\.[^.]+$/, '');
  
  return (
    <picture>
      {/* WebP sources */}
      <source
        type="image/webp"
        srcSet={`
          ${basePath}_small.webp 400w,
          ${basePath}_medium.webp 800w,
          ${basePath}_large.webp 1200w
        `}
        sizes={sizes || "(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px"}
      />
      
      {/* Fallback JPEG */}
      <source
        type="image/jpeg"
        srcSet={`
          ${basePath}_small.jpg 400w,
          ${basePath}_medium.jpg 800w,
          ${basePath}_large.jpg 1200w
        `}
        sizes={sizes || "(max-width: 400px) 400px, (max-width: 800px) 800px, 1200px"}
      />
      
      {/* Ultimate fallback */}
      <Image src={src} alt={alt} loading="lazy" />
    </picture>
  );
};

1.3 Lazy Loading Images

// Use Intersection Observer for lazy loading
import { useEffect, useRef, useState } from 'react';

export const useLazyLoad = () => {
  const [isVisible, setIsVisible] = useState(false);
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const observer = new IntersectionObserver(
      ([entry]) => {
        if (entry.isIntersecting) {
          setIsVisible(true);
          observer.disconnect();
        }
      },
      { rootMargin: '50px' }
    );

    if (ref.current) {
      observer.observe(ref.current);
    }

    return () => observer.disconnect();
  }, []);

  return { ref, isVisible };
};

// Usage
const LazyImage = ({ src, alt }: { src: string; alt: string }) => {
  const { ref, isVisible } = useLazyLoad();
  
  return (
    <div ref={ref}>
      {isVisible ? (
        <img src={src} alt={alt} />
      ) : (
        <div style={{ height: 200, background: '#f0f0f0' }} />
      )}
    </div>
  );
};

1.4 Font Optimization

<!-- frontend/public/index.html -->
<head>
  <!-- Preconnect to font CDN -->
  <link rel="preconnect" href="https://fonts.googleapis.com" />
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
  
  <!-- Load fonts with display=swap -->
  <link 
    href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Montserrat:wght@600;700;800&display=swap" 
    rel="stylesheet" 
  />
</head>

Or use local fonts:

/* frontend/src/fonts.css */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-Regular.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
}

@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-Bold.woff2') format('woff2');
  font-weight: 700;
  font-display: swap;
}

1.5 Bundle Size Reduction

Analyze Bundle

cd frontend
npm run build
npx source-map-explorer 'build/static/js/*.js'

Tree Shaking

// Import only what you need
// ❌ Bad
import _ from 'lodash';

// ✅ Good
import debounce from 'lodash/debounce';

Remove Unused Dependencies

npm install -g depcheck
depcheck

1.6 Caching Strategy

// frontend/src/services/api.ts
import { QueryClient } from '@tanstack/react-query';

export const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      cacheTime: 10 * 60 * 1000, // 10 minutes
      refetchOnWindowFocus: false,
      retry: 1,
      // Add cache keys
      queryKeyHashFn: (queryKey) => JSON.stringify(queryKey),
    },
  },
});

// Prefetch critical data
queryClient.prefetchQuery(['settings'], fetchSettings);

2. Backend Performance

2.1 Database Query Optimization

Use Database Indexes

-- Add indexes for frequently queried columns
CREATE INDEX idx_articles_published ON articles(published, published_at DESC);
CREATE INDEX idx_articles_slug ON articles(slug);
CREATE INDEX idx_articles_category ON articles(category_id);
CREATE INDEX idx_players_team ON players(team_id);
CREATE INDEX idx_matches_date ON matches(match_date);

-- Composite indexes
CREATE INDEX idx_articles_published_featured ON articles(published, featured, published_at DESC);

N+1 Query Prevention

// ❌ Bad - N+1 queries
var articles []models.Article
db.Find(&articles)
for _, article := range articles {
    db.Model(&article).Association("Category").Find(&article.Category)
}

// ✅ Good - Single query with joins
var articles []models.Article
db.Preload("Category").Preload("Author").Find(&articles)

Pagination

func GetArticlesPaginated(c *gin.Context, db *gorm.DB) {
    page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
    pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
    
    if page < 1 {
        page = 1
    }
    if pageSize > 100 {
        pageSize = 100 // Prevent excessive queries
    }
    
    var articles []models.Article
    var total int64
    
    db.Model(&models.Article{}).Where("published = ?", true).Count(&total)
    
    offset := (page - 1) * pageSize
    db.Where("published = ?", true).
        Order("published_at DESC").
        Limit(pageSize).
        Offset(offset).
        Preload("Category").
        Find(&articles)
    
    c.JSON(http.StatusOK, gin.H{
        "items":      articles,
        "page":       page,
        "page_size":  pageSize,
        "total":      total,
        "total_pages": (total + int64(pageSize) - 1) / int64(pageSize),
    })
}

2.2 Caching Layer

Redis Integration

// pkg/cache/redis.go
package cache

import (
    "context"
    "encoding/json"
    "time"
    
    "github.com/redis/go-redis/v9"
)

var RedisClient *redis.Client

func InitRedis(addr string) {
    RedisClient = redis.NewClient(&redis.Options{
        Addr: addr,
        DB:   0,
    })
}

func Get(ctx context.Context, key string, dest interface{}) error {
    val, err := RedisClient.Get(ctx, key).Result()
    if err != nil {
        return err
    }
    return json.Unmarshal([]byte(val), dest)
}

func Set(ctx context.Context, key string, value interface{}, expiration time.Duration) error {
    json, err := json.Marshal(value)
    if err != nil {
        return err
    }
    return RedisClient.Set(ctx, key, json, expiration).Err()
}

// Usage in controller
func (bc *BaseController) GetArticles(c *gin.Context) {
    cacheKey := "articles:published"
    
    var articles []models.Article
    err := cache.Get(c, cacheKey, &articles)
    
    if err == nil {
        c.JSON(http.StatusOK, articles)
        return
    }
    
    // Cache miss - fetch from DB
    bc.DB.Where("published = ?", true).Find(&articles)
    cache.Set(c, cacheKey, articles, 5*time.Minute)
    
    c.JSON(http.StatusOK, articles)
}

In-Memory Cache

// Simple in-memory cache for small datasets
type Cache struct {
    sync.RWMutex
    data map[string]cacheItem
}

type cacheItem struct {
    value      interface{}
    expiration time.Time
}

func (c *Cache) Get(key string) (interface{}, bool) {
    c.RLock()
    defer c.RUnlock()
    
    item, exists := c.data[key]
    if !exists || time.Now().After(item.expiration) {
        return nil, false
    }
    
    return item.value, true
}

func (c *Cache) Set(key string, value interface{}, ttl time.Duration) {
    c.Lock()
    defer c.Unlock()
    
    c.data[key] = cacheItem{
        value:      value,
        expiration: time.Now().Add(ttl),
    }
}

2.3 Connection Pooling

// main.go or database initialization
sqlDB, _ := db.DB()

// Set connection pool parameters
sqlDB.SetMaxIdleConns(10)
sqlDB.SetMaxOpenConns(100)
sqlDB.SetConnMaxLifetime(time.Hour)
sqlDB.SetConnMaxIdleTime(10 * time.Minute)

2.4 Response Compression

// Use gzip middleware
import "github.com/gin-contrib/gzip"

func main() {
    r := gin.Default()
    r.Use(gzip.Gzip(gzip.DefaultCompression))
    // ...
}

2.5 HTTP Caching Headers

func (bc *BaseController) GetPublicData(c *gin.Context) {
    // Set cache headers for public data
    c.Header("Cache-Control", "public, max-age=3600, stale-while-revalidate=86400")
    c.Header("ETag", generateETag(data))
    
    // Check If-None-Match header
    if c.GetHeader("If-None-Match") == etag {
        c.Status(http.StatusNotModified)
        return
    }
    
    c.JSON(http.StatusOK, data)
}

3. Database Performance

3.1 Query Optimization

-- Use EXPLAIN ANALYZE to check query performance
EXPLAIN ANALYZE
SELECT * FROM articles 
WHERE published = true 
ORDER BY published_at DESC 
LIMIT 20;

-- Add missing indexes based on EXPLAIN output

3.2 Vacuum and Analyze

-- Regular maintenance
VACUUM ANALYZE articles;
VACUUM ANALYZE players;
VACUUM ANALYZE matches;

-- Schedule in cron
0 2 * * * psql -d fotbal_club -c "VACUUM ANALYZE"

3.3 Connection Pooling with PgBouncer

# pgbouncer.ini
[databases]
fotbal_club = host=localhost port=5432 dbname=fotbal_club

[pgbouncer]
listen_port = 6432
listen_addr = *
auth_type = md5
auth_file = /etc/pgbouncer/userlist.txt
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20

4. Monitoring & Profiling

4.1 Go Profiling

import _ "net/http/pprof"

func main() {
    // Enable pprof in development
    if config.AppConfig.Debug {
        go func() {
            log.Println(http.ListenAndServe("localhost:6060", nil))
        }()
    }
    
    // ... rest of main
}

Access profiles:

  • CPU: http://localhost:6060/debug/pprof/profile?seconds=30
  • Memory: http://localhost:6060/debug/pprof/heap
  • Goroutines: http://localhost:6060/debug/pprof/goroutine

Analyze:

go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30

4.2 Frontend Performance Monitoring

// frontend/src/reportWebVitals.ts
import { Metric } from 'web-vitals';

const reportWebVitals = (onPerfEntry?: (metric: Metric) => void) => {
  if (onPerfEntry && onPerfEntry instanceof Function) {
    import('web-vitals').then(({ onCLS, onFID, onFCP, onLCP, onTTFB }) => {
      onCLS(onPerfEntry);
      onFID(onPerfEntry);
      onFCP(onPerfEntry);
      onLCP(onPerfEntry);
      onTTFB(onPerfEntry);
    });
  }
};

// Send to analytics
reportWebVitals((metric) => {
  console.log(metric);
  // Send to analytics endpoint
  fetch('/api/v1/analytics/vitals', {
    method: 'POST',
    body: JSON.stringify(metric),
  });
});

4.3 Database Query Logging

// Enable query logging in development
if config.AppConfig.Debug {
    db = db.Debug()
}

// Or custom logger
newLogger := logger.New(
    log.New(os.Stdout, "\r\n", log.LstdFlags),
    logger.Config{
        SlowThreshold:             200 * time.Millisecond,
        LogLevel:                  logger.Warn,
        IgnoreRecordNotFoundError: true,
        Colorful:                  true,
    },
)

db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{
    Logger: newLogger,
})

5. CDN & Static Assets

5.1 Use CDN for Static Assets

# nginx.conf
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
    expires 1y;
    add_header Cache-Control "public, immutable";
}

5.2 Asset Versioning

// In build process - add hash to filenames
output: {
    filename: '[name].[contenthash].js',
    chunkFilename: '[name].[contenthash].chunk.js',
}

6. Performance Budget

Set performance budgets to prevent regression:

// budget.json
{
  "budgets": [
    {
      "resourceSizes": [
        {
          "resourceType": "script",
          "budget": 300
        },
        {
          "resourceType": "total",
          "budget": 500
        }
      ],
      "resourceCounts": [
        {
          "resourceType": "third-party",
          "budget": 10
        }
      ]
    }
  ]
}

Performance Checklist

Frontend

  • Code splitting implemented
  • Images optimized (WebP, responsive)
  • Lazy loading for images/components
  • Fonts optimized (display=swap, preconnect)
  • Bundle size < 300KB (gzipped)
  • No console.logs in production
  • Service worker for caching
  • Debounced search inputs

Backend

  • Database indexes on query columns
  • Connection pooling configured
  • Response compression enabled
  • Cache headers set appropriately
  • N+1 queries eliminated
  • API pagination implemented
  • Redis cache for hot data
  • Slow query logging enabled

Database

  • Indexes created
  • Regular VACUUM ANALYZE
  • PgBouncer for connection pooling
  • Query performance analyzed
  • Backup strategy defined

Monitoring

  • Application metrics tracked
  • Error tracking configured
  • Performance alerts set
  • Database metrics monitored
  • CDN/cache hit rates tracked

Expected Improvements

After implementing these optimizations:

  • Page Load Time: 50-70% faster
  • Time to Interactive: 60-80% faster
  • First Contentful Paint: 40-60% faster
  • Bundle Size: 50-70% smaller
  • Server Response Time: 30-50% faster
  • Database Query Time: 40-70% faster
  • Lighthouse Score: 90+ on all metrics