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

Fix LCP Issues on Modx (Loading Speed)

Speed up MODX LCP by tuning chunk/snippet caching, optimizing template variable output, and compressing media source images.

Largest Contentful Paint (LCP) measures how quickly the main content of your MODX site loads. Poor LCP directly impacts SEO rankings and conversion rates.

Target: LCP under 2.5 seconds Good: Under 2.5s | Needs Improvement: 2.5-4.0s | Poor: Over 4.0s

For general LCP concepts, see the global LCP guide.

MODX-Specific LCP Issues

1. Template Parsing and Rendering Time

MODX must parse templates, process tags, and execute snippets before sending HTML. This can delay LCP.

Problem: Slow template generation delaying entire page load.

Diagnosis:

  • Add timing code to template:
[[!+modx.startTime:default=`[[!+modx.config.request_time:set=`modx.startTime`]]`]]
<!-- Your template content -->
<!-- Page generated in [[!+modx.totalTime:default=`[[!+modx.config.request_time:subtract=`[[!+modx.startTime]]`:set=`modx.totalTime`]]`]] seconds -->
  • Check MODX Manager: ReportsSystem Info → Enable debugging
  • Use MODX Console plugin to profile snippet execution

Solutions:

A. Enable and Optimize MODX Caching

Most critical optimization for MODX:

Enable Resource Caching:

  1. Edit resource
  2. Page Settings tab
  3. Check Cacheable
  4. Check Empty Cache
  5. Set Cache Refresh to appropriate value (e.g., 3600 for 1 hour)

Global Cache Settings:

System Settings:
cache_expires → 3600 (or higher)
cache_handler → xPDOFileCache (default, fastest)

Verify cache is working:

// Add to template temporarily
[[!+modx.resource.cacheable:default=`Not cached!`]]

B. Cache Snippets

Convert uncached snippets to cached when possible:

<!-- Before: Uncached (slower) -->
[[!getResources? &parents=`5` &limit=`10`]]

<!-- After: Cached (faster) -->
[[getResources? &parents=`5` &limit=`10`]]

When to use uncached snippets:

  • User-specific content (cart, user profile)
  • Real-time data (stock levels, current time)
  • Form tokens
  • Session-dependent content

When to cache:

  • Product listings
  • Article/blog listings
  • Navigation menus
  • Static content blocks

C. Optimize Heavy Snippets

getResources optimization:

<!-- Bad: No limit, processes all resources -->
[[!getResources? &parents=`5`]]

<!-- Good: Limited results, specific fields -->
[[getResources?
  &parents=`5`
  &limit=`10`
  &includeContent=`0`
  &includeTVs=`0`
  &processTVs=`0`
]]

pdoTools instead of getResources:

<!-- pdoResources is faster than getResources -->
[[pdoResources?
  &parents=`5`
  &limit=`10`
  &includeTVs=`thumbnail`
  &prepareTVs=`1`
]]

2. Unoptimized Images in Templates and TVs

Images from chunks, TVs, or uploaded via MODX File Browser often lack optimization.

Problem: Large hero images or product images loading slowly.

Diagnosis:

  • Run PageSpeed Insights
  • Check if images flagged as LCP element
  • Look for "Properly size images" warning
  • Inspect image file sizes in MODX File Browser

Solutions:

A. Use phpThumb for Image Resizing

MODX includes phpThumb for dynamic resizing:

<!-- Before: Full-size image -->
<img src="[[*hero_image]]" alt="[[*pagetitle]]">

<!-- After: Resized with phpThumb -->
<img
  src="[[*hero_image:phpthumb=`w=1920&h=600&zc=1`]]"
  srcset="[[*hero_image:phpthumb=`w=375&h=200&zc=1`]] 375w,
          [[*hero_image:phpthumb=`w=750&h=400&zc=1`]] 750w,
          [[*hero_image:phpthumb=`w=1920&h=600&zc=1`]] 1920w"
  sizes="100vw"
  width="1920"
  height="600"
  alt="[[*pagetitle]]"
  loading="eager"
>

phpThumb parameters:

  • w = width
  • h = height
  • zc=1 = zoom/crop
  • q=80 = quality (80 is good balance)

For pThumb extra (better performance):

[[*hero_image:pthumb=`w=1920&h=600&zc=1&q=80`]]

B. Set Explicit Dimensions

Always include width/height to prevent layout shift:

<!-- Get image dimensions from TV -->
<img
  src="[[*hero_image:phpthumb=`w=1920`]]"
  width="1920"
  height="600"
  alt="[[*pagetitle]]"
  loading="eager"
>

C. Preload LCP Image

Add to template <head>:

<!-- Preload hero image on homepage -->
[[*id:is=`1`:then=`
<link
  rel="preload"
  as="image"
  href="[[*hero_image:phpthumb=`w=1920&h=600&zc=1`]]"
  imagesrcset="[[*hero_image:phpthumb=`w=375&h=200&zc=1`]] 375w,
               [[*hero_image:phpthumb=`w=1920&h=600&zc=1`]] 1920w"
  imagesizes="100vw"
>
`]]

