Skip to content

Performance Optimization Guide

This guide provides strategies and best practices for optimizing performance when using WP Block to HTML, especially for large sites with substantial content.

Understanding Performance Challenges

When working with headless WordPress sites, several performance challenges may arise:

  1. Large Content Sets: Processing many blocks across multiple pages
  2. Complex Nested Structures: Deeply nested blocks requiring recursive processing
  3. Media-Heavy Content: Pages with numerous images, galleries, or embeds
  4. Server-Side Rendering: Additional processing requirements for SSR applications
  5. Bundle Size: Impact on application load time and performance

Performance Metrics

When optimizing, focus on these key metrics:

  • Processing Time: How long it takes to convert blocks to HTML
  • Memory Usage: Peak memory consumption during conversion
  • Bundle Size: Impact on application load time
  • Core Web Vitals: LCP, CLS, and FID/INP metrics for end users
  • Server Response Time: For server-rendered applications

Bundle Size Optimization

Selective Imports

Import only what you need to reduce bundle size:

javascript
// ❌ Bad: Importing the entire library
import * as wpBlockToHtml from 'wp-block-to-html';

// ✅ Good: Import only what you need
import { convertBlocks } from 'wp-block-to-html/core';
import { paragraphBlockHandler } from 'wp-block-to-html/blocks/text';
import { imageBlockHandler } from 'wp-block-to-html/blocks/media';

Dynamic Imports

For client-side applications, use dynamic imports to load transformers on demand:

javascript
async function processContent(blocks) {
  const { convertBlocks } = await import('wp-block-to-html/core');
  
  // Analyze which block types are present
  const blockTypes = new Set(blocks.map(block => block.blockName));
  
  // Only import needed transformers
  const transformers = {};
  
  if (blockTypes.has('core/paragraph') || blockTypes.has('core/heading')) {
    const textHandlers = await import('wp-block-to-html/blocks/text');
    // Add relevant handlers to transformers object
  }
  
  if (blockTypes.has('core/image') || blockTypes.has('core/gallery')) {
    const mediaHandlers = await import('wp-block-to-html/blocks/media');
    // Add relevant handlers to transformers object
  }
  
  return convertBlocks(blocks, { blockTransformers: transformers });
}

Tree-Shaking

Ensure your build setup is configured for effective tree-shaking:

javascript
// webpack.config.js
module.exports = {
  mode: 'production',
  optimization: {
    usedExports: true,
    sideEffects: true
  }
};

Processing Optimization

Memoization

Use memoization to avoid reprocessing identical blocks:

javascript
import memoize from 'lodash/memoize';

// Memoize the convertBlocks function based on input
const memoizedConvertBlocks = memoize(
  (blocks, options) => convertBlocks(blocks, options),
  (blocks, options) => {
    // Create a cache key based on the blocks and relevant options
    const blockIds = blocks.map(block => block.attrs?.id || block.blockName).join('|');
    const optionsKey = JSON.stringify({
      cssFramework: options.cssFramework,
      contentHandling: options.contentHandling
    });
    return `${blockIds}-${optionsKey}`;
  }
);

// Use the memoized function
const html = memoizedConvertBlocks(blocks, options);

Batch Processing

For large content sets, process blocks in batches:

javascript
async function processBatchedContent(allBlocks, options) {
  const BATCH_SIZE = 50;
  let result = '';
  
  // Process blocks in batches
  for (let i = 0; i < allBlocks.length; i += BATCH_SIZE) {
    const batch = allBlocks.slice(i, i + BATCH_SIZE);
    result += await convertBlocks(batch, options);
    
    // Allow event loop to process other tasks
    await new Promise(resolve => setTimeout(resolve, 0));
  }
  
  return result;
}

Web Workers

Offload processing to Web Workers in client-side applications:

javascript
// worker.js
import { convertBlocks } from 'wp-block-to-html';

self.addEventListener('message', (event) => {
  const { blocks, options } = event.data;
  const result = convertBlocks(blocks, options);
  self.postMessage(result);
});

