diff --git a/backend/optimize-database-indexes.sql b/backend/optimize-database-indexes.sql new file mode 100644 index 0000000..6bd36c2 --- /dev/null +++ b/backend/optimize-database-indexes.sql @@ -0,0 +1,31 @@ +-- Database Performance Optimization: Additional Indexes +-- Focus: Speed up frequently queried fields + +-- Products: Add missing indexes for common queries +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_products_slug ON products(slug) WHERE isactive = true; +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_products_active_featured ON products(isactive, isfeatured) WHERE isactive = true AND isfeatured = true; +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_products_active_category ON products(isactive, category) WHERE isactive = true; + +-- Blog: Add slug index for single post queries +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_blogposts_slug ON blogposts(slug) WHERE ispublished = true; +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_blogposts_published ON blogposts(ispublished, createdat DESC) WHERE ispublished = true; + +-- Portfolio: Add composite index for common query patterns +CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_portfolio_active_display ON portfolioprojects(isactive, displayorder, createdat DESC) WHERE isactive = true; + +-- Analyze tables to update statistics for query planner +ANALYZE products; +ANALYZE product_images; +ANALYZE blogposts; +ANALYZE portfolioprojects; +ANALYZE pages; + +-- Add database-level connection pooling settings for better performance +ALTER DATABASE skyartshop SET random_page_cost = 1.1; -- SSD optimization +ALTER DATABASE skyartshop SET effective_cache_size = '2GB'; -- Assume 2GB available for caching +ALTER DATABASE skyartshop SET shared_buffers = '512MB'; -- Increase shared buffer +ALTER DATABASE skyartshop SET work_mem = '16MB'; -- Increase work memory for sorting +ALTER DATABASE skyartshop SET maintenance_work_mem = '128MB'; -- For VACUUM and CREATE INDEX + +-- Vacuum analyze to reclaim space and update statistics +VACUUM ANALYZE; diff --git a/test-api-performance.sh b/test-api-performance.sh new file mode 100755 index 0000000..e6b3815 --- /dev/null +++ b/test-api-performance.sh @@ -0,0 +1,91 @@ +#!/bin/bash + +echo "=== Testing API Performance and Frontend-Backend Communication ===" +echo "" + +# Test endpoints with timing +test_endpoint() { + local endpoint="$1" + local description="$2" + + echo "Testing: $description" + echo "Endpoint: $endpoint" + + # First request (cold) + start=$(date +%s%N) + status=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:5000$endpoint") + end=$(date +%s%N) + time_ms=$(( (end - start) / 1000000 )) + + echo " ├─ HTTP Status: $status" + echo " ├─ Response Time (cold): ${time_ms}ms" + + # Second request (should be cached on backend) + start=$(date +%s%N) + status2=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:5000$endpoint") + end=$(date +%s%N) + time_ms2=$(( (end - start) / 1000000 )) + + echo " ├─ Response Time (warm): ${time_ms2}ms" + + # Calculate improvement + if [ $time_ms -gt 0 ]; then + improvement=$(( (time_ms - time_ms2) * 100 / time_ms )) + echo " └─ Performance gain: ${improvement}%" + fi + + echo "" +} + +# Test all main endpoints +test_endpoint "/api/products" "All Products" +test_endpoint "/api/products/featured?limit=4" "Featured Products" +test_endpoint "/api/categories" "Categories" +test_endpoint "/api/portfolio/projects" "Portfolio Projects" +test_endpoint "/api/blog/posts" "Blog Posts" +test_endpoint "/api/pages" "Custom Pages" + +# Test page loading +echo "=== Testing Page Load Times ===" +echo "" + +test_page() { + local page="$1" + local description="$2" + + echo "Testing: $description" + echo "Page: $page" + + start=$(date +%s%N) + status=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:5000$page") + end=$(date +%s%N) + time_ms=$(( (end - start) / 1000000 )) + + echo " ├─ HTTP Status: $status" + echo " └─ Load Time: ${time_ms}ms" + echo "" +} + +test_page "/home" "Home Page" +test_page "/shop" "Shop Page" +test_page "/portfolio" "Portfolio Page" +test_page "/blog" "Blog Page" + +echo "=== Database Index Verification ===" +PGPASSWORD=SkyArt2025Pass psql -h localhost -U skyartapp -d skyartshop -t -c " +SELECT + schemaname, + tablename, + indexname +FROM pg_indexes +WHERE schemaname = 'public' + AND indexname LIKE 'idx_%' +ORDER BY tablename, indexname; +" 2>/dev/null | head -30 + +echo "" +echo "=== Cache Statistics (if available) ===" +echo "Backend cache is enabled with middleware" +echo "" + +echo "✅ Performance testing complete!" diff --git a/website/public/assets/js/api-cache.js b/website/public/assets/js/api-cache.js new file mode 100644 index 0000000..a65d188 --- /dev/null +++ b/website/public/assets/js/api-cache.js @@ -0,0 +1,205 @@ +/** + * API Cache and Request Deduplication Manager + * Optimizes frontend-backend communication by: + * - Caching API responses + * - Deduplicating simultaneous requests + * - Reducing unnecessary network traffic + */ + +class APICache { + constructor() { + this.cache = new Map(); + this.pendingRequests = new Map(); + this.defaultTTL = 5 * 60 * 1000; // 5 minutes default + + // Custom TTL for different endpoints + this.ttlConfig = { + '/api/products': 5 * 60 * 1000, // 5 min + '/api/products/featured': 10 * 60 * 1000, // 10 min + '/api/categories': 30 * 60 * 1000, // 30 min + '/api/portfolio/projects': 10 * 60 * 1000, // 10 min + '/api/blog/posts': 5 * 60 * 1000, // 5 min + '/api/pages': 10 * 60 * 1000, // 10 min + }; + + // Start periodic cleanup + this.startCleanup(); + } + + /** + * Get cache key from URL + */ + getCacheKey(url) { + return url; + } + + /** + * Get TTL for specific endpoint + */ + getTTL(url) { + // Match base endpoint + for (const [endpoint, ttl] of Object.entries(this.ttlConfig)) { + if (url.startsWith(endpoint)) { + return ttl; + } + } + return this.defaultTTL; + } + + /** + * Check if cache entry is valid + */ + isValid(entry) { + return entry && Date.now() - entry.timestamp < entry.ttl; + } + + /** + * Get from cache + */ + get(url) { + const key = this.getCacheKey(url); + const entry = this.cache.get(key); + + if (this.isValid(entry)) { + console.log(`[Cache] HIT: ${url}`); + return entry.data; + } + + console.log(`[Cache] MISS: ${url}`); + return null; + } + + /** + * Set cache entry + */ + set(url, data) { + const key = this.getCacheKey(url); + const ttl = this.getTTL(url); + + this.cache.set(key, { + data, + timestamp: Date.now(), + ttl + }); + + console.log(`[Cache] SET: ${url} (TTL: ${ttl}ms)`); + } + + /** + * Clear specific cache entry + */ + clear(url) { + const key = this.getCacheKey(url); + this.cache.delete(key); + console.log(`[Cache] CLEAR: ${url}`); + } + + /** + * Clear all cache + */ + clearAll() { + this.cache.clear(); + console.log('[Cache] CLEARED ALL'); + } + + /** + * Fetch with caching and deduplication + */ + async fetch(url, options = {}) { + // Only cache GET requests + if (options.method && options.method !== 'GET') { + return fetch(url, options); + } + + // Check cache first + const cached = this.get(url); + if (cached) { + return { + json: async () => cached, + ok: true, + status: 200, + fromCache: true + }; + } + + // Check if request is already pending + if (this.pendingRequests.has(url)) { + console.log(`[Cache] DEDUP: ${url} - Waiting for pending request`); + return this.pendingRequests.get(url); + } + + // Make new request + console.log(`[Cache] FETCH: ${url}`); + const requestPromise = fetch(url, options) + .then(async response => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const data = await response.json(); + + // Cache successful response + this.set(url, data); + + // Remove from pending + this.pendingRequests.delete(url); + + return { + json: async () => data, + ok: true, + status: response.status, + fromCache: false + }; + }) + .catch(error => { + // Remove from pending on error + this.pendingRequests.delete(url); + throw error; + }); + + // Store as pending + this.pendingRequests.set(url, requestPromise); + + return requestPromise; + } + + /** + * Periodic cleanup of expired entries + */ + startCleanup() { + setInterval(() => { + let removed = 0; + for (const [key, entry] of this.cache.entries()) { + if (!this.isValid(entry)) { + this.cache.delete(key); + removed++; + } + } + if (removed > 0) { + console.log(`[Cache] Cleanup: Removed ${removed} expired entries`); + } + }, 60000); // Run every minute + } + + /** + * Get cache statistics + */ + getStats() { + return { + size: this.cache.size, + pending: this.pendingRequests.size, + entries: Array.from(this.cache.entries()).map(([key, entry]) => ({ + url: key, + age: Date.now() - entry.timestamp, + ttl: entry.ttl, + valid: this.isValid(entry) + })) + }; + } +} + +// Create global instance +window.apiCache = new APICache(); + +// Expose cache clearing for manual control +window.clearAPICache = () => window.apiCache.clearAll(); diff --git a/website/public/blog.html b/website/public/blog.html index 14cd327..34c06b3 100644 --- a/website/public/blog.html +++ b/website/public/blog.html @@ -254,11 +254,12 @@ + @@ -484,7 +485,7 @@ // Load featured products async function loadFeaturedProducts() { try { - const response = await fetch("/api/products/featured?limit=4"); + const response = await window.apiCache.fetch("/api/products/featured?limit=4"); if (response.ok) { const data = await response.json(); if (data.products && data.products.length > 0) { diff --git a/website/public/portfolio.html b/website/public/portfolio.html index f69149b..07e008a 100644 --- a/website/public/portfolio.html +++ b/website/public/portfolio.html @@ -325,6 +325,7 @@ + @@ -258,7 +259,7 @@ "Fetching product from API:", `/api/products/${productId}` ); - const response = await fetch(`/api/products/${productId}`); + const response = await window.apiCache.fetch(`/api/products/${productId}`); const data = await response.json(); console.log("API response:", data); @@ -702,7 +703,7 @@ '
Loading recommendations...
'; // Fetch products from same category - const response = await fetch("/api/products"); + const response = await window.apiCache.fetch("/api/products"); const data = await response.json(); if (data.success && data.products) { diff --git a/website/public/shop.html b/website/public/shop.html index 5a066b4..003ef23 100644 --- a/website/public/shop.html +++ b/website/public/shop.html @@ -864,6 +864,7 @@ + @@ -960,7 +961,7 @@ async function loadProducts() { try { console.log("Shop page: Loading products from API..."); - const response = await fetch("/api/products"); + const response = await window.apiCache.fetch("/api/products"); const data = await response.json(); console.log("Shop page: API response:", data);