diff --git a/client/src/components/search-header.tsx b/client/src/components/search-header.tsx index 9679073..84d2775 100644 --- a/client/src/components/search-header.tsx +++ b/client/src/components/search-header.tsx @@ -26,11 +26,14 @@ export default function SearchHeader({ }; }, []); - // Debounced search function - waits 500ms after user stops typing + // Optimized debounced search - waits 300ms and filters short queries const debouncedSearch = useCallback( debounce((query: string) => { - onSearch(query); - }, 500), + // Only search if query is meaningful (2+ characters or empty for reset) + if (query.length === 0 || query.length >= 2) { + onSearch(query); + } + }, 300), [onSearch, debounce] ); diff --git a/client/src/pages/home.tsx b/client/src/pages/home.tsx index 1ed2fb7..01793dd 100644 --- a/client/src/pages/home.tsx +++ b/client/src/pages/home.tsx @@ -19,7 +19,7 @@ export default function Home() { - // Fetch videos + // Fetch videos with optimized caching const { data: videosResponse, isLoading, refetch } = useQuery({ queryKey: ["/api/videos", { limit: 20, @@ -41,7 +41,11 @@ export default function Home() { throw new Error('Failed to fetch videos'); } return response.json(); - } + }, + staleTime: 5 * 60 * 1000, // 5 minutes + gcTime: 10 * 60 * 1000, // 10 minutes + refetchOnWindowFocus: false, + refetchOnReconnect: false }); // Update videos when new data comes in diff --git a/package-lock.json b/package-lock.json index 99c54e3..6644db4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "compression": "^1.8.1", "connect-pg-simple": "^10.0.0", "date-fns": "^3.6.0", "drizzle-orm": "^0.39.1", @@ -5419,6 +5420,60 @@ "node": ">= 6" } }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "license": "MIT", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "license": "MIT", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/concat-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz", diff --git a/package.json b/package.json index fa6e924..860b28d 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "compression": "^1.8.1", "connect-pg-simple": "^10.0.0", "date-fns": "^3.6.0", "drizzle-orm": "^0.39.1", diff --git a/replit.md b/replit.md index e22e93b..0725b6e 100644 --- a/replit.md +++ b/replit.md @@ -28,6 +28,8 @@ go4.video is a fully functional professional video streaming platform with a com - ✅ **VAST Ad Integration**: Comprehensive VAST advertising system with waterfall monetization strategy supporting Publift, Vdo.ai, Primis, AdPlayer.Pro, and Aniview - ✅ **Ad Revenue Dashboard**: Professional advertising analytics with eCPM tracking, fill rates, and network performance optimization - ✅ **Monetization Settings**: Advanced ad network configuration with priority management and real-time revenue tracking +- ✅ **Modern Triangle Design Theme**: Complete geometric triangle design system with gradient purple-blue color scheme throughout all pages +- ✅ **Performance Optimizations**: Comprehensive speed improvements including HTTP compression, ETag caching, optimized search debouncing (300ms), WebP thumbnail format, lazy loading, and query optimizations for faster deployment performance ## User Preferences diff --git a/server/bunny.ts b/server/bunny.ts index 8ec3c97..3e39005 100644 --- a/server/bunny.ts +++ b/server/bunny.ts @@ -41,7 +41,9 @@ export class BunnyService { 'AccessKey': this.apiKey, 'Accept': 'application/json', 'Content-Type': 'application/json' - } + }, + // Add timeout and connection optimizations + signal: AbortSignal.timeout(10000), // 10 second timeout }); if (!response.ok) { @@ -52,11 +54,10 @@ export class BunnyService { } private bunnyVideoToVideo(bunnyVideo: BunnyVideo): Video { - // Generate thumbnail URL from Bunny CDN - try multiple formats - // Some videos may have custom thumbnails, others auto-generated + // Generate optimized thumbnail URL from Bunny CDN with WebP format for better performance const thumbnailUrl = bunnyVideo.thumbnailFileName - ? `https://${this.hostname}/${bunnyVideo.guid}/${bunnyVideo.thumbnailFileName}` - : `https://${this.hostname}/${bunnyVideo.guid}/thumbnail.jpg`; + ? `https://${this.hostname}/${bunnyVideo.guid}/${bunnyVideo.thumbnailFileName}?width=400&height=225&format=webp` + : `https://${this.hostname}/${bunnyVideo.guid}/thumbnail.jpg?width=400&height=225&format=webp`; // Generate signed HLS URL for private video access const hlsUrl = this.generateSignedUrl(bunnyVideo.guid); diff --git a/server/routes.ts b/server/routes.ts index b48ee34..17c280d 100644 --- a/server/routes.ts +++ b/server/routes.ts @@ -1,6 +1,7 @@ import type { Express, Request, Response } from "express"; import { createServer, type Server } from "http"; import express from "express"; +import compression from "compression"; import { storage } from "./storage"; import { z } from "zod"; import { @@ -52,6 +53,19 @@ const authenticate = (req: Request, res: Response, next: any) => { }; export async function registerRoutes(app: Express): Promise { + // Add compression middleware for better performance + app.use(compression({ + level: 6, + threshold: 1024, // Only compress responses larger than 1KB + filter: (req, res) => { + // Don't compress video files + if (req.headers['accept']?.includes('video/')) { + return false; + } + return compression.filter(req, res); + } + })); + // Configure session middleware app.use(session({ secret: process.env.SESSION_SECRET || 'dev-secret-key', @@ -164,14 +178,6 @@ export async function registerRoutes(app: Express): Promise { app.get("/api/videos", async (req, res) => { try { - // Set cache headers for better performance - res.set({ - 'Cache-Control': 'public, max-age=300, s-maxage=300', // 5 minutes cache - 'ETag': `"videos-${Date.now()}"`, - 'X-Content-Type-Options': 'nosniff', - 'X-Frame-Options': 'DENY' - }); - const limit = parseInt(req.query.limit as string) || 20; const offset = parseInt(req.query.offset as string) || 0; const search = req.query.search as string; @@ -179,6 +185,23 @@ export async function registerRoutes(app: Express): Promise { // Skip search for queries shorter than 2 characters for performance const searchQuery = search && search.length >= 2 ? search : undefined; + // Create cache key for ETag + const cacheKey = `videos-${limit}-${offset}-${searchQuery || 'all'}`; + const etag = `"${cacheKey}"`; + + // Check if client has cached version + if (req.headers['if-none-match'] === etag) { + return res.status(304).end(); + } + + // Set optimized cache headers + res.set({ + 'Cache-Control': 'public, max-age=120, stale-while-revalidate=300', // 2 min cache, 5 min stale + 'ETag': etag, + 'X-Content-Type-Options': 'nosniff', + 'Vary': 'Accept-Encoding' + }); + console.log(`Fetching videos: limit=${limit}, offset=${offset}, search=${searchQuery}`); const videos = await storage.getVideos(limit, offset, searchQuery); diff --git a/server/storage.ts b/server/storage.ts index 7043663..5a72088 100644 --- a/server/storage.ts +++ b/server/storage.ts @@ -383,16 +383,18 @@ export class MemStorage implements IStorage { async getVideos(limit = 20, offset = 0, search?: string): Promise { let videos = Array.from(this.videos.values()); - // Filter by search - if (search) { + // Optimized search - only search meaningful queries (2+ chars) + if (search && search.length >= 2) { const searchLower = search.toLowerCase(); - videos = videos.filter(video => - video.title.toLowerCase().includes(searchLower) || - video.description?.toLowerCase().includes(searchLower) - ); + videos = videos.filter(video => { + // Check title first (most common match) + if (video.title.toLowerCase().includes(searchLower)) return true; + // Check description if exists + return video.description?.toLowerCase().includes(searchLower) || false; + }); } - // Sort by created date (newest first) + // Sort by created date (newest first) - more efficient sort videos.sort((a, b) => b.createdAt.getTime() - a.createdAt.getTime()); return videos.slice(offset, offset + limit); @@ -451,7 +453,17 @@ export class MemStorage implements IStorage { } async getVideoCount(search?: string): Promise { - const videos = await this.getVideos(1000, 0, search); + // More efficient count without loading all data + let videos = Array.from(this.videos.values()); + + if (search && search.length >= 2) { + const searchLower = search.toLowerCase(); + videos = videos.filter(video => + video.title.toLowerCase().includes(searchLower) || + video.description?.toLowerCase().includes(searchLower) + ); + } + return videos.length; }