mirror of
https://github.com/Dvorinka/MyClubServer.git
synced 2026-06-03 18:22:57 +00:00
746 lines
15 KiB
Markdown
746 lines
15 KiB
Markdown
# 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:
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```tsx
|
|
// 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
|
|
|
|
```html
|
|
<!-- 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:
|
|
|
|
```css
|
|
/* 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
|
|
|
|
```bash
|
|
cd frontend
|
|
npm run build
|
|
npx source-map-explorer 'build/static/js/*.js'
|
|
```
|
|
|
|
#### Tree Shaking
|
|
|
|
```javascript
|
|
// Import only what you need
|
|
// ❌ Bad
|
|
import _ from 'lodash';
|
|
|
|
// ✅ Good
|
|
import debounce from 'lodash/debounce';
|
|
```
|
|
|
|
#### Remove Unused Dependencies
|
|
|
|
```bash
|
|
npm install -g depcheck
|
|
depcheck
|
|
```
|
|
|
|
### 1.6 Caching Strategy
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```sql
|
|
-- 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
|
|
|
|
```go
|
|
// ❌ 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
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
|
|
```go
|
|
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
|
|
|
|
```sql
|
|
-- 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
|
|
|
|
```sql
|
|
-- 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
|
|
|
|
```ini
|
|
# 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
|
|
|
|
```go
|
|
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:
|
|
```bash
|
|
go tool pprof http://localhost:6060/debug/pprof/profile?seconds=30
|
|
```
|
|
|
|
### 4.2 Frontend Performance Monitoring
|
|
|
|
```typescript
|
|
// 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
|
|
|
|
```go
|
|
// 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
|
|
# 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
|
|
|
|
```javascript
|
|
// 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:
|
|
|
|
```javascript
|
|
// 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
|