Performance Optimization
Maximize app performance for smooth operation at scale.
Understanding Performance
Section titled “Understanding Performance”Performance Goals
Section titled “Performance Goals”Target metrics:
Page Load Time:• Admin dashboard: <2 seconds• File library: <3 seconds (1000+ files)• Order history: <2 seconds• Settings pages: <1.5 seconds
API Response Time:• File operations: <500ms• Product mapping: <300ms• Download link generation: <200ms• Webhook processing: <1 second
Download Performance:• Link generation: <500ms• First byte: <200ms (TTFB)• Download speed: 10+ MB/s• Success rate: >98%
System Reliability:• Uptime: 99.9%• Error rate: <0.5%• Database queries: <100ms average• Background jobs: <5 min processingPerformance Impact
Section titled “Performance Impact”Customer experience:
Fast performance (< 2s loads):✓ 95% customer satisfaction✓ 15% higher conversion rate✓ 8% lower cart abandonment✓ Fewer support tickets✓ Better reviews and ratings
Slow performance (> 5s loads):❌ 70% customer satisfaction❌ 25% lower conversion❌ 40% cart abandonment increase❌ 3x more support tickets❌ Negative reviewsDatabase Optimization
Section titled “Database Optimization”Database Maintenance
Section titled “Database Maintenance”Regular maintenance schedule:
Weekly tasks:
Settings → Advanced → Database → Maintenance
Tasks to run:☐ Analyze database statistics☐ Update query planner statistics☐ Identify slow queries☐ Review connection pool usage
Command (if direct access):ANALYZE VERBOSE;
Result: Updated statistics for query optimizerTime: 2-5 minutesBenefit: 10-30% faster queriesMonthly tasks:
☐ Rebuild database indexes☐ Vacuum full database☐ Review table bloat☐ Archive old data
Command:REINDEX DATABASE alva_production;VACUUM FULL ANALYZE;
Result: Reclaimed space, rebuilt indexesTime: 15-30 minutes (maintenance window)Benefit: 20-50% faster queries, reduced storageIndex Optimization
Section titled “Index Optimization”Essential indexes:
Files table:
-- Optimize file searchesCREATE INDEX idx_files_shop_id ON files(shop_id);CREATE INDEX idx_files_created_at ON files(created_at DESC);CREATE INDEX idx_files_file_type ON files(file_type);CREATE INDEX idx_files_active ON files(is_active) WHERE is_active = true;
-- Composite index for common queriesCREATE INDEX idx_files_shop_active ON files(shop_id, is_active)WHERE is_active = true;
Query improvement:Before: 2,400ms (full table scan)After: 45ms (index scan)Speedup: 53x fasterOrders/Purchases table:
-- Optimize order lookupsCREATE INDEX idx_purchases_shop_id ON purchases(shop_id);CREATE INDEX idx_purchases_customer_email ON purchases(customer_email);CREATE INDEX idx_purchases_order_date ON purchases(order_date DESC);CREATE INDEX idx_purchases_fraud_status ON purchases(fraud_status);
-- Composite for fraud checkingCREATE INDEX idx_purchases_shop_fraud ON purchases(shop_id, fraud_status, order_date DESC);
Query improvement:Before: 1,800msAfter: 35msSpeedup: 51x fasterDownloads table:
-- Optimize download trackingCREATE INDEX idx_downloads_token ON downloads(token) WHERE expires_at > NOW();CREATE INDEX idx_downloads_customer ON downloads(customer_email, created_at DESC);CREATE INDEX idx_downloads_product ON downloads(product_id, created_at DESC);
-- IP trackingCREATE INDEX idx_downloads_ip_count ON downloads(customer_ip, token);
Query improvement:Before: 950msAfter: 12msSpeedup: 79x fasterQuery Optimization
Section titled “Query Optimization”Efficient query patterns:
Bad query (N+1 problem):
// ❌ BAD: Makes 1000+ database queriesconst files = await prisma.file.findMany({ where: { shopId: shop.id }});
for (const file of files) { // Separate query for each file! const productMappings = await prisma.productMapping.findMany({ where: { fileId: file.id } }); file.products = productMappings;}
Result: 1,000 files = 1,001 queriesTime: 15-25 secondsGood query (eager loading):
// ✅ GOOD: Makes 1 database queryconst files = await prisma.file.findMany({ where: { shopId: shop.id }, include: { productMappings: { include: { product: true } } }});
Result: 1,000 files = 1 queryTime: 450msSpeedup: 33-55x fasterPagination for large datasets:
Bad approach:
// ❌ BAD: Loads all 10,000 orders at onceconst orders = await prisma.purchase.findMany({ where: { shopId: shop.id }, orderBy: { orderDate: 'desc' }});
Result:Memory: 250 MBTime: 8-12 secondsBrowser: Freezes, may crashGood approach:
// ✅ GOOD: Paginated with cursorconst ITEMS_PER_PAGE = 50;
const orders = await prisma.purchase.findMany({ where: { shopId: shop.id }, orderBy: { orderDate: 'desc' }, take: ITEMS_PER_PAGE, skip: page * ITEMS_PER_PAGE});
const total = await prisma.purchase.count({ where: { shopId: shop.id }});
Result:Memory: 2.5 MB (100x less)Time: 180ms (44-66x faster)Browser: Smooth, no freezingData Archival
Section titled “Data Archival”Archive old data:
Archival strategy:
Archive criteria:• Orders > 2 years old• Cancelled/refunded orders > 1 year• Test data (any age)• Failed fraud checks > 6 months• Expired download tokens > 90 days
Archive process:1. Export to CSV2. Store in separate archive database/table3. Delete from main tables4. Keep 2-year rolling window
Frequency: MonthlyArchive procedure:
-- Export old orders to archive tableINSERT INTO purchases_archiveSELECT * FROM purchasesWHERE order_date < NOW() - INTERVAL '2 years';
-- Verify exportSELECT COUNT(*) FROM purchases_archiveWHERE order_date < NOW() - INTERVAL '2 years';
-- Delete archived recordsDELETE FROM purchasesWHERE order_date < NOW() - INTERVAL '2 years';
-- Vacuum to reclaim spaceVACUUM ANALYZE purchases;
Result:Before: 850,000 orders, 2.4 GBAfter: 125,000 orders, 340 MBSpeedup: 5-8x faster queriesCaching Strategy
Section titled “Caching Strategy”Application Caching
Section titled “Application Caching”Multi-layer caching:
Layer 1: Browser cache (client-side)
Static assets:• Images: 7 days• CSS/JS bundles: 365 days (versioned URLs)• Fonts: 365 days
Cache-Control headers:Cache-Control: public, max-age=31536000, immutable
Result: 80% fewer asset requestsLayer 2: CDN cache (edge)
Cloudflare R2 CDN:• File downloads: 24 hours• Download page HTML: 5 minutes• API responses: No cache (dynamic)
Cache-Control: public, max-age=86400
Result: 95% cache hit rateGlobal: <100ms latencyLayer 3: Application cache (server)
Redis/memory cache:• Product data: 30 minutes• Shop settings: 60 minutes• File metadata: 60 minutes• Database query results: 5 minutes
TTL (time-to-live): Auto-expire
Result: 70% fewer database queriesCache Implementation
Section titled “Cache Implementation”File metadata caching:
Without cache:
// ❌ Every request hits databaseasync function getFile(fileId) { const file = await prisma.file.findUnique({ where: { id: fileId } }); return file;}
Result per 1,000 requests:Database queries: 1,000Avg response time: 85msDatabase load: HighWith cache:
// ✅ Cache for 1 hourimport { cache } from './cache';
async function getFile(fileId) { const cacheKey = `file:${fileId}`;
// Try cache first let file = await cache.get(cacheKey);
if (!file) { // Cache miss: query database file = await prisma.file.findUnique({ where: { id: fileId } });
// Store in cache for 1 hour await cache.set(cacheKey, file, 3600); }
return file;}
Result per 1,000 requests:Database queries: 15-20 (98% cache hit rate)Avg response time: 8ms (10.6x faster)Database load: MinimalCache Invalidation
Section titled “Cache Invalidation”When to clear cache:
File updates:
// Clear cache when file modifiedasync function updateFile(fileId, updates) { // Update database const file = await prisma.file.update({ where: { id: fileId }, data: updates });
// Invalidate cache await cache.delete(`file:${fileId}`); await cache.delete(`files:shop:${file.shopId}`);
// Purge CDN cache await cloudflare.purgeCache(file.cdnUrl);
return file;}Settings changes:
// Clear all shop-related cachesasync function updateShopSettings(shopId, settings) { await prisma.shop.update({ where: { id: shopId }, data: settings });
// Clear all shop caches await cache.deletePattern(`shop:${shopId}:*`); await cache.deletePattern(`files:shop:${shopId}:*`); await cache.deletePattern(`products:shop:${shopId}:*`);}Manual cache clear:
Settings → Advanced → Performance → Clear Cache
Options:☐ Clear all caches (full reset)☐ Clear file metadata only☐ Clear product data only☐ Clear download pages only
When to use:• After bulk file updates• After settings changes• When seeing stale data• Before testing new featuresCDN and Asset Optimization
Section titled “CDN and Asset Optimization”CDN Configuration
Section titled “CDN Configuration”Cloudflare R2 CDN setup:
Optimal settings:
Settings → Storage → CDN → Cloudflare R2
☑ CDN enabled☑ Global edge caching (300+ locations)☑ HTTP/2 enabled☑ HTTP/3 (QUIC) enabled☑ Brotli compression☑ Auto-minify: HTML, CSS, JS
Cache rules:• File downloads: 24 hours• Thumbnails: 7 days• Static assets: 365 days
Result:• 40-60% faster downloads globally• 80% reduced origin requests• 99.99% availabilityGeographic performance:
Download performance by region:
North America:• TTFB: 45ms• Download: 35 MB/s• CDN: 95% cache hit
Europe:• TTFB: 65ms• Download: 32 MB/s• CDN: 93% cache hit
Asia:• TTFB: 85ms• Download: 28 MB/s• CDN: 91% cache hit
Oceania:• TTFB: 95ms• Download: 25 MB/s• CDN: 89% cache hit
Target: <100ms TTFB globallyAsset Optimization
Section titled “Asset Optimization”Image optimization:
Product images:
Optimization:• Format: WebP (70% smaller than JPEG)• Fallback: JPEG for old browsers• Lazy loading: Images below fold• Responsive: Serve appropriate size
Example:Original: product.jpg (450 KB)Optimized: product.webp (135 KB)Savings: 70%
HTML:<picture> <source srcset="product.webp" type="image/webp"> <img src="product.jpg" alt="Product" loading="lazy"></picture>Icon sprites:
Combine icons into single sprite sheet:Before: 25 icon files, 25 HTTP requestsAfter: 1 sprite file, 1 HTTP request
Savings:• 24 fewer HTTP requests• 40% smaller total size (compression)• Faster page loads (600ms → 350ms)CSS and JavaScript:
Minification:
Build process:npm run build
Result:CSS: 125 KB → 42 KB (66% reduction)JS: 850 KB → 320 KB (62% reduction)
Techniques:• Remove whitespace• Remove comments• Shorten variable names• Tree-shaking (remove unused code)Code splitting:
Load only needed JavaScript:
Before (monolithic):• Bundle: 850 KB• Initial load: 850 KB• Time to interactive: 4.2s
After (code splitting):• Main bundle: 180 KB• Route chunks: 50-120 KB each• Initial load: 180 KB• Time to interactive: 1.8s• Improvement: 2.3x fasterFrontend Performance
Section titled “Frontend Performance”React Component Optimization
Section titled “React Component Optimization”Memoization:
Prevent unnecessary re-renders:
import { memo, useMemo, useCallback } from 'react';
// ❌ BAD: Re-renders on every parent updatefunction FileList({ files }) { return ( <div> {files.map(file => ( <FileCard key={file.id} file={file} /> ))} </div> );}
// ✅ GOOD: Only re-renders when files changeconst FileList = memo(function FileList({ files }) { const sortedFiles = useMemo(() => { return files.sort((a, b) => b.createdAt - a.createdAt); }, [files]);
return ( <div> {sortedFiles.map(file => ( <MemoizedFileCard key={file.id} file={file} /> ))} </div> );});
const MemoizedFileCard = memo(FileCard);
Result:Before: 250ms render time (1,000 files)After: 45ms render timeSpeedup: 5.5x fasterVirtual Scrolling
Section titled “Virtual Scrolling”Large lists optimization:
Without virtual scrolling:
// ❌ BAD: Renders all 5,000 files at oncefunction FileLibrary({ files }) { return ( <div> {files.map(file => ( <FileRow key={file.id} file={file} /> ))} </div> );}
Result:Initial render: 8-12 secondsMemory: 450 MBBrowser: Laggy scrollingWith virtual scrolling:
// ✅ GOOD: Only renders visible rows (~30)import { VariableSizeList } from 'react-window';
function FileLibrary({ files }) { return ( <VariableSizeList height={600} itemCount={files.length} itemSize={(index) => 80} width="100%" > {({ index, style }) => ( <div style={style}> <FileRow file={files[index]} /> </div> )} </VariableSizeList> );}
Result:Initial render: 180ms (44-66x faster)Memory: 12 MB (37x less)Browser: Smooth 60fps scrollingDebouncing and Throttling
Section titled “Debouncing and Throttling”Search input optimization:
Without debouncing:
// ❌ BAD: API call on every keystrokefunction SearchBox() { const handleSearch = async (query) => { const results = await fetch(`/api/search?q=${query}`); // ... };
return ( <input type="search" onChange={(e) => handleSearch(e.target.value)} /> );}
Result:User types "tutorial" (8 characters)API calls: 8 (t, tu, tut, tuto, tutor, tutori, tutoria, tutorial)Cost: Expensive, unnecessary loadWith debouncing:
// ✅ GOOD: API call only after user stops typingimport { debounce } from 'lodash';
function SearchBox() { const debouncedSearch = useMemo( () => debounce(async (query) => { const results = await fetch(`/api/search?q=${query}`); // ... }, 300), // Wait 300ms after last keystroke [] );
return ( <input type="search" onChange={(e) => debouncedSearch(e.target.value)} /> );}
Result:User types "tutorial" (8 characters)API calls: 1 (only "tutorial")Reduction: 87.5% fewer API callsBackend Performance
Section titled “Backend Performance”API Rate Limiting
Section titled “API Rate Limiting”Protect against abuse:
Rate limit configuration:
Settings → Advanced → API → Rate Limiting
Limits:• Anonymous: 60 requests/hour• Authenticated: 1,000 requests/hour• Admin API: 5,000 requests/hour
Headers returned:X-RateLimit-Limit: 1000X-RateLimit-Remaining: 856X-RateLimit-Reset: 1709856000
When exceeded:Status: 429 Too Many RequestsRetry-After: 3600 (seconds)Background Job Optimization
Section titled “Background Job Optimization”Fraud check worker:
Inefficient polling:
// ❌ BAD: Checks every 1 second, wastes resourcessetInterval(async () => { const jobs = await prisma.fraudCheckQueue.findMany({ where: { status: 'pending' } });
for (const job of jobs) { await processFraudCheck(job); }}, 1000); // Every second
Result:Database queries: 86,400/dayMost queries: No jobs (wasted)Database load: HighEfficient polling with backoff:
// ✅ GOOD: Adaptive polling intervallet pollInterval = 5000; // Start with 5 secondsconst MIN_INTERVAL = 5000;const MAX_INTERVAL = 60000;
async function pollJobs() { const jobs = await prisma.fraudCheckQueue.findMany({ where: { status: 'pending' }, take: 10 // Batch size });
if (jobs.length > 0) { // Jobs found: process and reduce interval await Promise.all(jobs.map(processFraudCheck)); pollInterval = Math.max(MIN_INTERVAL, pollInterval / 2); } else { // No jobs: increase interval (exponential backoff) pollInterval = Math.min(MAX_INTERVAL, pollInterval * 1.5); }
setTimeout(pollJobs, pollInterval);}
pollJobs();
Result:Database queries: 2,000-5,000/day (90% reduction)Database load: MinimalAdaptability: Fast when busy, slow when idleDatabase Connection Pooling
Section titled “Database Connection Pooling”Optimize connections:
Configuration:
DATABASE_URL=postgresql://user:pass@host:5432/db? connection_limit=20& pool_timeout=10& connect_timeout=5
Connection pool:• Min connections: 5• Max connections: 20• Idle timeout: 10 minutes• Connection timeout: 5 seconds
Why pooling helps:• Reuse connections (avoid TCP handshake)• Limit max connections (prevent overload)• Queue requests when pool full• Auto-recovery from connection errorsPool monitoring:
Monitor connection pool:Settings → Advanced → Database → Connection Pool
Metrics:• Active connections: 12/20• Idle connections: 8• Queued requests: 0• Average wait time: 2ms
Alerts:⚠️ Pool 90% full: Consider increasing limit❌ Queue building up: Database overloadedMonitoring and Alerting
Section titled “Monitoring and Alerting”Performance Monitoring
Section titled “Performance Monitoring”Key metrics to track:
Dashboard metrics:
Analytics → Performance
Response times:• P50 (median): 180ms• P95: 650ms• P99: 1,200msTarget: P95 < 1,000ms
Throughput:• Requests/min: 450• Downloads/min: 120• Orders/min: 25
Error rates:• 2xx success: 98.5%• 4xx client errors: 1.2%• 5xx server errors: 0.3%Target: 5xx < 0.5%
Database:• Query time P95: 85ms• Connection pool: 65% utilization• Slow queries (>1s): 2/hourReal User Monitoring (RUM)
Section titled “Real User Monitoring (RUM)”Track actual user experience:
Core Web Vitals:
Metrics tracked:• LCP (Largest Contentful Paint): 1.8s Target: < 2.5s ✓
• FID (First Input Delay): 45ms Target: < 100ms ✓
• CLS (Cumulative Layout Shift): 0.08 Target: < 0.1 ✓
• TTFB (Time to First Byte): 320ms Target: < 500ms ✓
Score: 95/100 (Excellent)By page type:
Dashboard:• LCP: 1.2s• FID: 35ms• Score: 98/100
File Library (1,000+ files):• LCP: 2.1s• FID: 65ms• Score: 89/100
Order History:• LCP: 1.5s• FID: 40ms• Score: 95/100Alerting Configuration
Section titled “Alerting Configuration”Set up alerts:
Critical alerts (immediate action):
Settings → Monitoring → Alerts → Critical
Triggers:☑ Error rate >2% for 5 minutes☑ API response time P95 >2s for 5 minutes☑ Download success rate <95% for 10 minutes☑ Database connection pool >90% for 5 minutes☑ Server down/unreachable
Notification:• Email: dev-team@example.com• SMS: +1-555-0100 (on-call)• Slack: #alerts-critical• PagerDuty: Escalation policyWarning alerts (investigate soon):
Triggers:☑ Error rate >1% for 15 minutes☑ API response time P95 >1s for 15 minutes☑ Download speed <5 MB/s for 30 minutes☑ Database slow queries >10/hour☑ CDN cache hit rate <80%
Notification:• Email: dev-team@example.com• Slack: #alerts-warningPerformance Budgets
Section titled “Performance Budgets”Set performance targets:
Budget enforcement:
Performance budgets:• Page weight: 500 KB max• JavaScript bundle: 200 KB max• CSS: 50 KB max• Images: 200 KB max per page• Time to interactive: 3s max• API response: 500ms max
Enforcement:• CI/CD checks on build• Alerts if budget exceeded• Block deployment if >10% over budget
Example CI check:✅ Bundle size: 185 KB (92% of budget)✅ Page weight: 445 KB (89% of budget)❌ API response P95: 650ms (130% of budget)Result: Build fails, investigate before deployingLoad Testing
Section titled “Load Testing”Testing Infrastructure
Section titled “Testing Infrastructure”Before major releases:
Load test procedure:
Tools:• Apache JMeter (open source)• k6 (modern, scriptable)• Artillery (Node.js-based)
Test scenarios:1. Normal load: 100 concurrent users2. Peak load: 500 concurrent users3. Stress test: 1,000+ concurrent users4. Spike test: Sudden traffic surge
Duration:• Ramp-up: 5 minutes• Sustained: 30 minutes• Ramp-down: 5 minutesExample k6 test:
import http from 'k6/http';import { check, sleep } from 'k6';
export let options = { stages: [ { duration: '5m', target: 100 }, // Ramp up to 100 users { duration: '30m', target: 100 }, // Stay at 100 users { duration: '5m', target: 0 }, // Ramp down to 0 ], thresholds: { http_req_duration: ['p(95)<1000'], // 95% requests < 1s http_req_failed: ['rate<0.01'], // <1% failure rate },};
export default function () { // Test file library let res = http.get('https://yourshop.myshopify.com/admin/files'); check(res, { 'status is 200': (r) => r.status === 200, 'response < 2s': (r) => r.timings.duration < 2000, });
sleep(1);
// Test download link generation res = http.post('https://yourshop.myshopify.com/api/download', { order_id: '12345' }); check(res, { 'link generated': (r) => r.status === 200, 'response < 500ms': (r) => r.timings.duration < 500, });
sleep(2);}Run test:
k6 run --vus 100 --duration 30m load-test.js
Results:✓ status is 200: 98.5%✓ response < 2s: 96.2%✓ link generated: 99.1%✓ response < 500ms: 97.8%
http_req_duration: avg=450ms, p(95)=850mshttp_req_failed: 1.2%
Conclusion: System handles load wellTroubleshooting Slow Performance
Section titled “Troubleshooting Slow Performance”Identify Bottlenecks
Section titled “Identify Bottlenecks”Diagnostic process:
Step 1: Measure
Tools:• Browser DevTools (Network, Performance)• Analytics dashboard• Database query logs• Server logs
Identify slow areas:☐ Slow page loads (>3s)☐ Slow API responses (>1s)☐ Slow database queries (>100ms)☐ High error rates (>1%)☐ Memory leaks (growing usage)Step 2: Profile
Use browser profiler:1. Open DevTools → Performance2. Click Record3. Navigate/interact with app4. Stop recording5. Analyze flame graph
Look for:• Long tasks (>50ms, blocks UI)• Excessive re-renders• Memory leaks• Slow network requestsStep 3: Database profiling
-- Find slow queries (PostgreSQL)SELECT query, calls, total_time / 1000 as total_seconds, mean_time / 1000 as avg_seconds, max_time / 1000 as max_secondsFROM pg_stat_statementsWHERE mean_time > 100 -- Slower than 100msORDER BY total_time DESCLIMIT 10;
Example result:Query: SELECT * FROM files WHERE shop_id = ?Calls: 45,000Avg: 850msMax: 3,200msIssue: Missing index on shop_id
Fix: CREATE INDEX idx_files_shop_id ON files(shop_id);Result: Avg drops to 12ms (70x faster)Common Performance Issues
Section titled “Common Performance Issues”Issue 1: Slow file library (1,000+ files)
Symptoms:• Page load: 8-15 seconds• Browser freezing• High memory usage
Causes:• Loading all files at once (no pagination)• No virtual scrolling• Rendering all file cards upfront• No caching
Fixes:✓ Enable pagination (50 files per page)✓ Implement virtual scrolling✓ Lazy load file thumbnails✓ Cache file metadata (1 hour)✓ Add database indexes
Result:Before: 12s load, 450 MB memoryAfter: 1.8s load, 45 MB memoryImprovement: 6.6x faster, 10x less memoryIssue 2: Slow order processing
Symptoms:• Webhook processing >5 seconds• Delayed email notifications• Fraud checks timing out
Causes:• Synchronous processing (blocking)• No background jobs• N+1 database queries• External API calls in webhook
Fixes:✓ Move to background jobs (async)✓ Batch database queries✓ Cache product/file data✓ Optimize fraud check queries✓ Parallel processing where possible
Result:Before: 6.5s webhook processingAfter: 450ms webhook + background jobImprovement: 14x faster, non-blockingIssue 3: Slow download link generation
Symptoms:• Customer waits 3-5 seconds for download• High bounce rate• Support complaints
Causes:• Database query for every file in order• Generating signed URLs synchronously• No caching of download metadata• Checking fraud status on every request
Fixes:✓ Cache download tokens (5 minutes)✓ Pre-generate links on order (async)✓ Batch database queries✓ Cache fraud status✓ Use database indexes
Result:Before: 3.8s link generationAfter: 180ms link retrievalImprovement: 21x fasterShopify-Specific Optimizations
Section titled “Shopify-Specific Optimizations”Shopify API Rate Limits
Section titled “Shopify API Rate Limits”Respect limits:
Rate limit tiers:
Shopify Plus: 20 requests/secondAdvanced: 10 requests/secondStandard: 4 requests/secondBasic: 2 requests/second
Leaky bucket algorithm:• Requests added to bucket• Bucket drains at fixed rate• Bucket full = rate limited
Header: X-Shopify-Shop-Api-Call-Limit: 32/40Meaning: 32 of 40 requests used, 8 remainingRate limit strategy:
// ✅ GOOD: Respect rate limitsimport { delay } from './utils';
class ShopifyClient { constructor() { this.requestQueue = []; this.maxRequestsPerSecond = 4; // For Standard plan }
async request(endpoint) { // Wait if rate limit approaching const callLimit = response.headers['x-shopify-shop-api-call-limit']; const [used, max] = callLimit.split('/').map(Number);
if (used >= max * 0.9) { // 90% of limit: wait 1 second await delay(1000); }
return fetch(`https://shop.myshopify.com/admin/api/2024-01/${endpoint}`); }}GraphQL vs REST
Section titled “GraphQL vs REST”Choose efficient API:
REST (multiple requests):
// ❌ SLOWER: 3 separate REST API callsconst product = await fetch('/admin/api/2024-01/products/123.json');const variants = await fetch('/admin/api/2024-01/products/123/variants.json');const metafields = await fetch('/admin/api/2024-01/products/123/metafields.json');
Time: 850ms (3 round trips)API calls: 3/40 usedGraphQL (single request):
// ✅ FASTER: 1 GraphQL requestconst query = ` query { product(id: "gid://shopify/Product/123") { title variants(first: 10) { edges { node { title price } } } metafields(first: 10) { edges { node { namespace key value } } } } }`;
const result = await fetch('/admin/api/2024-01/graphql.json', { method: 'POST', body: JSON.stringify({ query })});
Time: 280ms (1 round trip)API calls: 1/40 usedImprovement: 3x faster, 66% fewer API callsBest Practices Summary
Section titled “Best Practices Summary”Performance Checklist
Section titled “Performance Checklist”Before launch:
☐ Database indexes on all foreign keys☐ Pagination enabled for large lists (>100 items)☐ Caching strategy implemented☐ CDN configured and tested☐ Assets optimized (images, CSS, JS)☐ Virtual scrolling for long lists☐ Background jobs for async operations☐ Rate limiting configured☐ Load testing completed☐ Monitoring and alerts set up☐ Performance budgets definedOngoing maintenance:
☐ Weekly database optimization☐ Monthly index rebuild☐ Quarterly data archival☐ Review slow query logs weekly☐ Monitor cache hit rates☐ Review error logs daily☐ Load test before major releases☐ Update performance budgets quarterlyQuick Wins
Section titled “Quick Wins”Immediate improvements:
1. Enable caching (5 minutes)
Settings → Performance → Caching☑ Enable all caching layers
Impact: 50-70% faster page loadsEffort: Minimal2. Add database indexes (10 minutes)
-- Essential indexesCREATE INDEX idx_files_shop_id ON files(shop_id);CREATE INDEX idx_purchases_shop_id ON purchases(shop_id);CREATE INDEX idx_downloads_token ON downloads(token);
Impact: 10-50x faster queriesEffort: Low3. Enable CDN (15 minutes)
Settings → Storage → CDN☑ Enable Cloudflare R2 CDN
Impact: 40-60% faster downloadsEffort: Low4. Implement pagination (30 minutes)
// Replace full list with pagination<DataTable rows={files} pagination={true} rowsPerPage={50}/>
Impact: 5-10x faster page loadsEffort: Medium5. Optimize images (20 minutes)
# Convert to WebPnpm install sharpnode scripts/convert-images-to-webp.js
Impact: 60-70% smaller imagesEffort: LowPerformance Goals by Shop Size
Section titled “Performance Goals by Shop Size”Small Shop (< 100 orders/month)
Section titled “Small Shop (< 100 orders/month)”Targets:
Page load: <2sAPI response: <500msDownload speed: 10+ MB/sUptime: 99.5%
Configuration:• Database: Basic optimization• Caching: Moderate (1 hour)• CDN: Enabled• Monitoring: Weekly checksMedium Shop (100-1,000 orders/month)
Section titled “Medium Shop (100-1,000 orders/month)”Targets:
Page load: <1.5sAPI response: <300msDownload speed: 20+ MB/sUptime: 99.9%
Configuration:• Database: Weekly optimization, monthly archival• Caching: Aggressive (multi-layer)• CDN: Optimized with custom rules• Monitoring: Daily checks, automated alertsLarge Shop (1,000+ orders/month)
Section titled “Large Shop (1,000+ orders/month)”Targets:
Page load: <1sAPI response: <200msDownload speed: 30+ MB/sUptime: 99.95%
Configuration:• Database: Daily optimization, weekly archival• Caching: Aggressive (Redis + CDN)• CDN: Custom domain, optimized routing• Monitoring: Real-time monitoring, 24/7 alerts• Load testing: Before all releases• Performance budgets: Strictly enforcedNext Steps
Section titled “Next Steps”- Optimizing File Delivery - File compression and delivery
- Security Best Practices - Secure your app
- Understanding Dashboard Metrics - Monitor performance