Copied to clipboard!

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:

  1. 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;
    
  2. 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 sessions
  • sessionStorage: 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 limit values for better cache hit rates
  • Avoid large offset values (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:

  1. Navigate to DatabaseQuery Performance
  2. Identify slow queries (> 100ms)
  3. Check if indexes are being used (EXPLAIN ANALYZE)
  4. 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

  1. Upload: Store original high-quality image
  2. Generate Variants: Create thumbnail, web, and print sizes
  3. Use WebP/AVIF: Modern formats for 30-50% smaller files
  4. Lazy Load: Use loading="lazy" for below-fold images
  5. Responsive: Serve appropriate size based on viewport

Database Maintenance

Regular maintenance for optimal performance:

  1. Vacuum: Run VACUUM ANALYZE periodically (Supabase does this automatically)
  2. Monitor Growth: Check table sizes and plan for scaling
  3. Archive Old Versions: Delete unused versions to reduce bloat
  4. 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.