// Main application
function processInWorker(blocks, options) {
  return new Promise((resolve, reject) => {
    const worker = new Worker('./worker.js', { type: 'module' });
    
    worker.onmessage = (event) => {
      resolve(event.data);
      worker.terminate();
    };
    
    worker.onerror = (error) => {
      reject(error);
      worker.terminate();
    };
    
    worker.postMessage({ blocks, options });
  });
}

Server-Side Rendering Optimization

SSR Optimization Module

Use the built-in SSR optimization module:

javascript
import { processBlocksForSSR, convertBlocks } from 'wp-block-to-html';

// Apply SSR optimizations
const optimizedBlocks = processBlocksForSSR(blocks, {
  optimizationLevel: 'maximum',
  lazyLoadMedia: true,
  prioritizeAboveTheFold: true
});

// Convert the optimized blocks
const html = convertBlocks(optimizedBlocks);

Streaming SSR

For Next.js or other frameworks with streaming support:

javascript
import { renderToNodeStream } from 'react-dom/server';
import { convertBlocksToReact } from 'wp-block-to-html/react';

// In your server rendering function
function handleRequest(req, res) {
  // Fetch blocks from WordPress API
  
  // Convert blocks to React components
  const reactComponents = convertBlocksToReact(blocks, {
    ssrOptions: {
      enabled: true,
      optimizationLevel: 'balanced'
    }
  });
  
  // Create a stream of the rendered output
  const stream = renderToNodeStream(reactComponents);
  
  // Set headers and pipe stream to response
  res.setHeader('Content-Type', 'text/html');
  stream.pipe(res);
}

Caching

Implement caching for converted blocks:

javascript
import { createClient } from 'redis';

const redis = createClient();
await redis.connect();

async function getConvertedContent(postId, options) {
  const cacheKey = `wp-block-html:${postId}:${JSON.stringify(options)}`;
  
  // Try to get from cache
  const cached = await redis.get(cacheKey);
  if (cached) {
    return cached;
  }
  
  // Fetch blocks from WordPress API
  const blocks = await fetchBlocksFromAPI(postId);
  
  // Convert blocks
  const html = convertBlocks(blocks, options);
  
  // Store in cache (expires in 1 hour)
  await redis.set(cacheKey, html, { EX: 3600 });
  
  return html;
}

Media Optimization

Lazy Loading

Enable lazy loading for media elements:

javascript
import { convertBlocks } from 'wp-block-to-html';

const html = convertBlocks(blocks, {
  ssrOptions: {
    enabled: true,
    lazyLoadMedia: true,
    preserveFirstImage: true  // Don't lazy-load the first image (for LCP)
  }
});

Image Size Attributes

Ensure images have explicit width and height attributes to prevent layout shifts:

javascript
import { convertBlocks } from 'wp-block-to-html';

// Custom transformer for images
const imageBlockHandler = {
  transform(block, options) {
    const { attrs = {} } = block;
    const { url, alt = '', width, height } = attrs;
    
    return `
      <img 
        src="${url}" 
        alt="${alt}" 
        ${width ? `width="${width}"` : ''} 
        ${height ? `height="${height}"` : ''}
        loading="lazy"
      />
    `;
  }
};

const html = convertBlocks(blocks, {
  blockTransformers: {
    'core/image': imageBlockHandler
  }
});

Responsive Images

Generate responsive images with the srcset attribute:

javascript
function generateSrcSet(imageUrl) {
  // Extract base URL and extension
  const [base, ext] = imageUrl.split(/\.(?=[^.]+$)/);
  
  // Define widths for responsive images
  const widths = [320, 640, 960, 1280, 1920];
  
  // Generate srcset attribute
  return widths
    .map(width => `${base}-${width}w.${ext} ${width}w`)
    .join(', ');
}

const imageBlockHandler = {
  transform(block, options) {
    const { attrs = {} } = block;
    const { url, alt = '', width, height } = attrs;
    
    const srcset = generateSrcSet(url);
    
    return `
      <img 
        src="${url}" 
        srcset="${srcset}"
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
        alt="${alt}" 
        width="${width}" 
        height="${height}"
        loading="lazy"
      />
    `;
  }
};

