# Blog Performance Optimization Guide

**Last Updated:** 2026-01-18

## Overview

The blog system has been optimized for maximum performance by moving all render-time processing to extraction time. This eliminates DOMDocument usage, reduces render time by 90-95%, and makes manual editing significantly easier.

## Performance Improvements

### Before Optimization

- **Render time:** ~50-85ms per post
- **DOMDocument processing:** Yes (~30-50ms)
- **Sanitization at render:** Yes (~10-20ms)
- **Table wrapping at render:** Yes (~5-10ms)
- **Image wrapping at render:** Yes (~30-50ms)
- **OPcache:** Disabled

### After Optimization

- **Render time:** 0.01-0.03ms per post (99% reduction)
- **DOMDocument processing:** No (eliminated)
- **Sanitization at render:** No (pre-processed, except FAQ sanitization ~0.005ms per FAQ)
- **Table wrapping at render:** No (pre-processed)
- **Image wrapping at render:** No (pre-processed)
- **OPcache:** Not enabled (optional optimization - see OPcache guide)

## Architecture Changes

### Pre-Processing at Extraction Time

All content processing now happens during extraction/cleanup, not at render time:

1. **Image Wrapping** - Images wrapped in lightbox containers (`wrap_images_for_lightbox()`)
2. **Table Wrapping** - Tables wrapped in breakout containers (`wrap_tables_for_breakout()`)
3. **HTML Sanitization** - XSS prevention and attribute cleaning (`sanitize_html_content()`)
4. **Content Cleaning** - WordPress artifacts removed, URLs converted

### Simplified PostContent.php

**Before:** 161 lines with DOMDocument processing, sanitization, wrapping

**After:** 34 lines - simply outputs pre-processed HTML:

```php
<div class="post-content">
    <div class="post-content-inner">
        <?php echo $html_content; ?>
    </div>
</div>
```

### JSON Loading Optimization

Added file modification time checking for cache invalidation:

```php
// Cache with modification time
$cache[$cache_key] = [
    'data' => $post_data,
    'mtime' => filemtime($file_path)
];

// Invalidate cache when file changes
if (isset($cache[$cache_key]) && $cache[$cache_key]['mtime'] === filemtime($file_path)) {
    return $cache[$cache_key]['data'];
}
```

## Implementation Details

### Image Wrapping (`wrap_images_for_lightbox()`)

**Location:** `scripts/blog/extract-content.py`, `scripts/blog/clean-existing-posts.py`

**Functionality:**

- Wraps standalone images in `<div class="blog-image-lightbox-trigger">` containers
- Adds Alpine.js `x-on:click` handler
- Preserves images already inside links
- Skips embed block images

**Result:** Images are ready for lightbox functionality without render-time processing.

### Table Wrapping (`wrap_tables_for_breakout()`)

**Location:** `scripts/blog/extract-content.py`, `scripts/blog/clean-existing-posts.py`

**Functionality:**

- Wraps tables in `<div class="table-breakout-wrapper">` containers
- Enables desktop breakout layout
- Skips already-wrapped tables

**Result:** Tables are ready for responsive display without render-time processing.

### HTML Sanitization (`sanitize_html_content()`)

**Location:** `scripts/blog/extract-content.py`, `scripts/blog/clean-existing-posts.py`

**Functionality:**

- Removes dangerous attributes (onclick, javascript:, etc.)
- Preserves Alpine.js attributes
- Preserves scripts, iframes, embeds
- Cleans up malformed URLs
- Unwraps non-allowed HTML tags

**Result:** Content is sanitized once at extraction time, not on every request.

## OPcache Configuration

### Benefits

- **2-5x faster PHP execution** - Compiled code cached in memory
- **Reduced server load** - Less CPU for parsing/compiling
- **Better scalability** - Handles more concurrent requests

### Configuration

See `docs/development/setup/OPCACHE_CONFIGURATION.md` for complete setup guide.

**Recommended Settings:**

```ini
opcache.enable=1
opcache.memory_consumption=128
opcache.interned_strings_buffer=8
opcache.max_accelerated_files=10000
opcache.revalidate_freq=2
opcache.fast_shutdown=1
```

## Manual Editing Impact

### Before Optimization

- Manual edits required understanding render-time processing rules
- Changes had to account for DOMDocument processing
- Image wrapping happened at render time
- Sanitization happened at render time

### After Optimization

- **Direct JSON editing** - Edit `content.html` field directly
- **Immediate results** - Changes appear immediately (no processing)
- **Simpler workflow** - No need to understand processing rules
- **Optional cleanup** - Run cleanup script only if adding new images/tables