D. Use WebP Format

Install ImageSlim or similar extra to convert to WebP:

[[*hero_image:phpthumb=`w=1920&h=600&f=webp&q=80`]]

Or use picture element for fallback:

<picture>
  <source srcset="[[*hero_image:phpthumb=`w=1920&f=webp&q=80`]]" type="image/webp">
  <img src="[[*hero_image:phpthumb=`w=1920&q=80`]]" alt="[[*pagetitle]]" width="1920" height="600">
</picture>

3. Plugin Overhead

Plugins executing on every page can significantly delay rendering.

Problem: Plugins on OnWebPagePrerender adding processing time.

Diagnosis:

Check plugin execution time:

<?php
// Add to plugin start
$startTime = microtime(true);

// Plugin code here...

// At plugin end
$execTime = microtime(true) - $startTime;
$modx->log(modX::LOG_LEVEL_INFO, "Plugin {$modx->event->name} took {$execTime}s");

Audit active plugins:

  1. ElementsPlugins
  2. Note which plugins are enabled
  3. Check System Events tab for each
  4. Disable non-essential plugins for testing

Solutions:

A. Disable Unnecessary Plugins

Disable plugins that run on every page but aren't needed:
- Development/debug plugins in production
- Unused tracking plugins
- Legacy plugins

B. Optimize Plugin Code

<?php
/**
 * Optimized Plugin Example
 * Only execute when necessary
 */

// Exit early if not needed
if ($modx->context->key === 'mgr') return; // Skip in manager

// Cache expensive operations
$cacheKey = 'plugin_data_' . $modx->resource->id;
$cacheOptions = [xPDO::OPT_CACHE_KEY => 'custom'];

$data = $modx->cacheManager->get($cacheKey, $cacheOptions);

if (!$data) {
    // Expensive operation only if not cached
    $data = performExpensiveOperation();
    $modx->cacheManager->set($cacheKey, $data, 3600, $cacheOptions);
}

// Use cached data...

C. Move Logic to Snippets

Instead of plugin on every page, use cached snippet on specific templates:

<!-- Instead of plugin on all pages -->
[[snippetName]]  <!-- Only on pages that need it -->

4. Template JavaScript and CSS

Render-blocking scripts in template head delay LCP.

Problem: JavaScript and CSS files blocking page render.

Diagnosis:

Solutions:

A. Use Defer for JavaScript

In template:

<!-- Before: Blocking -->
<script src="assets/js/theme.js"></script>

<!-- After: Deferred -->
<script src="assets/js/theme.js" defer></script>

<!-- Or async for non-dependent scripts -->
<script src="assets/js/analytics.js" async></script>

In chunk or template:

<script defer src="[[++assets_url]]js/theme.js"></script>

B. Inline Critical CSS

Create chunk with critical CSS:

Chunk: critical_css