Framework-Specific Optimizations

React

Optimize React components:

jsx
import React, { memo, useMemo } from 'react';
import { convertBlocksToReact } from 'wp-block-to-html/react';

// Memoize expensive component
const MemoizedContent = memo(function Content({ blocks, options }) {
  // Use useMemo to prevent unnecessary reconversion
  const components = useMemo(() => 
    convertBlocksToReact(blocks, options),
    [blocks, options.cssFramework, options.contentHandling]
  );
  
  return <div className="content">{components}</div>;
});

// Usage
function Post({ post }) {
  return (
    <article>
      <h1>{post.title}</h1>
      <MemoizedContent blocks={post.content} options={{ cssFramework: 'tailwind' }} />
    </article>
  );
}

Vue

Optimize Vue components:

vue
<template>
  <div class="content">
    <component 
      v-for="(component, index) in components" 
      :key="index" 
      :is="component.type" 
      v-bind="component.props" 
    />
  </div>
</template>

<script>
import { convertBlocksToVue } from 'wp-block-to-html/vue';

export default {
  props: {
    blocks: Array,
    options: Object
  },
  computed: {
    // Memoize the conversion
    components() {
      return convertBlocksToVue(this.blocks, this.options);
    }
  }
}
</script>

Advanced Techniques

Code Splitting

Split your application by content types:

javascript
// Dynamic import based on content type
async function loadContentProcessor(contentType) {
  switch (contentType) {
    case 'blog':
      return import('./processors/blog-processor');
    case 'product':
      return import('./processors/product-processor');
    case 'page':
      return import('./processors/page-processor');
    default:
      return import('./processors/default-processor');
  }
}

// Usage
async function processContent(contentType, blocks) {
  const processor = await loadContentProcessor(contentType);
  return processor.convert(blocks);
}

Analysis and Optimization

Analyze your content to optimize transformer selection:

javascript
function analyzeContent(blocks) {
  const stats = {
    totalBlocks: 0,
    blockTypes: {},
    nestingDepth: 0,
    mediaCount: 0
  };
  
  function traverseBlocks(blockList, depth = 0) {
    for (const block of blockList) {
      stats.totalBlocks++;
      stats.blockTypes[block.blockName] = (stats.blockTypes[block.blockName] || 0) + 1;
      stats.nestingDepth = Math.max(stats.nestingDepth, depth);
      
      // Count media blocks
      if (['core/image', 'core/gallery', 'core/video'].includes(block.blockName)) {
        stats.mediaCount++;
      }
      
      // Process inner blocks
      if (block.innerBlocks && block.innerBlocks.length > 0) {
        traverseBlocks(block.innerBlocks, depth + 1);
      }
    }
  }
  
  traverseBlocks(blocks);
  return stats;
}

// Use analysis to determine optimization strategy
function determineOptimizationStrategy(blocks) {
  const analysis = analyzeContent(blocks);
  
  if (analysis.totalBlocks > 100) {
    return 'batch-processing';
  } else if (analysis.mediaCount > 20) {
    return 'media-optimization';
  } else if (analysis.nestingDepth > 5) {
    return 'recursive-optimization';
  } else {
    return 'standard';
  }
}

Pre-processing Content

Pre-process content to optimize before conversion:

javascript
function preprocessBlocks(blocks) {
  return blocks.map(block => {
    // Clone to avoid mutating original
    const processedBlock = { ...block };
    
    // Optimize image blocks
    if (block.blockName === 'core/image') {
      processedBlock.attrs = { ...block.attrs };
      
      // Add loading attribute if missing
      if (!processedBlock.attrs.loading) {
        processedBlock.attrs.loading = 'lazy';
      }
      
      // Ensure width and height are present
      if (!processedBlock.attrs.width || !processedBlock.attrs.height) {
        // In real implementation, you might fetch image dimensions
        processedBlock.attrs.width = processedBlock.attrs.width || 800;
        processedBlock.attrs.height = processedBlock.attrs.height || 600;
      }
    }
    
    // Process inner blocks recursively
    if (block.innerBlocks && block.innerBlocks.length > 0) {
      processedBlock.innerBlocks = preprocessBlocks(block.innerBlocks);
    }
    
    return processedBlock;
  });
}

