Grav Performance Optimization — Page Speed, CDN and Image Optimization
In this tutorial, you'll learn Grav performance optimization — improving page speed scores, integrating a CDN, optimizing images, implementing lazy loading, performance testing, and achieving high Lighthouse scores.
What You'll Learn
- Page speed optimization strategies for Grav
- CDN integration for static assets
- Image optimization: format, size, compression
- Lazy loading for images and content
- Performance testing with Lighthouse and WebPageTest
- Server-level optimization (PHP, Nginx, Apache)
Why It Matters
In WordPress, performance optimization requires multiple plugins and server tweaks. In Grav, performance is a core design principle — flat-file architecture, built-in caching, and asset pipeline. But even a fast CMS can be slowed down by unoptimized images, too many HTTP requests, or misconfigured server settings. Performance optimization is the key to good user experience, high search rankings, and low hosting costs.
Real-World Use
A marketing site was scoring 45 on Google Lighthouse (mobile). After implementing image optimization (WebP conversion, responsive images), enabling the asset pipeline, adding lazy loading, and configuring a CDN, the score jumped to 92. Organic traffic increased by 35% in the following month, directly attributed to the improved page speed.
Learning Path
flowchart LR
A["Caching Deep Dive"] --> B["Performance Optimization
← You are here"]:::current
B --> C["Security"]
C --> D["Git Workflow"]
D --> E["CLI Tools"]
E --> F["Production Deployment"]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
Image Optimization
WebP Conversion
Convert images to WebP format in templates:
{% set image = page.media['photo.jpg'] %}
<img src="{{ image.resize(800, 600).format('webp').quality(85).url }}"
alt="{{ page.title }}"
loading="lazy" />
Batch WebP Conversion
# Convert all JPEG/PNG in assets to WebP
for file in $(find user/themes/mytheme/images -name "*.jpg" -o -name "*.png"); do
convert "$file" -quality 85 "${file%.*}.webp"
done
Responsive Images
{% set image = page.media['hero.jpg'] %}
<img
src="{{ image.resize(480, 320).format('webp').quality(85).url }}"
srcset="
{{ image.resize(480, 320).format('webp').url }} 480w,
{{ image.resize(768, 512).format('webp').url }} 768w,
{{ image.resize(1200, 800).format('webp').url }} 1200w,
{{ image.resize(1920, 1280).format('webp').url }} 1920w
"
sizes="(max-width: 480px) 100vw, (max-width: 768px) 100vw,
(max-width: 1200px) 100vw, 1920px"
loading="lazy"
decoding="async"
alt="{{ page.title }}"
/>
Asset Pipeline Optimization
Enable all pipeline features:
# user/config/system.yaml
assets:
css_pipeline: true
css_minify: true
js_pipeline: true
js_minify: true
enable_asset_timestamp: true
collections:
jquery: 'system://assets/jquery/jquery.min.js?v=3.6.0'
Inline Critical CSS
{% block stylesheets %}
{# Inline critical CSS #}
{% do assets.addInlineCss("
body { font-family: system-ui, sans-serif; line-height: 1.6; }
header { background: var(--primary, #3498db); padding: 1rem; }
") %}
{{ assets.css('critical') }}
{# Load non-critical CSS asynchronously #}
{% do assets.addCss('theme://css/styles.css', {
priority: 10,
loading: 'async'
}) %}
{% endblock %}
Defer Non-Critical JavaScript
{% do assets.addJs('theme://js/main.js', { loading: 'defer', group: 'footer' }) %}
{% do assets.addJs('theme://js/analytics.js', { loading: 'async', group: 'footer' }) %}
{% do assets.addJs('theme://js/vendor.js', { loading: 'defer', group: 'footer' }) %}
Lazy Loading
Native Lazy Loading
<img src="{{ image.url }}" loading="lazy" alt="" />
<iframe src="{{ url }}" loading="lazy" title="Embedded content"></iframe>
Lazy Loading for Background Images
<div class="lazy-bg" data-bg="{{ image.resize(1200, 800).url }}">
<div class="content">{{ page.title }}</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
var lazyBgs = document.querySelectorAll('.lazy-bg');
var observer = new IntersectionObserver(function(entries) {
entries.forEach(function(entry) {
if (entry.isIntersecting) {
entry.target.style.backgroundImage =
'url(' + entry.target.dataset.bg + ')';
observer.unobserve(entry.target);
}
});
});
lazyBgs.forEach(function(bg) { observer.observe(bg); });
});
</script>
CDN Integration
Cloudflare
Configure Cloudflare in user/config/system.yaml:
system:
cache:
gzip: true
assets:
enable_asset_timestamp: true
In Cloudflare dashboard:
- Add your domain
- Enable "Proxied" (orange cloud) for DNS records
- Enable "Auto Minify" for HTML, CSS, JS
- Enable "Brotli" compression
- Configure "Page Rules" for static asset caching
Custom CDN URLs
# user/config/system.yaml
assets:
cdn: 'https://cdn.dodatech.com'
This prepends the CDN URL to all asset paths.
Server-Level Optimization
PHP Configuration
; php.ini optimization
memory_limit = 256M
max_execution_time = 60
opcache.enable = 1
opcache.memory_consumption = 128
opcache.max_accelerated_files = 10000
opcache.revalidate_freq = 2
Nginx Configuration
# /etc/nginx/sites-available/grav
# Gzip
gzip on;
gzip_types text/plain text/css application/json application/javascript
text/xml application/xml text/javascript image/svg+xml;
gzip_min_length 256;
gzip_comp_level 6;
# Cache static assets
location ~* \.(jpg|jpeg|png|gif|ico|webp|svg|css|js|woff|woff2)$ {
expires 365d;
add_header Cache-Control "public, immutable";
}
# FastCGI cache
fastcgi_cache_path /var/cache/nginx/grav levels=1:2 keys_zone=grav:10m
inactive=60m;
location ~ \.php$ {
include fastcgi_params;
fastcgi_pass unix:/var/run/php/php8.1-fpm.sock;
fastcgi_cache grav;
fastcgi_cache_valid 200 60m;
fastcgi_cache_use_stale error timeout updating;
}
Performance Testing
Lighthouse CLI
# Install Lighthouse
npm install -g lighthouse
# Test a page
lighthouse https://example.com --view
# Generate JSON report
lighthouse https://example.com --output=json --output-path=report.json
WebPageTest
# Use the WebPageTest API
curl "https://www.webpagetest.org/runtest.php?url=https://example.com&f=json"
Custom Performance Monitoring
// Add to base template
{% if grav.user.authorize('admin.super') %}
<div class="performance-metrics">
<span>Page generated in: {{ grav.timer.getTime() }}ms</span>
<span>Memory: {{ grav.timer.getMemory() }}</span>
</div>
{% endif %}
Learning Path
flowchart LR
A["Caching Deep Dive"] --> B["Performance Optimization
← You are here"]:::current
B --> C["Security"]
C --> D["Git Workflow"]
D --> E["CLI Tools"]
classDef current fill:#38bdf8,color:#0f172a,stroke-width:2px
Common Mistakes
Not optimizing images before uploading: Uploading 5MB JPEG images slows every page load. Resize images to their display size and compress them before uploading. Use WebP format for 30% smaller files.
Loading all assets on every page: Include conditional asset loading. Blog-specific CSS should only load on blog pages. Use
{% if page.template == 'blog' %}{% do assets.addCss(...)%}{% endif %}.Not using a CDN: A CDN serves assets from servers close to the user, reducing latency. Without a CDN, a user in India downloads assets from a server in the US.
Blocking render with JavaScript: Render-blocking JS delays page rendering. Use
deferorasyncfor all non-critical JavaScript. Move scripts to the footer.Ignoring mobile performance: Desktop performance is usually good. Mobile performance (3G networks, slower CPUs) is the real challenge. Test on mobile with Lighthouse's mobile preset.
Practice Questions
What are the three biggest performance gains for a Grav site? Answer: Enable caching with Redis, enable asset pipeline (CSS/JS merging and Minification), and optimize images (WebP format, responsive sizes, lazy loading).
How do you serve different image sizes to different devices? Answer: Use the
srcsetandsizesattributes in<img>tags. Generate multiple sizes with Grav'sresize()action and specify width descriptors.What is the purpose of
loading="lazy"on images? Answer: It tells the browser to defer loading offscreen images until the user scrolls near them. This reduces initial page load size and speeds up the first paint.How does a CDN improve Grav site performance? Answer: A CDN caches static assets (CSS, JS, images) on servers worldwide. Users download assets from the nearest server, reducing latency. This can cut load times by 50-70% for global audiences.
Challenge: Optimize a Grav site from a baseline Lighthouse score of 40 to 90+. Start by measuring the baseline with Lighthouse. Implement: WebP image conversion with responsive srcset, asset pipeline with CSS/JS merging and minification, critical CSS inlining, deferred JavaScript, lazy loading for all below-the-fold images, CDN integration (Cloudflare), Redis caching, OPcache configuration, and Nginx FastCGI caching. Measure progress after each optimization and document the score improvements.
FAQ
Mini Project
Goal: Take a Grav site from a baseline score to 95+ Lighthouse.
- Measure baseline performance with Lighthouse
- Convert all images to WebP with quality 85
- Implement responsive images with srcset (4 breakpoints)
- Enable and test the asset pipeline
- Inline critical CSS, defer non-critical
- Add lazy loading to all images and iframes
- Configure Redis caching with 7-day TTL
- Set up Cloudflare CDN
- Optimize PHP and Nginx configuration
- Measure final score, document the improvements
What's Next
Now your site is optimized for speed. Next, learn security:
Continue to Lesson 37: Security — Hardening, .htaccess, CSP headers, and XSS prevention.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro