Skip to content

Grav Performance Optimization — Page Speed, CDN and Image Optimization

DodaTech Updated 2026-06-27 7 min read

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:

  1. Add your domain
  2. Enable "Proxied" (orange cloud) for DNS records
  3. Enable "Auto Minify" for HTML, CSS, JS
  4. Enable "Brotli" compression
  5. 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

  1. 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.

  2. 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 %}.

  3. 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.

  4. Blocking render with JavaScript: Render-blocking JS delays page rendering. Use defer or async for all non-critical JavaScript. Move scripts to the footer.

  5. 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

  1. 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).

  2. How do you serve different image sizes to different devices? Answer: Use the srcset and sizes attributes in <img> tags. Generate multiple sizes with Grav's resize() action and specify width descriptors.

  3. 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.

  4. 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.

  5. 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

What is a good Lighthouse score for a Grav site?

80+ is good, 90+ is excellent. Grav can achieve 95-100 with proper optimization. Focus on Performance, Accessibility, and Best Practices categories.

How much can image optimization improve page speed?

Image optimization typically reduces page weight by 60-80%. WebP conversion alone saves 30% compared to JPEG. Proper sizing (not loading 1920px images for 400px display slots) saves even more.

Should I use a caching plugin on top of Grav's built-in cache?

No. Grav's built-in caching is comprehensive. Server-level caching (Nginx FastCGI, Varnish) can be added in front of Grav, but avoid plugin-level caching plugins.

How do I measure the performance impact of my changes?

Use Lighthouse (before/after), WebPageTest (waterfall view), and Grav's built-in timer (grav.timer.getTime()). Compare metrics before and after each change.

What is the most common performance bottleneck in Grav?

Unoptimized images — they account for 60-80% of page weight. The second most common is too many HTTP requests from unmerged CSS/JS files.

Mini Project

Goal: Take a Grav site from a baseline score to 95+ Lighthouse.

  1. Measure baseline performance with Lighthouse
  2. Convert all images to WebP with quality 85
  3. Implement responsive images with srcset (4 breakpoints)
  4. Enable and test the asset pipeline
  5. Inline critical CSS, defer non-critical
  6. Add lazy loading to all images and iframes
  7. Configure Redis caching with 7-day TTL
  8. Set up Cloudflare CDN
  9. Optimize PHP and Nginx configuration
  10. 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