Drupal Performance Optimization — Caching, CDN and Server Tuning
In this tutorial, you'll learn how to optimize Drupal performance: configuring caching subsystems, integrating Varnish for HTTP caching, setting up CDN with Cloudflare or Fastly, tuning database queries, optimizing PHP configuration, and using monitoring tools like New Relic.
What You'll Learn
- Drupal cache subsystems: Page, Dynamic Page, Render, Views, Block, Entity, Twig, PHP OPcache
- Varnish integration with the Purge module and ban expressions
- CDN integration with Cloudflare, Fastly, and Akamai
- BigPipe optimization for Lazy Loading blocks
- Database optimization: Solr/Elasticsearch, MySQL query tuning, Devel query logging
- PHP tuning: OPcache, memory_limit, max_execution_time, realpath_cache
- Web server tuning: Nginx fastcgi_cache, Apache mod_cache, KeepAlive
- Image optimization: ImageAPI, WebP, lazy loading
- CSS/JS aggregation and bandwidth optimization
- Profiling with WebProfiler, XHProf, and New Relic
- Load testing with Apache Bench, Siege, JMeter
Why It Matters
A slow Drupal site loses visitors, ranks lower in search results, and costs more in server resources. Drupal's caching system is powerful but requires intentional configuration. Without caching enabled, each page request loads dozens of modules, runs multiple database queries, and renders Twig templates from scratch. With proper caching, the same page loads in milliseconds. For sites with thousands of concurrent users, performance optimization is the difference between a usable site and a timeout error.
Real-World Use
A news website with 2 million monthly visitors runs on Drupal. Without optimization, page load time was 6 seconds. After enabling page caching, views caching, and BigPipe, load time dropped to 1.5 seconds. After adding Varnish and a CDN, time to first byte dropped to 200 milliseconds. The site now handles traffic spikes during breaking news without crashing. The optimization paid for itself in reduced server costs and higher ad revenue from improved engagement.
Learning Path
flowchart LR A[Migrate API] --> B[Performance Optimization] B --> C[Cache Subsystems] C --> D[Varnish and CDN] D --> E[BigPipe Optimization] E --> F[Database Tuning] F --> G[PHP and Server Tuning] G --> H[Monitoring and Testing] H --> I[Backups and Go-Live]
Drupal Cache Subsystems
Drupal has multiple caching layers. Understanding each one helps you configure them correctly.
Page Cache
Caches the entire HTML output for anonymous users:
<?php
// settings.php: enable page cache
$settings['cache']['bins']['page'] = 'cache.backend.database';
$config['system.performance']['cache']['page']['use_internal'] = true;
$config['system.performance']['cache']['page']['max_age'] = 21600; // 6 hours
Dynamic Page Cache
Caches pages for authenticated users, excluding personalized content:
<?php
// settings.php: enable dynamic page cache
$settings['cache']['bins']['dynamic_page_cache'] = 'cache.backend.database';
Render Cache
Caches rendered entities and individual render elements:
<?php
// Render cache is enabled by default
// It stores rendered output of entities, blocks, etc.
Views Cache
Views caching stores the query results and rendered output:
# Configure Views caching at /admin/structure/views
# Settings per display:
# - None: no caching (slowest)
# - Tag-based: invalidates when related content changes
# - Time-based: expires after N seconds
<?php
// Programmatically set views cache
$view = \Drupal\views\Views::getView('articles');
$view->setDisplay('page_1');
$view->display_handler->overrideOption('cache', [
'type' => 'tag',
]);
Block Cache
Each block can be cached independently:
<?php
// In a custom block plugin
class MyBlock extends BlockBase {
public function build() {
return [
'#markup' => $this->t('Hello!'),
'#cache' => [
'max-age' => 3600,
'contexts' => ['user.roles'],
'tags' => ['node_list'],
],
];
}
public function getCacheMaxAge() {
return 3600;
}
}
Twig Cache
Twig templates are compiled to PHP and cached:
<?php
// settings.php: Twig caching for production
$settings['twig_debug'] = false;
$settings['cache']['bins']['render'] = 'cache.backend.database';
PHP OPcache
OPcache caches compiled PHP scripts. Enable it in php.ini:
; php.ini
opcache.enable=1
opcache.memory_consumption=256
opcache.interned_strings_buffer=16
opcache.max_accelerated_files=20000
opcache.revalidate_freq=60
opcache.validate_timestamps=0 ; Production: disable validation
Varnish Integration
Varnish is an HTTP accelerator that sits in front of Drupal:
# default.vcl - Varnish configuration for Drupal
vcl 4.0;
backend default {
.host = "127.0.0.1";
.port = "8080";
}
sub vcl_recv {
# Only cache GET and HEAD requests
if (req.method != "GET" && req.method != "HEAD") {
return (pass);
}
# Bypass cache for authenticated users
if (req.http.Cookie ~ "SESS") {
return (pass);
}
# Remove all cookies except session
if (req.http.Cookie) {
set req.http.Cookie = "";
}
# Strip query strings for static files
if (req.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|svg|webp)$") {
unset req.http.Cookie;
}
}
sub vcl_backend_response {
# Set TTL for uncacheable items
if (beresp.ttl <= 0s) {
set beresp.ttl = 120s;
}
# Cache static files for 30 days
if (bereq.url ~ "\.(css|js|png|jpg|jpeg|gif|ico|svg|webp)$") {
set beresp.ttl = 30d;
}
}
# Install the Purge module for cache invalidation
composer require drupal/purge drush/drush
drush pm:enable purge purge_drush
# Configure purge processors
drush config:set purge.plugins core '{"plugin_id": "core"}'
CDN Integration
Cloudflare
# Install Cloudflare module
composer require drupal/cloudflare
drush pm:enable cloudflare
<?php
// settings.php: Cloudflare configuration
$settings['reverse_proxy'] = true;
$settings['reverse_proxy_addresses'] = [
'173.245.48.0/20',
'103.21.244.0/22',
'103.22.200.0/22',
// Full list at https://www.cloudflare.com/ips
];
$settings['reverse_proxy_trusted_headers'] =
\Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_FOR |
\Symfony\Component\HttpFoundation\Request::HEADER_X_FORWARDED_PROTO;
Fastly
composer require drupal/fastly
drush pm:enable fastly
Configure Fastly service ID and API key at /admin/config/services/fastly.
BigPipe Optimization
BigPipe loads page content progressively, sending the main content first and loading personalized blocks (shopping cart, user menu) asynchronously:
<?php
// settings.php: enable BigPipe
$settings['big_pipe'] = true;
// BigPipe is enabled by default in Drupal 10+.
// Blocks marked with #cache context 'user' or 'session'
// are loaded via BigPipe after the main content.
<?php
// In a custom block
public function build() {
return [
'#markup' => $this->getDynamicContent(),
'#cache' => [
'contexts' => ['user'],
'max-age' => 0, // Never cache personalized content
],
];
}
Database Optimization
Query Logging with Devel
composer require drupal/devel
drush pm:enable devel
<?php
// Enable query logging in settings.php for development
$settings['devel'] = [
'query_sort' => 'source',
'query_count' => 100,
];
Database Tuning
-- MySQL performance tuning for Drupal
SET GLOBAL innodb_buffer_pool_size = 1G;
SET GLOBAL query_cache_size = 256M;
SET GLOBAL tmp_table_size = 64M;
SET GLOBAL max_allowed_packet = 128M;
-- Analyze slow queries
SHOW FULL PROCESSLIST;
SHOW VARIABLES LIKE 'slow_query%';
-- Common Drupal query optimizations
CREATE INDEX node_created_idx ON node_field_data (created DESC);
CREATE INDEX node_title_idx ON node_field_data (title);
Search with Solr/Elasticsearch
# Install search API
composer require drupal/search_api drupal/search_api_solr
drush pm:enable search_api search_api_solr
PHP Tuning
; php.ini for production Drupal
memory_limit = 256M
max_execution_time = 30
max_input_time = 60
upload_max_filesize = 64M
post_max_size = 64M
realpath_cache_size = 256K
realpath_cache_ttl = 600
; Session handling
session.gc_probability = 1
session.gc_divisor = 1000
session.gc_maxlifetime = 200000
session.save_path = /var/lib/php/sessions
; OPcache
opcache.enable = 1
opcache.memory_consumption = 256
opcache.interned_strings_buffer = 16
opcache.max_accelerated_files = 20000
opcache.revalidate_freq = 60
opcache.fast_shutdown = 1
Web Server Tuning
Nginx with fastcgi_cache
# nginx.conf: Drupal fastcgi caching
fastcgi_cache_path /var/cache/nginx levels=1:2 keys_zone=DRUPAL:10m inactive=60m;
server {
listen 80;
server_name example.com;
root /var/www/web;
# Cache zone for anonymous users
set $cache_bypass 0;
if ($cookie_SESS) {
set $cache_bypass 1;
}
location ~ \.php$ {
fastcgi_pass unix:/var/run/php/php8.3-fpm.sock;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
# Only cache if no session cookie
fastcgi_cache DRUPAL;
fastcgi_cache_bypass $cache_bypass;
fastcgi_no_cache $cache_bypass;
fastcgi_cache_valid 200 301 302 10m;
fastcgi_cache_use_stale updating error timeout;
}
}
Apache mod_cache
# .htaccess: Enable cache
<IfModule mod_cache.c>
CacheEnable disk /
CacheHeader on
CacheDefaultExpire 3600
CacheMaxExpire 86400
CacheIgnoreNoLastMod On
CacheStorePrivate On
</IfModule>
KeepAlive
# httpd.conf: Apache KeepAlive
KeepAlive On
MaxKeepAliveRequests 100
KeepAliveTimeout 5
Image Optimization
# Install ImageAPI Optimize
composer require drupal/imageapi_optimize drupal/imageapi_optimize_webp
drush pm:enable imageapi_optimize imageapi_optimize_webp
# Image style configuration
# Convert to WebP format
binaries:
webp: /usr/bin/cwebp
# Image style: Large (1200px wide)
name: large_webp
effects:
scale:
width: 1200
height: null
upscale: false
convert:
format: webp
quality: 85
Enable lazy loading for all images:
<?php
// In your theme or module
function mytheme_preprocess_image(&$variables) {
$variables['attributes']['loading'] = 'lazy';
}
CSS/JS Aggregation
<?php
// settings.php: enable aggregation
$config['system.performance']['css']['preprocess'] = true;
$config['system.performance']['js']['preprocess'] = true;
// Enable bandwidth optimization
$config['system.performance']['css']['gzip'] = true;
$config['system.performance']['js']['gzip'] = true;
Profiling and Monitoring
WebProfiler
drush pm:enable devel
# Access at /admin/devel -> http://example.com/admin/devel
New Relic
# newrelic.ini
newrelic.appname = "Drupal Production"
newrelic.daemon.logfile = /var/log/newrelic/daemon.log
newrelic.error_collector.enabled = true
newrelic.transaction_tracer.enabled = true
newrelic.transaction_tracer.threshold = "apdex_f"
Load Testing
# Apache Bench (ab)
ab -n 1000 -c 10 http://example.com/
# Siege
siege -c 50 -t 60s http://example.com/
# JMeter (GUI-based)
jmeter -n -t test-plan.jmx -l results.jtl
Common Mistakes
Not enabling page cache for anonymous users: By default, Drupal's page cache is off. Anonymous users get uncached pages, wasting server resources on every request.
Over-caching without invalidation Strategy: If you cache everything with long TTLs, stale content stays visible. Use cache tags and Purge for intelligent invalidation.
Enabling all cache bins without understanding them: Different caches serve different purposes. The render cache helps authenticated users. The page cache only helps anonymous. Configure each based on your traffic patterns.
Not using a CDN for static assets: Serving CSS, JS, and images from the same server as PHP requests increases load. A CDN offloads these files to edge servers closer to visitors.
Skipping database query optimization: A single unoptimized Views query can take seconds. Use Devel to identify slow queries, add database indexes, and optimize Views configurations.
Practice Questions
- What is the difference between Page Cache and Dynamic Page Cache, and which users does each serve?
- How would you configure BigPipe to load a user's shopping cart asynchronously while the rest of the page loads immediately?
- You notice that the site is slow for anonymous users. What are the first three things you check?
- Challenge: Set up a complete caching strategy for an e-commerce site built with Drupal. The site has: product listing pages (same for all users), a shopping cart (personalized per user), user account pages (personalized), admin pages (no caching), and static assets (images, CSS, JS). For each type of page, specify: which cache layer applies, the cache TTL, how invalidation works, and whether BigPipe applies.
FAQ
Mini Project
Goal: Performance-optimize a Drupal site for production traffic.
- Enable all relevant cache systems: page cache, dynamic page cache, render cache, views cache, Twig cache
- Enable CSS and JS aggregation
- Install and configure the Purge module with Varnish (or simulate with internal page cache)
- Set up image optimization: install ImageAPI Optimize, create a WebP image style, enable lazy loading
- Tune PHP OPcache settings for production
- Configure Nginx or Apache caching headers for static files
- Run a load test before and after optimization, measuring: requests per second, average response time, error rate, peak memory usage
- Document the performance improvement with before/after metrics
- Set up New Relic or a similar monitoring tool to track performance over time
What's Next
Now that you understand performance optimization, proceed to backups and maintenance to learn how to keep your Drupal site healthy. Then explore go-live checklist for a complete pre-launch readiness guide.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro