Performance Optimization - RumenDamyanov/js-chess GitHub Wiki
Comprehensive guide to optimizing performance across all framework implementations in the chess showcase.
Performance optimization in this chess showcase covers:
- Frontend bundle optimization
- Runtime performance tuning
- Memory management
- Network efficiency
- Backend integration optimization
- Real-time communication performance
- Mobile performance considerations
// angular-chess/src/app/app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
redirectTo: '/game',
pathMatch: 'full'
},
{
path: 'game',
loadComponent: () => import('./components/game/game.component').then(m => m.GameComponent)
},
{
path: 'analysis',
loadComponent: () => import('./components/analysis/analysis.component').then(m => m.AnalysisComponent)
},
{
path: 'settings',
loadComponent: () => import('./components/settings/settings.component').then(m => m.SettingsComponent)
}
];
@NgModule({
imports: [RouterModule.forRoot(routes, {
preloadingStrategy: PreloadAllModules, // Preload after initial load
enableTracing: false // Set to true for debugging
})],
exports: [RouterModule]
})
export class AppRoutingModule { }
// angular-chess/src/app/components/game/game.component.ts
import { Component, OnInit } from '@angular/core';
import { CommonModule } from '@angular/common';
// Lazy load heavy components
@Component({
selector: 'app-game',
standalone: true,
imports: [CommonModule],
template: `
<div class="game-container">
<app-chess-board></app-chess-board>
<!-- Lazy load analysis panel -->
@if (showAnalysis) {
<app-analysis-panel></app-analysis-panel>
}
<!-- Lazy load chat component -->
@if (showChat) {
<app-ai-chat></app-ai-chat>
}
</div>
`
})
export class GameComponent implements OnInit {
showAnalysis = false;
showChat = false;
async loadAnalysis() {
if (!this.showAnalysis) {
// Dynamic import for heavy analysis features
const { AnalysisPanelComponent } = await import('../analysis/analysis-panel.component');
this.showAnalysis = true;
}
}
async loadChat() {
if (!this.showChat) {
// Dynamic import for AI chat
const { AiChatComponent } = await import('../ai-chat/ai-chat.component');
this.showChat = true;
}
}
}
// react-chess/src/App.jsx
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import LoadingSpinner from './components/LoadingSpinner';
// Lazy load route components
const Game = lazy(() => import('./pages/Game'));
const Analysis = lazy(() => import('./pages/Analysis'));
const Settings = lazy(() => import('./pages/Settings'));
// Lazy load heavy features
const AIChat = lazy(() => import('./components/AIChat'));
const AnalysisPanel = lazy(() => import('./components/AnalysisPanel'));
function App() {
return (
<Router>
<div className="app">
<Suspense fallback={<LoadingSpinner />}>
<Routes>
<Route path="/" element={<Game />} />
<Route path="/analysis" element={<Analysis />} />
<Route path="/settings" element={<Settings />} />
</Routes>
</Suspense>
</div>
</Router>
);
}
export default App;
// react-chess/src/hooks/useLazyComponent.js
import { useState, useCallback } from 'react';
export function useLazyComponent(importFn) {
const [Component, setComponent] = useState(null);
const [loading, setLoading] = useState(false);
const [error, setError] = useState(null);
const loadComponent = useCallback(async () => {
if (Component) return Component;
setLoading(true);
setError(null);
try {
const module = await importFn();
const LoadedComponent = module.default || module;
setComponent(() => LoadedComponent);
return LoadedComponent;
} catch (err) {
setError(err);
throw err;
} finally {
setLoading(false);
}
}, [importFn, Component]);
return { Component, loading, error, loadComponent };
}
// vue-chess/src/router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const routes = [
{
path: '/',
name: 'Game',
component: () => import('../views/GameView.vue')
},
{
path: '/analysis',
name: 'Analysis',
component: () => import('../views/AnalysisView.vue')
},
{
path: '/settings',
name: 'Settings',
component: () => import('../views/SettingsView.vue')
}
];
const router = createRouter({
history: createWebHistory(),
routes
});
export default router;
<!-- vue-chess/src/components/GameBoard.vue -->
<template>
<div class="game-board">
<ChessBoard />
<!-- Lazy load heavy components -->
<Suspense>
<AnalysisPanel v-if="showAnalysis" />
<template #fallback>
<div class="loading">Loading analysis...</div>
</template>
</Suspense>
<Suspense>
<AIChat v-if="showChat" />
<template #fallback>
<div class="loading">Loading chat...</div>
</template>
</Suspense>
</div>
</template>
<script setup>
import { ref, defineAsyncComponent } from 'vue';
import ChessBoard from './ChessBoard.vue';
// Lazy load heavy components
const AnalysisPanel = defineAsyncComponent(() => import('./AnalysisPanel.vue'));
const AIChat = defineAsyncComponent(() => import('./AIChat.vue'));
const showAnalysis = ref(false);
const showChat = ref(false);
</script>
// webpack.config.js (for React and Vue)
const path = require('path');
const webpack = require('webpack');
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer');
module.exports = {
// Production optimizations
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
priority: 20
},
chess: {
test: /[\\/]src[\\/]chess[\\/]/,
name: 'chess-engine',
chunks: 'all',
priority: 10
},
ai: {
test: /[\\/]src[\\/]ai[\\/]/,
name: 'ai-features',
chunks: 'all',
priority: 10
}
}
},
usedExports: true,
sideEffects: false,
minimizer: [
new TerserPlugin({
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
})
]
},
// Tree shaking
resolve: {
alias: {
'@': path.resolve(__dirname, 'src'),
'chess-engine': path.resolve(__dirname, 'src/chess'),
'shared': path.resolve(__dirname, '../shared')
}
},
// Performance budgets
performance: {
maxAssetSize: 250000,
maxEntrypointSize: 250000,
hints: 'warning'
},
plugins: [
// Analyze bundle size
process.env.ANALYZE && new BundleAnalyzerPlugin(),
// Define environment variables
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV)
})
].filter(Boolean)
};
// shared/utils/index.js - Proper ES module exports
export { ChessEngine } from './chess-engine';
export { AIAnalyzer } from './ai-analyzer';
export { WebSocketClient } from './websocket-client';
export { GameHistory } from './game-history';
// Individual exports for better tree shaking
export const moveValidation = {
isValidMove,
isPieceBlocked,
isInCheck
};
export const boardUtils = {
createBoard,
cloneBoard,
getBoardFEN
};
// Optimized imports in components
// ❌ Bad - imports entire library
import * as utils from 'shared/utils';
// ✅ Good - imports only what's needed
import { ChessEngine, moveValidation } from 'shared/utils';
import { isValidMove } from 'shared/utils/move-validation';
// shared/performance/ObjectPool.js
export class ObjectPool {
constructor(createFn, resetFn, initialSize = 10) {
this.createFn = createFn;
this.resetFn = resetFn;
this.pool = [];
this.active = new Set();
// Pre-populate pool
for (let i = 0; i < initialSize; i++) {
this.pool.push(this.createFn());
}
}
acquire() {
let obj;
if (this.pool.length > 0) {
obj = this.pool.pop();
} else {
obj = this.createFn();
}
this.active.add(obj);
return obj;
}
release(obj) {
if (this.active.has(obj)) {
this.active.delete(obj);
this.resetFn(obj);
this.pool.push(obj);
}
}
clear() {
this.pool.length = 0;
this.active.clear();
}
getStats() {
return {
poolSize: this.pool.length,
activeCount: this.active.size,
totalAllocated: this.pool.length + this.active.size
};
}
}
// Usage for chess positions
export const positionPool = new ObjectPool(
() => ({
board: new Array(64).fill(null),
turn: 'white',
castling: { K: true, Q: true, k: true, q: true },
enPassant: null,
halfmove: 0,
fullmove: 1
}),
(position) => {
position.board.fill(null);
position.turn = 'white';
position.castling = { K: true, Q: true, k: true, q: true };
position.enPassant = null;
position.halfmove = 0;
position.fullmove = 1;
},
50 // Pre-allocate 50 positions
);
// shared/chess/MoveGenerator.js
export class OptimizedMoveGenerator {
constructor() {
// Pre-computed move patterns
this.knightMoves = this.precomputeKnightMoves();
this.kingMoves = this.precomputeKingMoves();
this.rayDirections = [
[-1, -1], [-1, 0], [-1, 1],
[0, -1], [0, 1],
[1, -1], [1, 0], [1, 1]
];
// Bitboard representations for faster computation
this.bitboards = {
white: 0n,
black: 0n,
occupied: 0n
};
}
precomputeKnightMoves() {
const moves = new Array(64);
const deltas = [[-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1]];
for (let square = 0; square < 64; square++) {
const rank = Math.floor(square / 8);
const file = square % 8;
moves[square] = [];
for (const [dr, df] of deltas) {
const newRank = rank + dr;
const newFile = file + df;
if (newRank >= 0 && newRank < 8 && newFile >= 0 && newFile < 8) {
moves[square].push(newRank * 8 + newFile);
}
}
}
return moves;
}
precomputeKingMoves() {
const moves = new Array(64);
for (let square = 0; square < 64; square++) {
const rank = Math.floor(square / 8);
const file = square % 8;
moves[square] = [];
for (let dr = -1; dr <= 1; dr++) {
for (let df = -1; df <= 1; df++) {
if (dr === 0 && df === 0) continue;
const newRank = rank + dr;
const newFile = file + df;
if (newRank >= 0 && newRank < 8 && newFile >= 0 && newFile < 8) {
moves[square].push(newRank * 8 + newFile);
}
}
}
}
return moves;
}
generateMoves(position, piece, square) {
const moves = [];
const pieceType = piece.type.toLowerCase();
switch (pieceType) {
case 'pawn':
return this.generatePawnMoves(position, piece, square);
case 'knight':
return this.generateKnightMoves(position, piece, square);
case 'bishop':
return this.generateBishopMoves(position, piece, square);
case 'rook':
return this.generateRookMoves(position, piece, square);
case 'queen':
return this.generateQueenMoves(position, piece, square);
case 'king':
return this.generateKingMoves(position, piece, square);
default:
return moves;
}
}
generateKnightMoves(position, piece, square) {
const moves = [];
const precomputed = this.knightMoves[square];
for (const targetSquare of precomputed) {
const targetPiece = position.board[targetSquare];
if (!targetPiece || targetPiece.color !== piece.color) {
moves.push({
from: square,
to: targetSquare,
piece: piece,
capture: targetPiece
});
}
}
return moves;
}
// Use typed arrays for better performance
generateSlidingMoves(position, piece, square, directions) {
const moves = [];
const rank = Math.floor(square / 8);
const file = square % 8;
for (const [dr, df] of directions) {
let currentRank = rank + dr;
let currentFile = file + df;
while (currentRank >= 0 && currentRank < 8 && currentFile >= 0 && currentFile < 8) {
const targetSquare = currentRank * 8 + currentFile;
const targetPiece = position.board[targetSquare];
if (targetPiece) {
if (targetPiece.color !== piece.color) {
moves.push({
from: square,
to: targetSquare,
piece: piece,
capture: targetPiece
});
}
break; // Can't continue in this direction
} else {
moves.push({
from: square,
to: targetSquare,
piece: piece,
capture: null
});
}
currentRank += dr;
currentFile += df;
}
}
return moves;
}
}
// shared/components/VirtualMoveList.js
export class VirtualMoveList {
constructor(container, itemHeight = 40, bufferSize = 5) {
this.container = container;
this.itemHeight = itemHeight;
this.bufferSize = bufferSize;
this.scrollTop = 0;
this.items = [];
this.renderedItems = new Map();
this.setupScrollListener();
}
setItems(items) {
this.items = items;
this.updateVirtualList();
}
setupScrollListener() {
let ticking = false;
this.container.addEventListener('scroll', () => {
if (!ticking) {
requestAnimationFrame(() => {
this.handleScroll();
ticking = false;
});
ticking = true;
}
});
}
handleScroll() {
this.scrollTop = this.container.scrollTop;
this.updateVirtualList();
}
updateVirtualList() {
const containerHeight = this.container.clientHeight;
const totalHeight = this.items.length * this.itemHeight;
// Calculate visible range
const startIndex = Math.max(0, Math.floor(this.scrollTop / this.itemHeight) - this.bufferSize);
const endIndex = Math.min(
this.items.length - 1,
Math.ceil((this.scrollTop + containerHeight) / this.itemHeight) + this.bufferSize
);
// Update container height
this.container.style.height = `${totalHeight}px`;
// Remove items outside visible range
for (const [index, element] of this.renderedItems) {
if (index < startIndex || index > endIndex) {
element.remove();
this.renderedItems.delete(index);
}
}
// Add items in visible range
for (let i = startIndex; i <= endIndex; i++) {
if (!this.renderedItems.has(i) && this.items[i]) {
const element = this.createItemElement(this.items[i], i);
this.container.appendChild(element);
this.renderedItems.set(i, element);
}
}
}
createItemElement(item, index) {
const element = document.createElement('div');
element.className = 'move-item';
element.style.cssText = `
position: absolute;
top: ${index * this.itemHeight}px;
height: ${this.itemHeight}px;
width: 100%;
`;
element.innerHTML = this.renderItem(item, index);
return element;
}
renderItem(move, index) {
const moveNumber = Math.floor(index / 2) + 1;
const isWhite = index % 2 === 0;
return `
<div class="move-content">
${isWhite ? `<span class="move-number">${moveNumber}.</span>` : ''}
<span class="move-notation">${move.notation}</span>
<span class="move-time">${move.time || ''}</span>
</div>
`;
}
}
// shared/api/OptimizedApiClient.js
export class OptimizedApiClient {
constructor(baseURL) {
this.baseURL = baseURL;
this.cache = new Map();
this.requestQueue = [];
this.batchTimer = null;
this.retryCount = new Map();
this.maxRetries = 3;
}
// Request batching
batchRequest(requests) {
this.requestQueue.push(...requests);
if (!this.batchTimer) {
this.batchTimer = setTimeout(() => {
this.processBatch();
}, 10); // 10ms batching window
}
}
async processBatch() {
if (this.requestQueue.length === 0) return;
const batch = [...this.requestQueue];
this.requestQueue.length = 0;
this.batchTimer = null;
// Group by endpoint
const grouped = batch.reduce((acc, req) => {
const key = `${req.method}:${req.endpoint}`;
if (!acc[key]) acc[key] = [];
acc[key].push(req);
return acc;
}, {});
// Process each group
await Promise.all(
Object.values(grouped).map(group => this.processRequestGroup(group))
);
}
async processRequestGroup(requests) {
if (requests.length === 1) {
return this.processRequest(requests[0]);
}
// Batch multiple requests of the same type
const batchEndpoint = requests[0].endpoint + '/batch';
const batchData = requests.map(req => req.data);
try {
const response = await fetch(this.baseURL + batchEndpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(batchData)
});
const results = await response.json();
// Resolve individual promises
requests.forEach((req, index) => {
req.resolve(results[index]);
});
} catch (error) {
// Fallback to individual requests
await Promise.all(requests.map(req => this.processRequest(req)));
}
}
// Request deduplication
async request(method, endpoint, data = null, options = {}) {
const cacheKey = `${method}:${endpoint}:${JSON.stringify(data)}`;
// Check cache for GET requests
if (method === 'GET' && this.cache.has(cacheKey)) {
const cached = this.cache.get(cacheKey);
if (Date.now() - cached.timestamp < (options.cacheTime || 5000)) {
return cached.data;
}
}
return new Promise((resolve, reject) => {
const request = {
method,
endpoint,
data,
options,
resolve,
reject,
cacheKey
};
// Add to batch or process immediately
if (options.batch !== false) {
this.batchRequest([request]);
} else {
this.processRequest(request);
}
});
}
async processRequest(request) {
const { method, endpoint, data, options, resolve, reject, cacheKey } = request;
try {
const response = await this.fetchWithRetry(method, endpoint, data, options);
const result = await response.json();
// Cache GET requests
if (method === 'GET') {
this.cache.set(cacheKey, {
data: result,
timestamp: Date.now()
});
}
resolve(result);
} catch (error) {
reject(error);
}
}
async fetchWithRetry(method, endpoint, data, options) {
const url = this.baseURL + endpoint;
const retryKey = `${method}:${endpoint}`;
let attempt = this.retryCount.get(retryKey) || 0;
while (attempt < this.maxRetries) {
try {
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
...options.headers
},
body: data ? JSON.stringify(data) : undefined,
signal: options.signal
});
if (response.ok) {
this.retryCount.delete(retryKey);
return response;
}
if (response.status >= 400 && response.status < 500) {
// Client errors shouldn't be retried
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
} catch (error) {
attempt++;
this.retryCount.set(retryKey, attempt);
if (attempt >= this.maxRetries) {
this.retryCount.delete(retryKey);
throw error;
}
// Exponential backoff
const delay = Math.min(1000 * Math.pow(2, attempt - 1), 10000);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
clearCache() {
this.cache.clear();
}
getCacheStats() {
return {
size: this.cache.size,
entries: Array.from(this.cache.keys())
};
}
}
// Global API client instance
export const apiClient = new OptimizedApiClient('/api');
// shared/websocket/OptimizedWebSocket.js
export class OptimizedWebSocket {
constructor(url, options = {}) {
this.url = url;
this.options = {
reconnectInterval: 1000,
maxReconnectInterval: 30000,
reconnectDecay: 1.5,
maxReconnectAttempts: 10,
binaryType: 'arraybuffer',
...options
};
this.ws = null;
this.reconnectAttempts = 0;
this.messageQueue = [];
this.subscriptions = new Map();
this.isConnected = false;
// Message compression
this.compressionEnabled = options.compression !== false;
this.messageBuffer = [];
this.flushTimer = null;
this.connect();
}
connect() {
try {
this.ws = new WebSocket(this.url);
this.ws.binaryType = this.options.binaryType;
this.ws.onopen = this.handleOpen.bind(this);
this.ws.onmessage = this.handleMessage.bind(this);
this.ws.onclose = this.handleClose.bind(this);
this.ws.onerror = this.handleError.bind(this);
} catch (error) {
this.handleError(error);
}
}
handleOpen() {
this.isConnected = true;
this.reconnectAttempts = 0;
// Send queued messages
while (this.messageQueue.length > 0) {
const message = this.messageQueue.shift();
this.send(message.data, message.options);
}
this.emit('connect');
}
handleMessage(event) {
try {
let data = event.data;
// Handle binary messages
if (data instanceof ArrayBuffer) {
data = this.decompressMessage(data);
} else {
data = JSON.parse(data);
}
// Route to appropriate handler
if (data.type && this.subscriptions.has(data.type)) {
const handlers = this.subscriptions.get(data.type);
handlers.forEach(handler => handler(data));
}
this.emit('message', data);
} catch (error) {
console.error('Error parsing WebSocket message:', error);
}
}
handleClose(event) {
this.isConnected = false;
this.emit('disconnect', event);
if (!event.wasClean && this.reconnectAttempts < this.options.maxReconnectAttempts) {
this.scheduleReconnect();
}
}
handleError(error) {
this.emit('error', error);
}
scheduleReconnect() {
const interval = Math.min(
this.options.reconnectInterval * Math.pow(this.options.reconnectDecay, this.reconnectAttempts),
this.options.maxReconnectInterval
);
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, interval);
}
send(data, options = {}) {
if (!this.isConnected) {
this.messageQueue.push({ data, options });
return;
}
try {
let message = data;
// Compress large messages
if (this.compressionEnabled && JSON.stringify(data).length > 1024) {
message = this.compressMessage(data);
} else {
message = JSON.stringify(data);
}
this.ws.send(message);
} catch (error) {
console.error('Error sending WebSocket message:', error);
}
}
// Message batching for high-frequency updates
batchSend(data, options = {}) {
this.messageBuffer.push(data);
if (!this.flushTimer) {
this.flushTimer = setTimeout(() => {
this.flushMessages();
}, options.batchDelay || 16); // ~60fps
}
}
flushMessages() {
if (this.messageBuffer.length === 0) return;
const batch = {
type: 'batch',
messages: [...this.messageBuffer]
};
this.messageBuffer.length = 0;
this.flushTimer = null;
this.send(batch);
}
subscribe(messageType, handler) {
if (!this.subscriptions.has(messageType)) {
this.subscriptions.set(messageType, new Set());
}
this.subscriptions.get(messageType).add(handler);
return () => {
const handlers = this.subscriptions.get(messageType);
if (handlers) {
handlers.delete(handler);
if (handlers.size === 0) {
this.subscriptions.delete(messageType);
}
}
};
}
compressMessage(data) {
// Simple compression - in practice, use libraries like pako
const json = JSON.stringify(data);
// Placeholder for actual compression
return new TextEncoder().encode(json);
}
decompressMessage(buffer) {
// Placeholder for actual decompression
const json = new TextDecoder().decode(buffer);
return JSON.parse(json);
}
emit(event, data) {
const handlers = this.subscriptions.get(event);
if (handlers) {
handlers.forEach(handler => handler(data));
}
}
close() {
if (this.ws) {
this.ws.close();
}
}
}
// shared/mobile/TouchOptimizer.js
export class TouchOptimizer {
constructor(element) {
this.element = element;
this.isTouch = false;
this.lastTouchTime = 0;
this.touchStartPos = { x: 0, y: 0 };
this.setupTouchHandlers();
}
setupTouchHandlers() {
// Passive listeners for better performance
this.element.addEventListener('touchstart', this.handleTouchStart.bind(this), { passive: true });
this.element.addEventListener('touchmove', this.handleTouchMove.bind(this), { passive: false });
this.element.addEventListener('touchend', this.handleTouchEnd.bind(this), { passive: true });
// Prevent 300ms click delay
this.element.addEventListener('click', this.handleClick.bind(this));
}
handleTouchStart(event) {
this.isTouch = true;
this.lastTouchTime = Date.now();
const touch = event.touches[0];
this.touchStartPos = { x: touch.clientX, y: touch.clientY };
}
handleTouchMove(event) {
const touch = event.touches[0];
const deltaX = Math.abs(touch.clientX - this.touchStartPos.x);
const deltaY = Math.abs(touch.clientY - this.touchStartPos.y);
// Prevent scrolling if user is dragging a piece
if (deltaX > 10 || deltaY > 10) {
event.preventDefault();
}
}
handleTouchEnd(event) {
// Reset touch state after a delay
setTimeout(() => {
this.isTouch = false;
}, 300);
}
handleClick(event) {
// Ignore clicks that come shortly after touch events
if (this.isTouch && Date.now() - this.lastTouchTime < 300) {
event.preventDefault();
event.stopPropagation();
}
}
}
/* Mobile-specific optimizations */
.chess-board {
/* Hardware acceleration */
transform: translateZ(0);
will-change: transform;
/* Prevent selection on mobile */
-webkit-touch-callout: none;
-webkit-user-select: none;
user-select: none;
/* Smooth scrolling */
-webkit-overflow-scrolling: touch;
}
.chess-piece {
/* Optimize animations */
transform: translateZ(0);
will-change: transform, opacity;
/* Prevent flickering during drag */
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
}
/* Touch-friendly sizing */
@media (max-width: 768px) {
.chess-square {
min-height: 44px; /* iOS minimum touch target */
min-width: 44px;
}
.game-controls button {
min-height: 44px;
padding: 12px 16px;
}
}
/* Reduce motion for better performance */
@media (prefers-reduced-motion: reduce) {
.chess-piece {
animation: none !important;
transition: none !important;
}
}
// shared/performance/PerformanceMonitor.js
export class PerformanceMonitor {
constructor() {
this.metrics = new Map();
this.observers = [];
this.isEnabled = true;
this.setupObservers();
}
setupObservers() {
// Performance Observer for various metrics
if ('PerformanceObserver' in window) {
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.recordMetric(entry.name, entry.duration, entry.entryType);
}
});
observer.observe({ entryTypes: ['measure', 'navigation', 'paint'] });
this.observers.push(observer);
}
// Long task observer
if ('PerformanceObserver' in window) {
const longTaskObserver = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
this.recordLongTask(entry);
}
});
longTaskObserver.observe({ entryTypes: ['longtask'] });
this.observers.push(longTaskObserver);
}
// Memory usage monitoring
this.startMemoryMonitoring();
}
recordMetric(name, value, type) {
if (!this.isEnabled) return;
const key = `${type}:${name}`;
if (!this.metrics.has(key)) {
this.metrics.set(key, {
count: 0,
total: 0,
min: Infinity,
max: -Infinity,
values: []
});
}
const metric = this.metrics.get(key);
metric.count++;
metric.total += value;
metric.min = Math.min(metric.min, value);
metric.max = Math.max(metric.max, value);
metric.values.push({ value, timestamp: Date.now() });
// Keep only last 100 values
if (metric.values.length > 100) {
metric.values = metric.values.slice(-100);
}
}
recordLongTask(entry) {
console.warn('Long task detected:', {
duration: entry.duration,
startTime: entry.startTime,
name: entry.name
});
this.recordMetric('long-task', entry.duration, 'longtask');
}
startMemoryMonitoring() {
if (!('memory' in performance)) return;
setInterval(() => {
const memory = performance.memory;
this.recordMetric('memory-used', memory.usedJSHeapSize, 'memory');
this.recordMetric('memory-total', memory.totalJSHeapSize, 'memory');
this.recordMetric('memory-limit', memory.jsHeapSizeLimit, 'memory');
}, 5000); // Check every 5 seconds
}
measureFunction(name, fn) {
return (...args) => {
const start = performance.now();
try {
const result = fn.apply(this, args);
if (result && typeof result.then === 'function') {
// Handle async functions
return result.finally(() => {
const duration = performance.now() - start;
this.recordMetric(name, duration, 'function');
});
} else {
const duration = performance.now() - start;
this.recordMetric(name, duration, 'function');
return result;
}
} catch (error) {
const duration = performance.now() - start;
this.recordMetric(name, duration, 'function');
throw error;
}
};
}
getMetrics() {
const result = {};
for (const [key, metric] of this.metrics) {
result[key] = {
count: metric.count,
average: metric.total / metric.count,
min: metric.min,
max: metric.max,
total: metric.total
};
}
return result;
}
getWebVitals() {
return new Promise((resolve) => {
const vitals = {};
// Largest Contentful Paint
new PerformanceObserver((list) => {
const entries = list.getEntries();
const lastEntry = entries[entries.length - 1];
vitals.lcp = lastEntry.renderTime || lastEntry.loadTime;
checkComplete();
}).observe({ entryTypes: ['largest-contentful-paint'] });
// First Input Delay
new PerformanceObserver((list) => {
const firstEntry = list.getEntries()[0];
vitals.fid = firstEntry.processingStart - firstEntry.startTime;
checkComplete();
}).observe({ entryTypes: ['first-input'] });
// Cumulative Layout Shift
let clsValue = 0;
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value;
}
}
vitals.cls = clsValue;
checkComplete();
}).observe({ entryTypes: ['layout-shift'] });
function checkComplete() {
if (vitals.lcp && vitals.fid && vitals.cls !== undefined) {
resolve(vitals);
}
}
// Timeout after 10 seconds
setTimeout(() => resolve(vitals), 10000);
});
}
exportMetrics() {
return {
timestamp: Date.now(),
url: window.location.href,
userAgent: navigator.userAgent,
metrics: this.getMetrics(),
webVitals: this.getWebVitals()
};
}
clear() {
this.metrics.clear();
}
disable() {
this.isEnabled = false;
this.observers.forEach(observer => observer.disconnect());
this.observers = [];
}
}
// Global performance monitor
export const performanceMonitor = new PerformanceMonitor();
// Convenience decorators
export function measurePerformance(name) {
return function(target, propertyKey, descriptor) {
const originalMethod = descriptor.value;
descriptor.value = performanceMonitor.measureFunction(`${target.constructor.name}.${propertyKey}`, originalMethod);
return descriptor;
};
}
# Optimized Dockerfile for production
FROM node:18-alpine AS base
WORKDIR /app
RUN apk add --no-cache libc6-compat
# Dependencies stage
FROM base AS deps
COPY package*.json ./
RUN npm ci --only=production --ignore-scripts && npm cache clean --force
# Build stage
FROM base AS builder
COPY package*.json ./
RUN npm ci --ignore-scripts
COPY . .
RUN npm run build
# Production stage
FROM node:18-alpine AS runner
WORKDIR /app
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/public ./public
USER nextjs
EXPOSE 3000
ENV PORT 3000
CMD ["npm", "start"]
# nginx.conf for production
server {
listen 80;
server_name your-domain.com;
# Enable gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types
text/plain
text/css
text/xml
text/javascript
application/javascript
application/xml+rss
application/json;
# Enable Brotli compression (if available)
brotli on;
brotli_comp_level 6;
brotli_types
text/plain
text/css
application/javascript
application/json
image/svg+xml;
# Static file caching
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ {
expires 1y;
add_header Cache-Control "public, immutable";
add_header Vary "Accept-Encoding";
}
# API proxy
location /api/ {
proxy_pass http://backend:8080/;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
# Connection pooling
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
}
# Frontend files
location / {
try_files $uri $uri/ /index.html;
add_header Cache-Control "no-cache, must-revalidate";
}
}
- Security - Security optimization and best practices
- Monitoring - Application monitoring and observability
- Deployment Guide - Production deployment strategies