/* Critical above-fold styles */
body { margin: 0; font-family: Arial, sans-serif; }
.header { background: #333; color: #fff; padding: 1rem; }
.hero { height: 600px; background-size: cover; }

In template head:

<style>
  [[$critical_css]]
</style>

<!-- Load full CSS asynchronously -->
<link
  rel="preload"
  href="[[++assets_url]]css/theme.css"
  as="style"
>
<noscript>
  <link rel="stylesheet" href="[[++assets_url]]css/theme.css">
</noscript>

C. Minimize Template Output

<!-- Remove whitespace in production -->
[[++minify_templates:is=`1`:then=`<!-- minified -->`]]

<!-- Use ClientConfig for settings -->
[[!++site_name]]  // Better than hardcoded

5. Database Query Optimization

Multiple TV queries and resource calls slow page generation.

Problem: Too many database queries for TVs and related resources.

Diagnosis:

Enable query logging temporarily:

System Settings:
debug → Yes
log_level → MODX_LOG_LEVEL_DEBUG

Check ReportsError Log for query counts.

Solutions:

A. Reduce TV Usage

<!-- Bad: Each TV = separate query if not processed -->
[[*tv1]]
[[*tv2]]
[[*tv3]]

<!-- Better: Process TVs in snippet -->
[[getTVs? &tvs=`tv1,tv2,tv3`]]

Or use pdoTools to fetch TVs efficiently:

[[pdoResources?
  &parents=`5`
  &includeTVs=`tv1,tv2,tv3`
  &prepareTVs=`1`
]]

B. Use Collections or Single Parent

Instead of multiple parent queries:

<!-- Bad: Multiple queries -->
[[getResources? &parents=`5,6,7,8,9`]]

<!-- Better: Single parent or depth -->
[[getResources? &parents=`5` &depth=`1`]]

C. Limit getResources Calls

<!-- Always set limits -->
[[getResources?
  &parents=`5`
  &limit=`10`        // Don't fetch all
  &offset=`0`
  &includeContent=`0` // Skip if not needed
  &includeTVs=`0`     // Skip if not needed
]]

6. External Resources and Third-Party Scripts

GTM, analytics, and tracking scripts can delay LCP.

Problem: Analytics and marketing scripts blocking render.

Solutions:

A. Load Analytics Asynchronously

<!-- GTM with async -->
<script async>(function(w,d,s,l,i){...GTM code...})</script>

<!-- GA4 with async -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-XXXXXX"></script>

B. Defer Non-Critical Tracking

<!-- Load tracking after LCP -->
<script>
  window.addEventListener('load', function() {
    // Load non-critical tracking
    setTimeout(function() {
      // Initialize chat widgets, social plugins, etc.
    }, 2000);
  });
</script>

C. Use GTM to Control Script Loading

Configure GTM to load tags after LCP:

  • Trigger: DOM Ready or Window Loaded
  • Instead of: All Pages (immediate)

7. Font Loading

Custom fonts in templates can delay LCP.

Problem: Web fonts blocking render.

Solutions:

A. Preload Fonts

<!-- In template head -->
<link
  rel="preload"
  href="[[++assets_url]]fonts/custom-font.woff2"
  as="font"
  type="font/woff2"
  crossorigin
>

B. Use font-display: swap

@font-face {
  font-family: 'CustomFont';
  src: url('[[++assets_url]]fonts/custom-font.woff2') format('woff2');
  font-display: swap; /* Show fallback immediately */
  font-weight: 400;
  font-style: normal;
}

C. Limit Font Variants

Only load fonts you actually use:

/* Bad: Loading too many variants */
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;600;700&display=swap');

/* Better: Only what's needed */
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;700&display=swap');

MODX Hosting and Server Optimization

Enable OPcache (PHP)

Dramatically improves PHP performance:

; php.ini
opcache.enable=1
opcache.memory_consumption=128
opcache.max_accelerated_files=10000
opcache.revalidate_freq=60

Verify in SystemSystem InfoPHP Info: Look for OPcache enabled.

Enable MODX Cache

System Settings:
cache_disabled → No (ensure cache is enabled)
cache_expires → 3600 or higher
cache_handler → xPDOFileCache (fastest)

Use PHP 8.0+

MODX Revolution 3.x supports PHP 8.0+, which is significantly faster:

  • Check current version: SystemSystem Info
  • Upgrade PHP via hosting control panel or contact host
  • Test thoroughly after upgrade

Enable GZIP Compression

In .htaccess:

# Enable GZIP compression
<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript
</IfModule>

Database Optimization

Optimize MODX database tables:

-- Via phpMyAdmin or command line
OPTIMIZE TABLE modx_site_content;
OPTIMIZE TABLE modx_site_tmplvars;
OPTIMIZE TABLE modx_site_tmplvar_contentvalues;

Or use MODX plugin:

<?php
// Run weekly via cron
$tables = [
    $modx->getTableName('modResource'),
    $modx->getTableName('modTemplateVar'),
    $modx->getTableName('modTemplateVarResource')
];

foreach ($tables as $table) {
    $modx->exec("OPTIMIZE TABLE {$table}");
}

Testing & Monitoring

Test LCP

Tools:

  1. PageSpeed Insights - Lab and field data
  2. WebPageTest - Detailed waterfall
  3. Chrome DevToolsLighthouse - Local testing
  4. Chrome DevToolsPerformance - Identify LCP element

Test Multiple Pages:

  • Homepage (often slowest)
  • Content pages
  • Product pages (if e-commerce)
  • Different templates

Test Conditions:

  • Desktop and mobile
  • Fast 3G and 4G (throttling in DevTools)
  • Different geographic locations

Monitor Over Time

Google Search Console:

Chrome User Experience Report (CrUX):

  • Real user data in PageSpeed Insights
  • 28-day rolling average

Custom Monitoring:

// Log page generation time
[[!+modx.totalTime]]

Track in GA4 or analytics to monitor trends.

Quick Wins Checklist

Start here for immediate LCP improvements:

  • Enable MODX cache for all published resources
  • Cache all snippets that don't need real-time data
  • Resize and optimize hero images with phpThumb
  • Add explicit width/height to images
  • Preload LCP image in template head
  • Use defer on non-critical JavaScript
  • Disable unused plugins
  • Limit getResources queries (max 10-20 results)
  • Skip includeContent and includeTVs when not needed
  • Enable OPcache on server
  • Test with PageSpeed Insights

Common LCP Elements in MODX

Page Type Common LCP Element Optimization Priority
Homepage Hero banner from TV Highest - Use phpThumb, preload
Articles Featured image chunk High - Optimize and set dimensions
Products Product image TV Highest - Resize, lazy load others
Listings First getResources item Medium - Cache snippet, limit

When to Hire a Developer

Consider hiring a MODX developer if:

  • LCP consistently over 4 seconds after optimizations
  • Complex custom snippets need optimization
  • Database queries require advanced optimization
  • Need custom caching strategy
  • Server configuration changes needed

Find MODX developers: MODX Professional Directory

Next Steps

For general LCP optimization strategies, see LCP Optimization Guide.