--- id: "ET-TRD-008" title: "Especificación Técnica - Performance Optimizations" type: "Technical Specification" status: "Done" priority: "Alta" epic: "OQI-003" project: "trading-platform" version: "1.0.0" created_date: "2025-12-05" updated_date: "2026-01-04" --- # ET-TRD-008: Especificación Técnica - Performance Optimizations **Version:** 1.0.0 **Fecha:** 2025-12-05 **Estado:** Pendiente **Épica:** [OQI-003](../_MAP.md) **Requerimiento:** RF-TRD-008 --- ## Resumen Esta especificación detalla las optimizaciones de performance para el módulo de trading, incluyendo Web Workers para cálculos pesados, virtualización de listas, caché estratégico, lazy loading de componentes y optimización de re-renders. --- ## Arquitectura ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ FRONTEND OPTIMIZATIONS │ │ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Component Layer │ │ │ │ ┌────────────────┐ ┌──────────────────┐ ┌────────────────┐ │ │ │ │ │ React.memo() │ │ useMemo() │ │ useCallback() │ │ │ │ │ │ Components │ │ Computations │ │ Functions │ │ │ │ │ └────────────────┘ └──────────────────┘ └────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Virtualization Layer │ │ │ │ ┌────────────────┐ ┌──────────────────┐ ┌────────────────┐ │ │ │ │ │ react-window │ │ Intersection │ │ Infinite │ │ │ │ │ │ Virtual Lists │ │ Observer │ │ Scroll │ │ │ │ │ └────────────────┘ └──────────────────┘ └────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Web Workers │ │ │ │ ┌────────────────┐ ┌──────────────────┐ ┌────────────────┐ │ │ │ │ │ Indicator │ │ Data │ │ Chart │ │ │ │ │ │ Calculations │ │ Processing │ │ Rendering │ │ │ │ │ └────────────────┘ └──────────────────┘ └────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ Cache Layer │ │ │ │ ┌────────────────┐ ┌──────────────────┐ ┌────────────────┐ │ │ │ │ │ IndexedDB │ │ Memory Cache │ │ Service │ │ │ │ │ │ Storage │ │ (Map/LRU) │ │ Worker │ │ │ │ │ └────────────────┘ └──────────────────┘ └────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ BACKEND OPTIMIZATIONS │ │ ┌──────────────────────────────────────────────────────────────────┐ │ │ │ ┌────────────────┐ ┌──────────────────┐ ┌────────────────┐ │ │ │ │ │ Redis Cache │ │ Database │ │ Query │ │ │ │ │ │ (Hot Data) │ │ Indexing │ │ Optimization │ │ │ │ │ └────────────────┘ └──────────────────┘ └────────────────┘ │ │ │ └──────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ ``` --- ## 1. Web Workers ### Indicator Calculation Worker **Ubicación:** `apps/frontend/src/modules/trading/workers/indicator.worker.ts` ```typescript import { IndicatorService } from '../services/indicator.service'; interface WorkerMessage { id: string; type: 'calculate' | 'batch'; indicatorType?: string; data?: any[]; params?: any; batch?: Array<{ indicatorType: string; data: any[]; params: any; }>; } interface WorkerResponse { id: string; success: boolean; data?: any; error?: string; } // Handle messages from main thread self.onmessage = (event: MessageEvent) => { const { id, type, indicatorType, data, params, batch } = event.data; try { if (type === 'calculate') { const result = IndicatorService.calculate(indicatorType!, data!, params!); self.postMessage({ id, success: true, data: result, } as WorkerResponse); } else if (type === 'batch') { const results = batch!.map((item) => IndicatorService.calculate(item.indicatorType, item.data, item.params) ); self.postMessage({ id, success: true, data: results, } as WorkerResponse); } } catch (error: any) { self.postMessage({ id, success: false, error: error.message, } as WorkerResponse); } }; ``` ### Worker Hook **Ubicación:** `apps/frontend/src/modules/trading/hooks/useWorker.ts` ```typescript import { useRef, useCallback, useEffect } from 'react'; interface WorkerPool { workers: Worker[]; queue: Array<{ id: string; message: any; resolve: (value: any) => void; reject: (error: any) => void; }>; activeWorkers: number; } export function useWorkerPool(workerCount: number = 4) { const poolRef = useRef({ workers: [], queue: [], activeWorkers: 0, }); const pendingRequests = useRef void; reject: (error: any) => void; }>>(new Map()); // Initialize worker pool useEffect(() => { for (let i = 0; i < workerCount; i++) { const worker = new Worker( new URL('../workers/indicator.worker.ts', import.meta.url), { type: 'module' } ); worker.onmessage = (event) => { const { id, success, data, error } = event.data; const request = pendingRequests.current.get(id); if (request) { if (success) { request.resolve(data); } else { request.reject(new Error(error)); } pendingRequests.current.delete(id); } poolRef.current.activeWorkers--; processQueue(); }; worker.onerror = (error) => { console.error('Worker error:', error); }; poolRef.current.workers.push(worker); } return () => { poolRef.current.workers.forEach((worker) => worker.terminate()); }; }, [workerCount]); const processQueue = useCallback(() => { const pool = poolRef.current; while ( pool.queue.length > 0 && pool.activeWorkers < pool.workers.length ) { const task = pool.queue.shift()!; const workerIndex = pool.activeWorkers % pool.workers.length; const worker = pool.workers[workerIndex]; pendingRequests.current.set(task.id, { resolve: task.resolve, reject: task.reject, }); worker.postMessage(task.message); pool.activeWorkers++; } }, []); const execute = useCallback((message: any): Promise => { return new Promise((resolve, reject) => { const id = `${Date.now()}_${Math.random()}`; const task = { id, message: { ...message, id }, resolve, reject, }; poolRef.current.queue.push(task); processQueue(); }); }, [processQueue]); return { execute }; } ``` **Uso:** ```typescript function ChartComponent() { const { execute } = useWorkerPool(4); const { klines } = useTradingStore(); const calculateIndicators = useCallback(async () => { const results = await execute({ type: 'batch', batch: [ { indicatorType: 'SMA', data: klines, params: { period: 20 }, }, { indicatorType: 'RSI', data: klines, params: { period: 14 }, }, ], }); return results; }, [klines, execute]); // Use results... } ``` --- ## 2. Virtualización de Listas ### Order List con react-window **Ubicación:** `apps/frontend/src/modules/trading/components/VirtualizedOrderList.tsx` ```typescript import React from 'react'; import { FixedSizeList as List } from 'react-window'; import AutoSizer from 'react-virtualized-auto-sizer'; import { PaperOrder } from '../types'; interface Props { orders: PaperOrder[]; onOrderClick?: (order: PaperOrder) => void; } export const VirtualizedOrderList: React.FC = ({ orders, onOrderClick, }) => { const Row = ({ index, style }: { index: number; style: React.CSSProperties }) => { const order = orders[index]; return (
onOrderClick?.(order)} >
); }; return ( {({ height, width }) => ( {Row} )} ); }; const OrderRow = React.memo(({ order }: { order: PaperOrder }) => { return (
{order.symbol}
{order.side.toUpperCase()} {order.type}
{order.quantity}
{order.status}
); }); ``` ### Infinite Scroll para Trades ```typescript import React, { useRef, useCallback } from 'react'; import { useInfiniteQuery } from '@tanstack/react-query'; import { api } from '../services/api'; export const InfiniteTradeList: React.FC = () => { const observerRef = useRef(); const lastElementRef = useRef(null); const { data, fetchNextPage, hasNextPage, isFetchingNextPage, } = useInfiniteQuery({ queryKey: ['trades'], queryFn: ({ pageParam = 0 }) => api.get('/paper/trades', { params: { limit: 50, offset: pageParam }, }), getNextPageParam: (lastPage, pages) => { if (lastPage.data.pagination.hasMore) { return pages.length * 50; } return undefined; }, }); // Intersection Observer para infinite scroll const lastElementObserver = useCallback( (node: HTMLDivElement | null) => { if (isFetchingNextPage) return; if (observerRef.current) { observerRef.current.disconnect(); } observerRef.current = new IntersectionObserver((entries) => { if (entries[0].isIntersecting && hasNextPage) { fetchNextPage(); } }); if (node) { observerRef.current.observe(node); } }, [isFetchingNextPage, fetchNextPage, hasNextPage] ); const trades = data?.pages.flatMap((page) => page.data.data) ?? []; return (
{trades.map((trade, index) => (
))} {isFetchingNextPage && (
Loading more...
)}
); }; ``` --- ## 3. Cache Estratégico ### IndexedDB Cache **Ubicación:** `apps/frontend/src/modules/trading/services/indexeddb-cache.ts` ```typescript import { openDB, DBSchema, IDBPDatabase } from 'idb'; interface TradingDB extends DBSchema { klines: { key: string; // symbol:interval value: { symbol: string; interval: string; data: any[]; timestamp: number; }; }; symbols: { key: string; value: { data: any[]; timestamp: number; }; }; } class IndexedDBCache { private db: IDBPDatabase | null = null; private readonly DB_NAME = 'trading-cache'; private readonly DB_VERSION = 1; async initialize() { if (this.db) return; this.db = await openDB(this.DB_NAME, this.DB_VERSION, { upgrade(db) { // Create object stores if (!db.objectStoreNames.contains('klines')) { db.createObjectStore('klines', { keyPath: 'key' }); } if (!db.objectStoreNames.contains('symbols')) { db.createObjectStore('symbols', { keyPath: 'key' }); } }, }); } async getKlines(symbol: string, interval: string) { if (!this.db) await this.initialize(); const key = `${symbol}:${interval}`; const cached = await this.db!.get('klines', key); if (!cached) return null; // Check if cache is still valid (5 minutes) const now = Date.now(); const cacheAge = now - cached.timestamp; const maxAge = 5 * 60 * 1000; // 5 minutes if (cacheAge > maxAge) { await this.db!.delete('klines', key); return null; } return cached.data; } async setKlines(symbol: string, interval: string, data: any[]) { if (!this.db) await this.initialize(); const key = `${symbol}:${interval}`; await this.db!.put('klines', { key, symbol, interval, data, timestamp: Date.now(), }); } async clearExpired() { if (!this.db) await this.initialize(); const now = Date.now(); const maxAge = 5 * 60 * 1000; // Clear expired klines const tx = this.db!.transaction('klines', 'readwrite'); const store = tx.objectStore('klines'); const allKeys = await store.getAllKeys(); for (const key of allKeys) { const item = await store.get(key); if (item && now - item.timestamp > maxAge) { await store.delete(key); } } await tx.done; } } export const indexedDBCache = new IndexedDBCache(); ``` ### Memory Cache (LRU) ```typescript // services/lru-cache.ts class LRUCache { private cache: Map; private maxSize: number; constructor(maxSize: number = 100) { this.cache = new Map(); this.maxSize = maxSize; } get(key: K): V | undefined { const value = this.cache.get(key); if (value !== undefined) { // Move to end (most recently used) this.cache.delete(key); this.cache.set(key, value); } return value; } set(key: K, value: V): void { // Delete if exists to re-add at end this.cache.delete(key); // Add to end this.cache.set(key, value); // Remove oldest if over capacity if (this.cache.size > this.maxSize) { const firstKey = this.cache.keys().next().value; this.cache.delete(firstKey); } } has(key: K): boolean { return this.cache.has(key); } clear(): void { this.cache.clear(); } get size(): number { return this.cache.size; } } export const tickerCache = new LRUCache(50); export const orderBookCache = new LRUCache(20); ``` --- ## 4. Component Optimizations ### React.memo y useMemo ```typescript // components/OptimizedOrderPanel.tsx import React, { useMemo, useCallback } from 'react'; export const OptimizedOrderPanel = React.memo(({ symbol, ticker, balance }) => { // Memoize expensive calculations const estimatedCost = useMemo(() => { return calculateEstimatedCost(orderQuantity, orderPrice, ticker?.price); }, [orderQuantity, orderPrice, ticker?.price]); const maxQuantity = useMemo(() => { return calculateMaxQuantity(balance.available, ticker?.price); }, [balance.available, ticker?.price]); // Memoize callbacks const handleSubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault(); // Submit logic }, [orderQuantity, orderPrice, symbol] ); return (
{/* Component JSX */}
); }, (prevProps, nextProps) => { // Custom comparison return ( prevProps.symbol === nextProps.symbol && prevProps.ticker?.price === nextProps.ticker?.price && prevProps.balance.available === nextProps.balance.available ); }); ``` ### Debouncing e Input Throttling ```typescript // hooks/useDebounce.ts import { useEffect, useState } from 'react'; export function useDebounce(value: T, delay: number = 300): T { const [debouncedValue, setDebouncedValue] = useState(value); useEffect(() => { const handler = setTimeout(() => { setDebouncedValue(value); }, delay); return () => { clearTimeout(handler); }; }, [value, delay]); return debouncedValue; } // Usage function SearchSymbol() { const [search, setSearch] = useState(''); const debouncedSearch = useDebounce(search, 500); useEffect(() => { if (debouncedSearch) { // API call only after 500ms of no typing searchSymbols(debouncedSearch); } }, [debouncedSearch]); return ( setSearch(e.target.value)} placeholder="Search symbols..." /> ); } ``` --- ## 5. Lazy Loading ### Code Splitting ```typescript // pages/TradingPage.tsx import React, { lazy, Suspense } from 'react'; // Lazy load heavy components const ChartComponent = lazy(() => import('../components/ChartComponent')); const OrderBookPanel = lazy(() => import('../components/OrderBookPanel')); const PositionsPanel = lazy(() => import('../components/PositionsPanel')); export const TradingPage: React.FC = () => { return (
}>
}> }>
); }; ``` ### Dynamic Imports ```typescript // Lazy load indicators async function loadIndicator(type: string) { switch (type) { case 'SMA': return (await import('../indicators/sma')).calculateSMA; case 'RSI': return (await import('../indicators/rsi')).calculateRSI; case 'MACD': return (await import('../indicators/macd')).calculateMACD; default: throw new Error(`Unknown indicator: ${type}`); } } ``` --- ## 6. Backend Optimizations ### Database Query Optimization ```sql -- Create covering indexes for common queries CREATE INDEX idx_paper_orders_user_status_symbol ON trading.paper_orders(user_id, status, symbol) INCLUDE (quantity, price, placed_at); -- Materialized view for position summary CREATE MATERIALIZED VIEW trading.position_summary AS SELECT user_id, symbol, COUNT(*) as total_positions, SUM(CASE WHEN status = 'open' THEN 1 ELSE 0 END) as open_positions, SUM(realized_pnl) as total_realized_pnl, SUM(unrealized_pnl) as total_unrealized_pnl FROM trading.paper_positions GROUP BY user_id, symbol; -- Refresh periodically CREATE OR REPLACE FUNCTION refresh_position_summary() RETURNS void AS $$ BEGIN REFRESH MATERIALIZED VIEW CONCURRENTLY trading.position_summary; END; $$ LANGUAGE plpgsql; ``` ### Connection Pooling ```typescript // db/pool.ts import { Pool } from 'pg'; export const pool = new Pool({ host: process.env.DB_HOST, port: parseInt(process.env.DB_PORT || '5432'), database: process.env.DB_NAME, user: process.env.DB_USER, password: process.env.DB_PASSWORD, max: 20, // Maximum pool size idleTimeoutMillis: 30000, connectionTimeoutMillis: 2000, }); // Prepared statements for frequently used queries export const preparedStatements = { getBalance: { name: 'get-balance', text: 'SELECT * FROM trading.paper_balances WHERE user_id = $1 AND asset = $2', }, getOpenPosition: { name: 'get-open-position', text: ` SELECT * FROM trading.paper_positions WHERE user_id = $1 AND symbol = $2 AND status = 'open' LIMIT 1 `, }, }; ``` --- ## 7. Performance Monitoring ### React DevTools Profiler ```typescript import { Profiler, ProfilerOnRenderCallback } from 'react'; const onRenderCallback: ProfilerOnRenderCallback = ( id, phase, actualDuration, baseDuration, startTime, commitTime ) => { if (actualDuration > 16) { // Log slow renders (> 1 frame at 60fps) console.warn(`Slow render in ${id}:`, { phase, actualDuration, baseDuration, }); } }; export function TradingPageWithProfiler() { return ( ); } ``` ### Performance Metrics ```typescript // services/performance.ts export class PerformanceMonitor { static measureRender(componentName: string, callback: () => void) { const startTime = performance.now(); callback(); const endTime = performance.now(); const duration = endTime - startTime; if (duration > 16) { console.warn(`${componentName} took ${duration.toFixed(2)}ms to render`); } return duration; } static measureAsync(name: string, promise: Promise) { const startTime = performance.now(); return promise.finally(() => { const endTime = performance.now(); const duration = endTime - startTime; console.log(`${name} took ${duration.toFixed(2)}ms`); }); } static markNavigation(name: string) { performance.mark(name); } static measureNavigation(start: string, end: string) { performance.measure(`${start} -> ${end}`, start, end); const measure = performance.getEntriesByName(`${start} -> ${end}`)[0]; console.log(`Navigation took ${measure.duration.toFixed(2)}ms`); } } ``` --- ## 8. Bundle Optimization ### Webpack Configuration ```javascript // webpack.config.js module.exports = { optimization: { splitChunks: { chunks: 'all', cacheGroups: { // Vendor libraries vendor: { test: /[\\/]node_modules[\\/]/, name: 'vendors', priority: 10, }, // Trading module trading: { test: /[\\/]src[\\/]modules[\\/]trading[\\/]/, name: 'trading', priority: 5, }, // Lightweight Charts (large library) charts: { test: /[\\/]node_modules[\\/]lightweight-charts[\\/]/, name: 'charts', priority: 15, }, }, }, runtimeChunk: 'single', }, }; ``` --- ## Performance Checklist ```markdown ## Frontend Performance - [x] Use React.memo for pure components - [x] Implement useMemo for expensive calculations - [x] Use useCallback for event handlers - [x] Virtualize long lists (react-window) - [x] Lazy load routes and heavy components - [x] Implement Web Workers for calculations - [x] Use IndexedDB for offline caching - [x] Debounce user inputs - [x] Optimize bundle size (code splitting) - [x] Use production builds ## Backend Performance - [x] Database indexing on frequent queries - [x] Connection pooling - [x] Redis caching for hot data - [x] Prepared statements - [x] Query result pagination - [x] Materialized views for aggregations - [x] Rate limiting - [x] Compression (gzip/brotli) ## Monitoring - [x] React Profiler for render times - [x] Performance.mark() for navigation - [x] API response time logging - [x] Error tracking (Sentry) - [x] Web Vitals tracking ``` --- ## Testing ```typescript describe('Performance Tests', () => { it('should render large order list in under 100ms', () => { const orders = generateMockOrders(1000); const startTime = performance.now(); render(); const endTime = performance.now(); expect(endTime - startTime).toBeLessThan(100); }); it('should calculate indicators in Web Worker', async () => { const klines = generateMockKlines(1000); const { execute } = renderHook(() => useWorkerPool(1)).result.current; const startTime = performance.now(); await execute({ type: 'calculate', indicatorType: 'SMA', data: klines, params: { period: 20 }, }); const endTime = performance.now(); expect(endTime - startTime).toBeLessThan(50); }); }); ``` --- ## Referencias - [React Performance Optimization](https://react.dev/learn/render-and-commit) - [Web Workers API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API) - [IndexedDB API](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) - [react-window Documentation](https://github.com/bvaughn/react-window) - [PostgreSQL Performance Tips](https://wiki.postgresql.org/wiki/Performance_Optimization)