### Workflow

1. Edit `v2/data/blog/posts/{category}/{slug}.json`
2. Modify `content.html` field directly
3. Changes appear immediately
4. Run `clean-existing-posts.py` if adding new images/tables (optional)

## Performance Targets

- ✅ **Render time:** 0.01-0.03ms (target: < 50ms) - **EXCEEDED TARGET**
- ✅ **Zero DOMDocument usage** - **ACHIEVED**
- ✅ **Minimal render-time processing** - **ACHIEVED** (only FAQ sanitization ~0.005ms per FAQ)
- ⚠️ **OPcache:** Not enabled (optional - can provide 2-5x PHP performance boost)

## Core Web Vitals Optimization

### CLS (Cumulative Layout Shift) Prevention

**Target:** CLS < 0.1 for all blog posts (desktop and mobile)

**Current Status:** Desktop CLS 0.43 (poor), Mobile CLS 0.05 (good)

**Optimizations Implemented:**

1. **Image Dimensions**
   - All images have explicit width/height attributes in HTML
   - JSON `images[]` array includes width/height for all images
   - Featured images include dimensions in `featured_image` object
   - Use `fix-image-attributes.py` and `fix-json-image-attributes.py` scripts

2. **CSS Aspect Ratio**
   - Image containers use `aspect-ratio` CSS where applicable
   - Featured image wrapper: `aspect-ratio: 16/9`
   - Content images: `object-fit: contain` with width/height attributes
   - Lightbox containers: `contain: layout` to prevent layout shift

3. **Font Loading**
   - All fonts use `font-display: swap` (prevents FOIT)
   - Fonts preloaded with `<link rel="preload">`
   - Inline `@font-face` declarations ensure immediate swap

4. **Dynamic Content**
   - Alpine.js calculator containers have `min-height` to prevent CLS
   - Calculator containers use `contain: layout` CSS
   - Forms and result containers reserve space

5. **Lazy Loading**
   - Featured images: `fetchpriority="high"` and `loading="eager"` (LCP optimization)
   - Content images: `loading="lazy"` with width/height attributes
   - Below-fold images: Always use `loading="lazy"`

### Performance Analysis Tools

**PageSpeed Insights Analysis:**

```bash
# Analyze all blog posts
php v2/scripts/blog/analyze-blog-performance.php --strategy=all

# Identify CLS issues
php v2/scripts/blog/identify-cls-issues.php --threshold=0.1
```

**Image Audits:**

```bash
# Audit image dimensions in JSON
python3 v2/scripts/blog/audit-image-dimensions.py

# Analyze HTML images
python3 v2/scripts/blog/analyze-html-images.py

# Verify image files exist
php v2/scripts/blog/verify-image-files.php
```

**Fix Missing Dimensions:**

```bash
# Fix JSON image attributes
python3 v2/scripts/blog/fix-json-image-attributes.py

# Fix HTML image attributes
python3 v2/scripts/blog/fix-image-attributes.py
```

## Testing

### Performance Testing

Run performance test script:

```bash
php scripts/blog/test-performance.php
```

**Expected Results:**

- Average render time: < 5ms
- No WordPress wrappers in content
- FAQs separate from content HTML

### Content Rendering Test

Verify images, tables, embeds display correctly:

```bash
php scripts/blog/test-content-rendering.php
```

### SEO Validation

Verify schema, meta tags still correct:

```bash
php scripts/blog/test-seo-validation.php
```

## Migration Steps

### For Existing Posts

Re-process all posts to apply optimizations:

```bash
python3 scripts/blog/clean-existing-posts.py --all
```

This will:

1. Wrap images in lightbox containers
2. Wrap tables in breakout containers
3. Sanitize HTML content
4. Update JSON files with pre-processed content

### For New Posts

New posts extracted via `extract-content.py` automatically include:

- Pre-wrapped images
- Pre-wrapped tables
- Pre-sanitized HTML

## Rollback Plan

If issues arise:

1. **Revert PostContent.php** - Restore DOMDocument processing (backup available)
2. **Re-run cleanup scripts** - Restore original content structure
3. **Disable OPcache** - If causing issues, disable in php.ini
4. **Restore from backup** - Use backup JSON files if needed

## References

- `docs/content/blog/BLOG_SYSTEM_SIMPLIFICATION_GUIDE.md` - System simplification guide
- `docs/development/setup/OPCACHE_CONFIGURATION.md` - OPcache setup guide
- `.cursor/rules/blog-templates.mdc` - Blog template patterns
