Chapter 12: Performance Optimization
Vibe CMS is designed for high performance from the ground up. This chapter covers the performance optimizations built into the system and best practices for maximizing speed and efficiency.
Database Optimization
Indexing Strategy
Vibe CMS uses a comprehensive indexing strategy to ensure fast queries across all tables:
Content Items Indexes:
-- Primary composite key for versioning
CREATE UNIQUE INDEX cms_content_items_pkey
ON cms_content_items (id, version_id);
-- Fast lookups by individual fields
CREATE INDEX cms_content_items_id_idx
ON cms_content_items (id);
CREATE INDEX cms_content_items_project_id_idx
ON cms_content_items (project_id);
CREATE INDEX cms_content_items_collection_id_idx
ON cms_content_items (collection_id);
CREATE INDEX cms_content_items_status_idx
ON cms_content_items (status);
-- Composite index for version queries
CREATE INDEX cms_content_items_project_version_idx
ON cms_content_items (project_id, version_id);
-- Temporal queries
CREATE INDEX cms_content_items_created_at_idx
ON cms_content_items (created_at);
Translation Data Indexes:
-- Composite primary key
CREATE UNIQUE INDEX cms_content_item_data_pkey
ON cms_content_item_data (id, version_id, locale);
-- Individual field indexes
CREATE INDEX cms_content_item_data_id_idx
ON cms_content_item_data (id);
CREATE INDEX cms_content_item_data_version_id_idx
ON cms_content_item_data (version_id);
CREATE INDEX cms_content_item_data_locale_idx
ON cms_content_item_data (locale);
File Storage Indexes:
-- SHA-256 hash for deduplication
CREATE INDEX cms_files_file_hash_idx
ON cms_files (file_hash);
-- MIME type filtering
CREATE INDEX cms_files_mime_type_idx
ON cms_files (mime_type);
-- Partial index for original files only
CREATE INDEX cms_files_storage_path_pattern_idx
ON cms_files (storage_path)
WHERE (storage_path LIKE '%/original/%');
JSONB Field Optimization
Content data is stored in JSONB fields for flexibility. While JSONB is highly efficient, consider:
Field Extraction: Use JSON operators for field access:
-- Fast: Uses JSONB operators SELECT data->>'title' FROM cms_content_item_data; -- Slower: Converts entire JSONB to text SELECT data::text FROM cms_content_item_data;GIN Indexes: For frequent JSONB queries, add GIN indexes:
-- Not enabled by default - add if needed CREATE INDEX idx_content_data_gin ON cms_content_item_data USING GIN (data);
Connection Pooling
Supabase provides built-in connection pooling. For high-traffic applications:
- Use Transaction mode for short queries (default)
- Use Session mode for long-running operations
- Configure pool size based on expected concurrent connections
Content Caching
Draft vs Published Table Separation
Vibe CMS uses a two-table architecture for optimal performance:
Draft Table (cms_content_items)
- Contains all versions of all content
- Used by authenticated editors
- Supports complex queries (filtering, versioning)
- Access controlled via SECURITY DEFINER functions
Published Table (cms_content_items_published)
- Contains only published content from the current published version
- Denormalized for maximum read performance
- No JOINs required - includes collection_slug, project_id inline
- Publicly accessible via read-only API functions
Table Structure Comparison:
-- Draft table: Normalized, supports versioning
cms_content_items (id, version_id, collection_id, status)
cms_content_item_data (id, version_id, locale, data)
cms_collections (id, slug)
-- Published table: Denormalized, optimized for reads
cms_content_items_published (
item_id, -- From cms_content_items.id
version_id, -- From cms_content_items.version_id
locale, -- From cms_content_item_data.locale
collection_slug, -- Denormalized from cms_collections.slug
project_id, -- Denormalized from cms_collections.project_id
data -- From cms_content_item_data.data
)
Trigger-Based Synchronization
The published table is synchronized via bulk operations during publishing:
-- publish_draft() function
DELETE FROM cms_content_items_published WHERE project_id = p_project_id;
INSERT INTO cms_content_items_published (
item_id, version_id, locale, collection_slug, project_id, data
)
SELECT
ci.id, ci.version_id, cid.locale, cc.slug, ci.project_id,
-- Clean data: only include fields in current schema
(SELECT jsonb_object_agg(key, value)
FROM jsonb_each(cid.data)
WHERE key IN (
SELECT field_name FROM cms_collection_fields
WHERE collection_id = ci.collection_id
))
FROM cms_content_items ci
JOIN cms_content_item_data cid ON cid.id = ci.id
JOIN cms_collections cc ON cc.id = ci.collection_id
WHERE ci.project_id = p_project_id
AND ci.version_id = draft_version_id
AND ci.status = 'published';
Denormalized field sync uses triggers for slug/locale changes:
-- Sync collection slug changes
CREATE TRIGGER collection_slug_sync_published
AFTER UPDATE OF slug ON cms_collections
FOR EACH ROW EXECUTE FUNCTION sync_collection_slug_to_published();
-- Sync locale code changes
CREATE TRIGGER locale_sync_published
AFTER UPDATE OF locale_code ON cms_locales
FOR EACH ROW EXECUTE FUNCTION sync_locale_to_published();
SDK Client-Side Caching
The TypeScript SDK provides built-in browser caching:
Cache Configuration:
import { createVibeCMS } from 'vibe-cms-sdk';
const cms = createVibeCMS({
projectId: 'your-project-id',
cache: {
enabled: true,
storage: 'localStorage', // or 'sessionStorage'
ttl: 300000, // 5 minutes (default)
},
});
Cache Storage Options:
localStorage: Persists across browser sessionssessionStorage: Clears when tab/window closes- Automatic fallback to in-memory cache if browser storage unavailable
Cache Key Structure:
vms:{projectId}:{locale}:{collectionSlug}:{queryType}[:{itemId}][:{paramHash}]
Examples:
vms:f669-...:en-US:blog-posts:all
vms:f669-...:en-US:blog-posts:item:abc123
vms:f669-...:en-US:asset:asset-url:xyz789
Cache Management:
// Clear all cache
await cms.cache.clear();
// Clear locale-specific cache
await cms.cache.clearLocaleCache('your-project-id', 'en-US');
// Clean up expired entries
await cms.cache.cleanup();
Cache Invalidation Strategies
For Static Sites (Recommended):
// Fetch at build time - no runtime cache needed
export const cms = createVibeCMS({
projectId: 'your-project-id',
// No cache configuration - fetches fresh during build
});
For Dynamic Sites:
// Short TTL for frequently updated content
const cms = createVibeCMS({
cache: { ttl: 60000 }, // 1 minute
});
// Longer TTL for stable content
const staticCms = createVibeCMS({
cache: { ttl: 3600000 }, // 1 hour
});
File Optimization
SHA-256 Deduplication
Vibe CMS automatically deduplicates files using SHA-256 hashing:
# File hash calculation
file_hash = hashlib.sha256(file_data).hexdigest()
# Check for existing file with same hash
existing_file = db.query(
"SELECT * FROM cms_files WHERE file_hash = %s",
file_hash
)
if existing_file:
# Return existing file reference instead of uploading
return existing_file
Benefits:
- Saves storage space (identical files stored once)
- Faster uploads (duplicate detection before upload)
- Indexed for O(1) lookup performance
Image Variants and Transformations
The file variants system supports multiple image sizes:
CREATE TABLE cms_file_variants (
file_id uuid,
variant_type text, -- 'thumbnail', 'web', 'print'
width integer,
height integer,
format text, -- 'jpeg', 'png', 'webp', 'avif'
quality integer,
file_size bigint,
storage_path text
);
-- Unique constraint prevents duplicate variants
CREATE UNIQUE INDEX cms_file_variants_unique
ON cms_file_variants (file_id, variant_type, width, height, format);
Storage Organization:
{account_id}/{project_id}/
original/{file_id}.jpg -- Original file
thumbnail/{file_id}.jpg -- 200x200 thumbnail
variant/{file_id}_800.webp -- 800px wide WebP
variant/{file_id}_1200.jpg -- 1200px wide JPEG
CDN Delivery
All files are stored in Supabase Storage with built-in CDN:
- Global edge network for fast delivery
- Automatic gzip/brotli compression
- HTTP/2 and HTTP/3 support
- 1-hour cache-control headers by default
Signed URLs for private files:
# Generate 24-hour signed URL
signed_url = storage.create_signed_url(
storage_path,
expires_in=86400 # 24 hours
)
Responsive Images
Implement responsive images for optimal performance:
<!-- Astro component example -->
<picture>
<source
srcset="{file.variants.webp_800}"
type="image/webp"
media="(max-width: 800px)"
/>
<source
srcset="{file.variants.webp_1200}"
type="image/webp"
media="(min-width: 801px)"
/>
<img
src="{file.original_url}"
alt="{file.alt_text}"
loading="lazy"
/>
</picture>
Query Performance
Pagination Strategies
The published content API supports efficient pagination:
CREATE FUNCTION get_published_content_items_for_collection(
p_project_id text,
p_collection_slug text,
p_locale text DEFAULT 'en-US',
p_limit integer DEFAULT 50,
p_offset integer DEFAULT 0
)
Usage:
// Page 1: Items 1-20
const page1 = await fetch(
`/api/content?collection=posts&limit=20&offset=0`
);
// Page 2: Items 21-40
const page2 = await fetch(
`/api/content?collection=posts&limit=20&offset=20`
);
Performance Tips:
- Use consistent
limitvalues for better cache hit rates - Avoid large
offsetvalues (use cursor-based pagination for huge datasets) - Index on sort columns for fast ORDER BY queries
Avoiding N+1 Queries
Vibe CMS functions are designed to prevent N+1 queries:
-- BAD: N+1 query pattern (not possible in Vibe CMS)
-- Fetch items
SELECT * FROM cms_content_items WHERE collection_id = 'x';
-- Then fetch translations for each item (N queries)
FOR EACH item: SELECT * FROM cms_content_item_data WHERE id = item.id;
-- GOOD: Single query with JOIN (Vibe CMS approach)
SELECT
ci.id, ci.status, cid.locale, cid.data
FROM cms_content_items ci
LEFT JOIN cms_content_item_data cid
ON cid.id = ci.id AND cid.version_id = ci.version_id
WHERE ci.collection_id = 'x';
Composite Indexes for Complex Queries
Composite indexes optimize multi-column queries:
-- Query: Find all items in a project/version
CREATE INDEX cms_content_items_project_version_idx
ON cms_content_items (project_id, version_id);
-- Query: Find collection items by slug
CREATE UNIQUE INDEX cms_collections_project_id_slug_key
ON cms_collections (project_id, slug);
-- Query: Find published items by collection and locale
CREATE INDEX cms_content_items_published_collection_slug_locale_idx
ON cms_content_items_published (collection_slug, locale);
API Performance
Bulk Operations vs Individual Calls
Vibe CMS provides bulk operations for better performance:
Individual calls (slower):
// DON'T: Multiple API calls
for (const item of items) {
await cms.collection('posts').id(item.id).update({
status: 'published'
});
}
Batch updates (faster):
// DO: Single bulk operation
await cms.collection('posts').bulkUpdate(
items.map(item => ({
id: item.id,
data: { status: 'published' }
}))
);
Parallel Requests
Use Promise.all() for parallel fetching:
// Fetch multiple collections in parallel
const [hero, features, testimonials] = await Promise.all([
cms.collection('landing-hero').first(),
cms.collection('features').all(),
cms.collection('testimonials').all(),
]);
Rate Limiting Considerations
For high-volume applications:
- Build-time fetching: No rate limits (recommended for static sites)
- Runtime fetching: Implement client-side caching and request debouncing
- API keys: Monitor usage via admin dashboard
Frontend Optimization
Static Site Generation (SSG) with Astro
Vibe CMS is optimized for SSG workflows:
// src/pages/blog/[slug].astro
---
import { cms } from '../../lib/cms';
// Fetch at build time (no runtime overhead)
export async function getStaticPaths() {
const posts = await cms.collection('blog-posts').all();
return posts.map(post => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
---
<article>
<h1>{post.title}</h1>
<div set:html={post.content} />
</article>
Performance Benefits:
- Zero runtime API calls
- Pre-rendered HTML served instantly
- Perfect Lighthouse scores (100/100/100/100)
- CDN-friendly static assets
Incremental Static Regeneration
For frequently updated content, use ISR patterns:
// Next.js example with ISR
export async function getStaticProps({ params }) {
const post = await cms.collection('posts').slug(params.slug);
return {
props: { post },
revalidate: 60, // Regenerate every 60 seconds
};
}
Asset Optimization
Images:
---
import { Image } from 'astro:assets';
const hero = await cms.collection('landing-hero').first();
---
<Image
src={hero.image_url}
alt={hero.image_alt}
width={1200}
height={630}
format="webp"
loading="lazy"
/>
Code Splitting:
// Lazy load heavy components
const HeavyComponent = lazy(() => import('./HeavyComponent'));
Monitoring and Profiling
Database Query Analysis
Supabase provides query performance insights:
- Navigate to Database → Query Performance
- Identify slow queries (> 100ms)
- Check if indexes are being used (
EXPLAIN ANALYZE) - Add missing indexes if needed
API Response Times
Monitor API performance:
// Add performance tracking
const start = performance.now();
const data = await cms.collection('posts').all();
const duration = performance.now() - start;
console.log(`Fetch took ${duration}ms`);
Cache Hit Rates
Track cache effectiveness:
const cacheStats = {
hits: 0,
misses: 0,
};
// Custom cache wrapper
const cachedFetch = async (key, fetcher) => {
const cached = await cms.cache.get(key);
if (cached) {
cacheStats.hits++;
return cached;
}
cacheStats.misses++;
const data = await fetcher();
await cms.cache.set(key, data);
return data;
};
console.log(`Cache hit rate: ${cacheStats.hits / (cacheStats.hits + cacheStats.misses) * 100}%`);
Best Practices
When to Use SSG vs SSR
Use SSG (recommended) when:
- Content changes infrequently (< hourly)
- Content is the same for all users
- Maximum performance is critical
- Building a marketing site, blog, or documentation
Use SSR when:
- Content changes constantly (real-time)
- Content is personalized per user
- Building a dashboard or admin interface
Content Update Frequency
Low frequency (daily/weekly):
// Build on every deploy
// No caching needed
const cms = createVibeCMS({ projectId: 'xxx' });
Medium frequency (hourly):
// Use ISR with 1-hour revalidation
// Or rebuild hourly via cron job
High frequency (minute):
// Enable SDK caching with short TTL
const cms = createVibeCMS({
cache: { enabled: true, ttl: 60000 },
});
Image Optimization Workflow
- Upload: Store original high-quality image
- Generate Variants: Create thumbnail, web, and print sizes
- Use WebP/AVIF: Modern formats for 30-50% smaller files
- Lazy Load: Use
loading="lazy"for below-fold images - Responsive: Serve appropriate size based on viewport
Database Maintenance
Regular maintenance for optimal performance:
- Vacuum: Run
VACUUM ANALYZEperiodically (Supabase does this automatically) - Monitor Growth: Check table sizes and plan for scaling
- Archive Old Versions: Delete unused versions to reduce bloat
- Review Indexes: Drop unused indexes, add missing ones
Performance Checklist
- Use SSG/ISR for public-facing pages
- Enable SDK caching for dynamic pages
- Implement lazy loading for images
- Use WebP/AVIF image formats
- Paginate large content collections
- Use
Promise.all()for parallel requests - Monitor cache hit rates
- Review database query performance
- Implement CDN caching headers
- Use code splitting for large components
Next Steps
Now that you understand Vibe CMS performance optimization:
- Implement SSG workflows for maximum speed
- Configure SDK caching based on your update frequency
- Optimize images with variants and modern formats
- Monitor and profile your application regularly
Need help? Join our Discord community or check the API Reference.