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
- Symfony Profiler (dev mode) -- access
/_profilerto see render times for each Twig block and database query count - PageSpeed Insights -- test production with HTTP cache enabled
- Check TTFB -- if TTFB exceeds 600ms, the bottleneck is Symfony/PHP rather than frontend assets
- 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>withdeferattribute - 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