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

Fix LCP Issues on Boltcms (Loading Speed)

Improve Bolt CMS LCP by optimizing Twig template rendering, enabling Symfony cache layers, and preloading above-fold images.

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. Bolt CMS (v5+) is built on Symfony and uses Twig templates, so LCP depends on Symfony's HTTP cache, Twig rendering speed, and theme asset delivery.

Bolt CMS-Specific LCP Causes

  • Symfony HTTP cache disabled -- Bolt 5 includes Symfony's reverse proxy cache but it is off by default, meaning every request hits the PHP render pipeline
  • Twig template complexity -- deeply nested {% include %} and {% embed %} calls with database queries per block
  • Thumbnail generation on first request -- Bolt generates image thumbnails on-the-fly; the first visitor to a new page waits for image processing
  • Unoptimized theme assets -- themes ship with unminified CSS/JS and no critical CSS extraction
  • Extension overhead -- Bolt extensions can add Doctrine queries and inject assets on every page

Fixes

1. Enable Symfony HTTP Cache

Bolt 5 includes Symfony's built-in HTTP cache. Enable it in your front controller:

// public/index.php
use App\Kernel;
use Symfony\Component\HttpKernel\HttpCache\HttpCache;
use Symfony\Component\HttpKernel\HttpCache\Store;

$kernel = new Kernel($_SERVER['APP_ENV'], (bool) $_SERVER['APP_DEBUG']);

// Wrap kernel in HTTP cache for production
if ($_SERVER['APP_ENV'] === 'prod') {
    $kernel = new HttpCache($kernel, new Store(__DIR__.'/../var/cache/http'));
}

Then set cache headers in your controllers or via Bolt's configuration:

# config/packages/framework.yaml
framework:
    http_cache:
        default_ttl: 3600  # Cache pages for 1 hour

Add cache annotations to your Twig templates:

{# In your page template -- set cache lifetime #}
{% block cache %}
    {{ response.setSharedMaxAge(3600) }}
    {{ response.setPublic() }}
{% endblock %}

2. Pre-Generate Thumbnails

Bolt generates thumbnails on first request using the Thumbs controller. Pre-warm them at deploy time:

# Create a simple script to warm thumbnail cache
#!/bin/bash
# warm-thumbnails.sh
SITE_URL="https://yoursite.com"
# Get all image paths from Bolt's database
bolt:console database:export-images | while read img; do
  curl -s -o /dev/null "${SITE_URL}/thumbs/1200x630/${img}"
  curl -s -o /dev/null "${SITE_URL}/thumbs/400x225/${img}"
done

Or configure Bolt's thumbnail settings for WebP output:

# config/bolt/config.yaml
thumbnails:
    default_thumbnail: [800, 600]
    default_cropping: crop
    quality: 80
    save_files: true  # Save generated thumbnails to disk
    allow_upscale: false

3. Optimize Twig Templates for LCP

Reduce template rendering time by minimizing database queries in Twig:

{# BAD: Multiple queries in the template #}
{% for record in records %}
  {% set author = record|related('author') %}  {# Extra query per record #}
  {% set category = record|related('category') %}  {# Another query #}
{% endfor %}

{# GOOD: Use setcontent with eager loading #}
{% setcontent records = 'entries' where { status: 'published' }
   limit 10 returnmultiple
   with { author: true, category: true }
%}

For the hero/banner section that is typically the LCP element:

{# Hero banner with optimized image loading #}
{% set hero = record.field('hero_image') %}
{% if hero is not empty %}
  <img
    src="{{ hero|thumbnail(1920, 600, 'crop') }}"
    srcset="{{ hero|thumbnail(640, 200, 'crop') }} 640w,
            {{ hero|thumbnail(1024, 320, 'crop') }} 1024w,
            {{ hero|thumbnail(1920, 600, 'crop') }} 1920w"
    sizes="100vw"
    width="1920"
    height="600"
    alt="{{ record.field('title') }}"
    loading="eager"
    fetchpriority="high"
  >
{% endif %}

4. Critical CSS and Deferred Assets

Split your theme's CSS into critical and non-critical:

{# In your base.html.twig <head> section #}
<style>
  /* Critical CSS -- only above-fold styles */
  .header { display: flex; height: 64px; align-items: center; }
  .hero { width: 100%; aspect-ratio: 16/5; }
  body { font-family: system-ui, sans-serif; margin: 0; }
</style>

{# Defer full stylesheet #}
<link rel="preload" href="{{ asset('css/theme.css') }}" as="style"
<noscript><link rel="stylesheet" href="{{ asset('css/theme.css') }}"></noscript>

{# Defer all JavaScript #}
<script defer src="{{ asset('js/theme.js') }}"></script>

5. Preload LCP Image

{# In base.html.twig <head> #}
{% block preload %}{% endblock %}

{# In page.html.twig #}
{% block preload %}
  {% if record.field('hero_image') is not empty %}
    <link rel="preload" as="image"
          href="{{ record.field('hero_image')|thumbnail(1920, 600, 'crop') }}"
          type="image/webp">
  {% endif %}
{% endblock %}

Measuring LCP on Bolt CMS

  1. Symfony Profiler (dev mode) -- access /_profiler to see render times for each Twig block and database query count
  2. PageSpeed Insights -- test production with HTTP cache enabled
  3. Check TTFB -- if TTFB exceeds 600ms, the bottleneck is Symfony/PHP rather than frontend assets
  4. Test page types: homepage (usually a content listing with multiple queries), single record pages (hero image is LCP), and taxonomy pages

Analytics Script Impact

Bolt's Twig templates give full control over script placement:

  • Place GTM or analytics scripts at the end of <body> with defer attribute
  • Bolt has no built-in analytics; all tracking is manually added to templates
  • Use Bolt's {{ asset() }} function to leverage Symfony's asset versioning for cache-busting without blocking