// Use pre-processed blocks
const optimizedBlocks = preprocessBlocks(blocks);
const html = convertBlocks(optimizedBlocks, options);

Case Study: Optimizing a Large Website

Scenario

A large content site with 10,000+ pages, each containing an average of 50-100 blocks, including:

  • Text blocks (paragraphs, headings, lists)
  • Media blocks (images, galleries, videos)
  • Layout blocks (columns, groups)
  • Dynamic blocks (latest posts, embeds)

Optimization Strategy

  1. Content Analysis: Analyze block usage across the site

    javascript
    const blockUsageMap = await analyzeAllContent();
    console.log('Most used blocks:', blockUsageMap);
  2. Selective Loading: Create a customized build with only necessary transformers

    javascript
    // Include only frequently used blocks
    const commonTransformers = {};
    for (const [blockName, count] of Object.entries(blockUsageMap)) {
      if (count > 100) { // Block appears in at least 100 pages
        commonTransformers[blockName] = getTransformerForBlock(blockName);
      }
    }
  3. SSR Optimization: Apply server-side optimizations

    javascript
    // Enable all optimizations for SSR
    const ssrOptions = {
      enabled: true,
      optimizationLevel: 'maximum',
      lazyLoadMedia: true,
      prioritizeAboveTheFold: true,
      preserveFirstImage: true,
      optimizationDepth: 3
    };
  4. Caching Strategy: Implement multi-level caching

    javascript
    // Cache levels:
    // 1. Block conversion cache (Redis)
    // 2. Page-level cache (CDN)
    // 3. Component-level cache (In-memory)
    
    async function getPageContent(pageId) {
      // Check Redis cache first
      const cached = await redisClient.get(`page:${pageId}`);
      if (cached) return cached;
      
      // Fetch blocks from WordPress
      const blocks = await fetchBlocksFromAPI(pageId);
      
      // Convert blocks with all optimizations
      const html = convertBlocks(
        processBlocksForSSR(blocks, ssrOptions),
        { blockTransformers: commonTransformers }
      );
      
      // Store in cache (expires in 1 hour)
      await redisClient.set(`page:${pageId}`, html, { EX: 3600 });
      
      return html;
    }
  5. Incremental Static Regeneration (for Next.js sites)

    javascript
    // In Next.js pages
    export async function getStaticProps({ params }) {
      const pageId = params.id;
      const html = await getPageContent(pageId);
      
      return {
        props: { html },
        // Regenerate page after 1 hour
        revalidate: 3600
      };
    }
    
    export async function getStaticPaths() {
      // Only pre-render the most popular pages
      const popularPages = await getPopularPages(100);
      
      return {
        paths: popularPages.map(page => ({ params: { id: page.id } })),
        fallback: 'blocking' // Generate other pages on demand
      };
    }
  6. Resource Monitoring to identify performance bottlenecks

    javascript
    // Add performance monitoring
    function monitorPerformance(pageId, startTime) {
      const duration = Date.now() - startTime;
      metrics.recordProcessingTime(pageId, duration);
      
      if (duration > 500) {
        logger.warn(`Slow page processing for ${pageId}: ${duration}ms`);
      }
    }

Results

  • 90% reduction in bundle size through selective imports
  • 70% improvement in page load times
  • 50% reduction in server processing time
  • Improved Core Web Vitals scores across all metrics

Performance Testing Tools

Conclusion

Optimizing WP Block to HTML for large sites requires a multi-faceted approach:

  1. Bundle Optimization: Use selective imports and tree-shaking
  2. Processing Efficiency: Apply memoization, batching, and workers
  3. SSR Optimization: Utilize built-in optimization features
  4. Caching Strategy: Implement multi-level caching
  5. Media Handling: Optimize images and enable lazy loading
  6. Framework Optimizations: Use framework-specific performance features
  7. Content Analysis: Understand and adapt to your content patterns

By applying these techniques, you can achieve excellent performance even with large and complex WordPress content.

Released under the MIT License.