F030 - Speed & Scalability Optimization
Objective
Optimize the manuscript analysis platform for sub-5-minute processing of 150k word manuscripts while maintaining high quality analysis and supporting thousands of concurrent users.
Quick Implementation
Using NextSaaS Performance Features
- Edge runtime optimization
- Static generation where possible
- Image optimization
- Bundle size management
- Caching strategies
New Requirements
- AI response streaming
- Parallel processing
- Database query optimization
- CDN integration
MVP Implementation
1. AI Processing Optimization
// packages/manuscript-analysis/src/services/optimized-analysis-engine.ts
import { Readable } from 'stream'
import pLimit from 'p-limit'
import { LRUCache } from 'lru-cache'
export class OptimizedAnalysisEngine {
private chunkLimit = pLimit(5) // Process 5 chunks in parallel
private cache = new LRUCache<string, any>({
max: 100,
ttl: 1000 * 60 * 60 // 1 hour cache
})
async analyzeManuscriptStream(
manuscriptId: string,
content: string,
onProgress: (progress: number) => void
): Promise<Readable> {
// Check cache first
const cacheKey = this.getCacheKey(manuscriptId, content)
const cached = this.cache.get(cacheKey)
if (cached) {
return Readable.from([cached])
}
// Split into optimal chunks for parallel processing
const chunks = this.createOptimalChunks(content)
const totalChunks = chunks.length
let processedChunks = 0
// Create readable stream for real-time updates
const stream = new Readable({
read() {}
})
// Process chunks in parallel with limit
const chunkPromises = chunks.map((chunk, index) =>
this.chunkLimit(async () => {
const result = await this.analyzeChunk(chunk, index)
processedChunks++
// Update progress
const progress = Math.round((processedChunks / totalChunks) * 100)
onProgress(progress)
// Stream partial results
stream.push(JSON.stringify({
type: 'partial',
chunkIndex: index,
data: result
}) + '\n')
return result
})
)
// Aggregate results
Promise.all(chunkPromises).then(async (results) => {
const aggregated = await this.aggregateResults(results)
// Cache the result
this.cache.set(cacheKey, aggregated)
// Send final result
stream.push(JSON.stringify({
type: 'complete',
data: aggregated
}) + '\n')
stream.push(null) // End stream
}).catch(error => {
stream.destroy(error)
})
return stream
}
private createOptimalChunks(content: string): string[] {
const MAX_TOKENS_PER_CHUNK = 3000
const OVERLAP_TOKENS = 200
// Use GPT tokenizer for accurate counting
const tokens = this.tokenize(content)
const chunks: string[] = []
for (let i = 0; i < tokens.length; i += MAX_TOKENS_PER_CHUNK - OVERLAP_TOKENS) {
const chunkTokens = tokens.slice(i, i + MAX_TOKENS_PER_CHUNK)
chunks.push(this.detokenize(chunkTokens))
}
return chunks
}
private async analyzeChunk(chunk: string, index: number): Promise<any> {
// Use lighter model for initial chunks, full model for critical sections
const model = index < 3 ? 'gpt-3.5-turbo' : 'gpt-4'
const response = await fetch('https://api.openai.com/v1/chat/completions', {
method: 'POST',
headers: {
'Authorization': `Bearer ${process.env.OPENAI_API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model,
messages: [{
role: 'system',
content: 'Analyze this manuscript section...'
}, {
role: 'user',
content: chunk
}],
temperature: 0.3,
max_tokens: 1000,
stream: false
})
})
return response.json()
}
private async aggregateResults(results: any[]): Promise<any> {
// Intelligent aggregation of chunk analyses
const aggregated = {
overall_score: 0,
category_scores: {},
strengths: [],
weaknesses: [],
detailed_feedback: {}
}
// Weight early chapters more heavily
const weights = results.map((_, i) => Math.max(1, 5 - i * 0.5))
const totalWeight = weights.reduce((a, b) => a + b, 0)
results.forEach((result, index) => {
const weight = weights[index] / totalWeight
// Aggregate scores with weighting
aggregated.overall_score += result.score * weight
// Merge other fields intelligently
})
return aggregated
}
}2. Database Query Optimization
// packages/database/src/optimizations.ts
import { createClient } from '@mystoryflow/database/server'
// Materialized view for dashboard stats
export const createMaterializedViews = async () => {
const supabase = getSupabaseBrowserClient()
// User dashboard stats
await supabase.rpc('create_materialized_view', {
view_name: 'user_dashboard_stats',
query: `
SELECT
u.id as user_id,
COUNT(DISTINCT m.id) as total_manuscripts,
COUNT(DISTINCT ma.id) as total_analyses,
AVG(ma.overall_score) as average_score,
MAX(ma.overall_score) as best_score,
COUNT(DISTINCT cs.id) as total_consultations,
COALESCE(SUM(m.word_count), 0) as total_words_analyzed
FROM users u
LEFT JOIN manuscripts m ON m.user_id = u.id
LEFT JOIN manuscript_analyses ma ON ma.manuscript_id = m.id
LEFT JOIN consultation_sessions cs ON cs.author_id = u.id
GROUP BY u.id
`,
refresh_interval: '1 hour'
})
// Genre performance stats
await supabase.rpc('create_materialized_view', {
view_name: 'genre_performance_stats',
query: `
SELECT
gd.primary_genre as genre,
COUNT(DISTINCT m.id) as manuscript_count,
AVG(ma.overall_score) as avg_score,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY ma.overall_score) as median_score,
AVG(ma.processing_time_ms) as avg_processing_time
FROM manuscripts m
JOIN genre_detections gd ON gd.manuscript_id = m.id
JOIN manuscript_analyses ma ON ma.manuscript_id = m.id
GROUP BY gd.primary_genre
`,
refresh_interval: '6 hours'
})
}
// Optimized queries with proper indexing
export const getOptimizedUserDashboard = async (userId: string) => {
const supabase = getSupabaseBrowserClient()
// Use materialized view for instant results
const { data: stats } = await supabase
.from('analyzer.user_dashboard_stats')
.select('*')
.eq('user_id', userId)
.single()
// Get recent items with optimized query
const { data: recentItems } = await supabase
.from('analyzer.manuscripts')
.select(`
id,
title,
created_at,
manuscript_analyses!inner (
overall_score,
created_at
)
`)
.eq('user_id', userId)
.order('manuscript_analyses.created_at', { ascending: false })
.limit(5)
return { stats, recentItems }
}3. Next.js Performance Optimizations
// next.config.js
module.exports = {
experimental: {
optimizeCss: true,
optimizePackageImports: ['@mystoryflow/ui', 'lucide-react']
},
images: {
domains: ['cdn.mystoryflow.com'],
formats: ['image/avif', 'image/webp']
},
compress: true,
poweredByHeader: false,
// Split chunks for better caching
webpack: (config, { isServer }) => {
if (!isServer) {
config.optimization.splitChunks = {
chunks: 'all',
cacheGroups: {
default: false,
vendors: false,
framework: {
name: 'framework',
chunks: 'all',
test: /[\\/]node_modules[\\/](react|react-dom|next)[\\/]/,
priority: 40,
enforce: true
},
lib: {
test: /[\\/]node_modules[\\/]/,
name(module) {
const packageName = module.context.match(
/[\\/]node_modules[\\/](.*?)([[\\/]|$)/
)[1]
return `npm.${packageName.replace('@', '')}`
},
priority: 30,
minChunks: 1,
reuseExistingChunk: true
},
commons: {
name: 'commons',
minChunks: 2,
priority: 20
},
shared: {
name(module, chunks) {
return crypto
.createHash('sha1')
.update(chunks.reduce((acc, chunk) => acc + chunk.name, ''))
.digest('hex') + (isServer ? '-server' : '-client')
},
priority: 10,
minChunks: 2,
reuseExistingChunk: true
}
}
}
}
return config
}
}4. Edge Function for Fast Analysis Status
// apps/analyzer-app/src/app/api/analysis/status/route.ts
import { NextRequest } from 'next/server'
export const runtime = 'edge'
// Use KV store for ultra-fast status checks
const statusCache = new Map<string, any>()
export async function GET(request: NextRequest) {
const manuscriptId = request.nextUrl.searchParams.get('manuscriptId')
if (!manuscriptId) {
return new Response('Missing manuscriptId', { status: 400 })
}
// Check memory cache first (edge runtime persists between requests)
const cached = statusCache.get(manuscriptId)
if (cached && Date.now() - cached.timestamp < 5000) {
return new Response(JSON.stringify(cached.data), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=5'
}
})
}
// Fetch from database with connection pooling
const response = await fetch(
`${process.env.SUPABASE_URL}/rest/v1/manuscript_analyses?manuscript_id=eq.${manuscriptId}&order=created_at.desc&limit=1`,
{
headers: {
'apikey': process.env.SUPABASE_ANON_KEY!,
'Authorization': `Bearer ${process.env.SUPABASE_ANON_KEY}`
}
}
)
const data = await response.json()
const analysis = data[0]
const status = {
status: analysis ? 'completed' : 'processing',
progress: analysis ? 100 : 50,
completedAt: analysis?.created_at
}
// Update cache
statusCache.set(manuscriptId, {
data: status,
timestamp: Date.now()
})
return new Response(JSON.stringify(status), {
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=5'
}
})
}5. Client-Side Performance
// apps/analyzer-app/src/hooks/useOptimizedData.ts
import { useQuery } from '@tanstack/react-query'
import { useState, useEffect, useRef } from 'react'
// Virtual scrolling for large lists
export function useVirtualList<T>({
items,
height,
itemHeight,
overscan = 5
}: {
items: T[]
height: number
itemHeight: number
overscan?: number
}) {
const [scrollTop, setScrollTop] = useState(0)
const scrollElementRef = useRef<HTMLDivElement>(null)
const startIndex = Math.max(0, Math.floor(scrollTop / itemHeight) - overscan)
const endIndex = Math.min(
items.length - 1,
Math.ceil((scrollTop + height) / itemHeight) + overscan
)
const visibleItems = items.slice(startIndex, endIndex + 1)
const totalHeight = items.length * itemHeight
const offsetY = startIndex * itemHeight
useEffect(() => {
const handleScroll = () => {
if (scrollElementRef.current) {
setScrollTop(scrollElementRef.current.scrollTop)
}
}
const element = scrollElementRef.current
element?.addEventListener('scroll', handleScroll, { passive: true })
return () => {
element?.removeEventListener('scroll', handleScroll)
}
}, [])
return {
scrollElementRef,
visibleItems,
totalHeight,
offsetY
}
}
// Optimized data fetching with prefetching
export function useManuscriptAnalysis(manuscriptId: string) {
const queryClient = useQueryClient()
// Main query
const analysisQuery = useQuery({
queryKey: ['analysis', manuscriptId],
queryFn: () => fetchAnalysis(manuscriptId),
staleTime: 1000 * 60 * 5, // 5 minutes
cacheTime: 1000 * 60 * 30 // 30 minutes
})
// Prefetch related data
useEffect(() => {
if (analysisQuery.data) {
// Prefetch author's other manuscripts
queryClient.prefetchQuery({
queryKey: ['manuscripts', analysisQuery.data.userId],
queryFn: () => fetchUserManuscripts(analysisQuery.data.userId)
})
// Prefetch similar genre analyses
queryClient.prefetchQuery({
queryKey: ['genre-comparisons', analysisQuery.data.genre],
queryFn: () => fetchGenreComparisons(analysisQuery.data.genre)
})
}
}, [analysisQuery.data, queryClient])
return analysisQuery
}6. Image Optimization Service
// apps/analyzer-app/src/lib/image-optimization.ts
import { getPlaiceholder } from 'plaiceholder'
export async function getOptimizedImage(src: string) {
try {
const { base64, img } = await getPlaiceholder(src)
return {
...img,
blurDataURL: base64
}
} catch (error) {
return {
src,
height: 600,
width: 800
}
}
}
// Usage in component
export async function CoachProfile({ coach }: { coach: any }) {
const optimizedAvatar = await getOptimizedImage(coach.avatar_url)
return (
<div>
<Image
{...optimizedAvatar}
alt={coach.full_name}
placeholder="blur"
loading="lazy"
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>
</div>
)
}7. Service Worker for Offline Support
// public/sw.js
const CACHE_NAME = 'story-analyzer-v1'
const urlsToCache = [
'/',
'/offline',
'/manifest.json',
'/_next/static/css/app.css',
'/_next/static/js/app.js'
]
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then((cache) => {
return cache.addAll(urlsToCache)
})
)
})
self.addEventListener('fetch', (event) => {
// Cache-first strategy for static assets
if (event.request.url.includes('/_next/static/')) {
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request).then((response) => {
return caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, response.clone())
return response
})
})
})
)
return
}
// Network-first strategy for API calls
if (event.request.url.includes('/api/')) {
event.respondWith(
fetch(event.request)
.then((response) => {
const responseClone = response.clone()
caches.open(CACHE_NAME).then((cache) => {
cache.put(event.request, responseClone)
})
return response
})
.catch(() => {
return caches.match(event.request)
})
)
return
}
// Default strategy
event.respondWith(
caches.match(event.request).then((response) => {
return response || fetch(event.request)
})
)
})8. Performance Monitoring
// apps/analyzer-app/src/lib/performance-monitor.ts
export class PerformanceMonitor {
private metrics: Map<string, number[]> = new Map()
measureApiCall(endpoint: string, duration: number) {
if (!this.metrics.has(endpoint)) {
this.metrics.set(endpoint, [])
}
const calls = this.metrics.get(endpoint)!
calls.push(duration)
// Keep only last 100 calls
if (calls.length > 100) {
calls.shift()
}
// Alert if performance degrades
const avg = calls.reduce((a, b) => a + b) / calls.length
if (avg > 1000) { // 1 second threshold
this.alertSlowEndpoint(endpoint, avg)
}
}
measureComponentRender(componentName: string, duration: number) {
if (duration > 16) { // 60fps threshold
console.warn(`Slow render: ${componentName} took ${duration}ms`)
}
}
private alertSlowEndpoint(endpoint: string, avgDuration: number) {
// Send to monitoring service
fetch('/api/monitoring/performance', {
method: 'POST',
body: JSON.stringify({
type: 'slow_endpoint',
endpoint,
averageDuration: avgDuration,
timestamp: new Date().toISOString()
})
})
}
}
// Usage
const monitor = new PerformanceMonitor()
export async function fetchWithMonitoring(url: string, options?: RequestInit) {
const start = performance.now()
try {
const response = await fetch(url, options)
const duration = performance.now() - start
monitor.measureApiCall(url, duration)
return response
} catch (error) {
const duration = performance.now() - start
monitor.measureApiCall(url, duration)
throw error
}
}MVP Acceptance Criteria
- Sub-5-minute analysis for 150k words
- Real-time progress updates
- Parallel chunk processing
- Database query optimization
- Edge function deployment
- Client-side performance
- Image optimization
- Caching strategies
- Bundle size < 200KB
- Lighthouse score > 90
Post-MVP Enhancements
- WebAssembly for heavy computations
- GPU acceleration for AI processing
- Global CDN deployment
- Advanced caching with Redis
- Microservices architecture
- Kubernetes scaling
- GraphQL with DataLoader
- Server-side streaming
- Predictive prefetching
- Background sync API
Implementation Time
- Development: 3 days
- Testing: 1 day
- Total: 4 days
Dependencies
- Edge runtime configuration
- CDN setup
- Performance monitoring tools
- Caching infrastructure
Performance Targets
- First Contentful Paint: < 1.5s
- Time to Interactive: < 3.5s
- Analysis Processing: < 5 minutes for 150k words
- API Response Time: < 200ms (p95)
- Bundle Size: < 200KB (gzipped)