Fix LCP Issues on Processwire (Loading Speed) | OpsBlu Docs

Fix LCP Issues on Processwire (Loading Speed)

Improve ProcessWire LCP by enabling ProCache full-page caching, using built-in image resizing API, and optimizing selector queries.

General Guide: See Global LCP Guide for universal concepts and fixes.

What is LCP?

Largest Contentful Paint measures when the largest content element becomes visible. Google recommends LCP under 2.5 seconds. ProcessWire is a PHP CMS with a powerful selector-based API, built-in image manipulation, and flexible template rendering. LCP depends on database query efficiency, image variation generation, and whether full-page caching is enabled.

ProcessWire-Specific LCP Causes

  • No built-in page cache -- ProcessWire executes PHP templates and database queries on every request unless ProCache or a custom cache is configured
  • Unresized images -- using $image->url directly serves full-resolution originals instead of generated variations
  • Complex selector queries -- $pages->find() with many conditions on large sites can be slow without proper indexes
  • Template file includes -- deeply nested $files->include() calls in template files add PHP overhead
  • Module overhead -- many active modules (FormBuilder, ProFields, etc.) add hooks that execute on every page load

Fixes

1. Enable ProCache or Implement Page Caching

ProCache (commercial module) provides static HTML caching:

// In /site/config.php
$config->proCacheEnabled = true;
$config->proCacheTime = 3600; // 1 hour

// Manual cache alternative (free):
// In your template file (e.g., home.php)
$cacheKey = 'page_' . $page->id . '_' . $page->modified;
$cached = $cache->get($cacheKey);
if ($cached) {
    echo $cached;
    return;
}
ob_start();
// ... your template rendering code ...
$output = ob_get_contents();
ob_end_flush();
$cache->save($cacheKey, $output, 3600);

2. Use ProcessWire's Image Resizing API

Always generate appropriately sized variations instead of serving originals:

// In your template file
$hero = $page->hero_image;
if ($hero) {
    // Create a resized variation (cached automatically)
    $img = $hero->size(1200, 630, ['quality' => 80, 'upscaling' => false]);
    echo "<img
        src='{$img->url}'
        width='{$img->width}' height='{$img->height}'
        alt='{$page->title}'
        loading='eager'
        fetchpriority='high'
        style='aspect-ratio: {$img->width}/{$img->height}; width: 100%; height: auto; object-fit: cover;'
    >";
}

Generate WebP variations:

// In /site/config.php
$config->imageSizerOptions('webpAdd', true);
$config->imageSizerOptions('webpQuality', 80);

// In template -- ProcessWire auto-generates .webp alongside .jpg
$img = $hero->size(1200, 630);
echo "<picture>
    <source srcset='{$img->webp->url}' type='image/webp'>
    <img src='{$img->url}' width='{$img->width}' height='{$img->height}'
         alt='{$page->title}' loading='eager' fetchpriority='high'>
</picture>";

3. Optimize Selector Queries

// SLOW: Fetching all fields for a listing
$posts = $pages->find("template=blog-post, sort=-date, limit=10");

// FASTER: Only load fields you need
$posts = $pages->find("template=blog-post, sort=-date, limit=10");
// Use autojoin for frequently accessed fields
// In /site/config.php:
$config->dbCache = true;

// Or use $pages->findRaw() for read-only listing data
$posts = $pages->findRaw("template=blog-post, sort=-date, limit=10", [
    'title', 'date', 'summary', 'hero_image'
]);

4. Inline Critical CSS and Defer Theme Styles

<!-- In your _main.php or head include -->
<head>
    <style>
        /* Critical above-fold CSS */
        body { font-family: system-ui, sans-serif; margin: 0; }
        .header { display: flex; height: 64px; align-items: center; }
        .hero { width: 100%; aspect-ratio: 16/9; }
        .content { max-width: 800px; margin: 0 auto; padding: 1rem; }
    </style>

    <link rel="preload" href="<?= $config->urls->templates ?>styles/main.css"
          as="style"
    <noscript>
        <link rel="stylesheet" href="<?= $config->urls->templates ?>styles/main.css">
    </noscript>
</head>

5. Add Server-Level Caching

# .htaccess in ProcessWire root
<IfModule mod_expires.c>
  ExpiresActive On
  ExpiresByType image/jpeg "access plus 1 year"
  ExpiresByType image/webp "access plus 1 year"
  ExpiresByType image/png "access plus 1 year"
  ExpiresByType text/css "access plus 1 month"
  ExpiresByType application/javascript "access plus 1 month"
</IfModule>

<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE text/html text/css application/javascript
</IfModule>

Measuring LCP on ProcessWire

  1. ProcessWire Debug Mode -- enable $config->debug = true in config.php to see query count and render time in the admin footer
  2. Tracy Debugger module -- install for detailed profiling of selectors, hooks, and template render time
  3. PageSpeed Insights -- test homepage and content-heavy listing pages
  4. TTFB check -- curl -w "TTFB: %{time_starttransfer}\n" -o /dev/null -s https://yoursite.com -- if over 400ms without cache, implement ProCache

Analytics Script Impact

  • ProcessWire has no built-in analytics -- all tracking is manually added to templates
  • Place <script async> tags at the bottom of _main.php before </body>
  • Use ProcessWire's markup regions or $config->scripts->append() to manage script loading order
  • Avoid synchronous scripts in template header includes