Performance best practices
6.5 Performance Best Practices
When to Use This Section
- You're optimizing theme performance
- You need guidance on caching strategies
- You're implementing responsive images and modern formats
- You want to optimize asset loading
Render Caching: Cache Tags, Contexts, and Max-Age
CRITICAL CONCEPT: Three Dimensions of Caching
1. CACHE TAGS — "What data does this depend on?"
$build['#cache']['tags'] = [
'node:123', // Invalidate when node 123 changes
'user:456', // Invalidate when user 456 changes
'config:system.site', // Invalidate when site config changes
'node_list:article', // Invalidate when any article changes
];
WHY: When tagged data changes, Drupal automatically invalidates cache.
COMMON MISTAKE:
// BAD: No cache tags
$build = ['#markup' => $this->renderContent()];
// Content changes won't invalidate cache
2. CACHE CONTEXTS — "What request context affects output?"
$build['#cache']['contexts'] = [
'url.path', // Different cache per URL
'user.roles', // Different cache per role
'languages', // Different cache per language
'theme', // Different cache per theme (if multisite)
];
WHY: Creates separate cache entries for different contexts.
EXAMPLE: Admin sees edit links, anonymous doesn't → Use 'user.permissions' context.
3. MAX-AGE — "How long is this valid?"
$build['#cache']['max-age'] = Cache::PERMANENT; // Until invalidated by tags
// OR
$build['#cache']['max-age'] = 3600; // 1 hour
// OR
$build['#cache']['max-age'] = 0; // Never cache
WHY: Balances performance with freshness.
DECISION:
- Cache::PERMANENT — Most content (invalidate with tags)
- 3600 (1 hour) — Time-sensitive data (stock prices, weather)
- 0 — User-specific, uncacheable (current time, CSRF tokens)
Library Loading: Conditional Loading and Critical CSS
Pattern: Conditional Library Attachment
BAD PRACTICE:
# THEME_NAME.info.yml
libraries:
- THEME_NAME/carousel-styles
- THEME_NAME/modal-styles
- THEME_NAME/accordion-styles
# Loading all libraries on every page
WHY BAD: Carousel CSS loads on pages without carousels.
GOOD PRACTICE:
{# Only attach carousel library when carousel component renders #}
{# components/carousel/carousel.scss auto-loaded with SDC #}
OR in custom block:
public function build() {
$build = [
'#markup' => '<div class="custom-feature">...</div>',
];
// Attach library only when block renders
$build['#attached']['library'][] = 'THEME_NAME/custom-feature';
return $build;
}
PERFORMANCE IMPACT: - Loading all libraries: ~500KB CSS on every page - Conditional loading: ~100KB CSS (80% reduction)
Pattern: Critical CSS
STRATEGY: Inline critical above-the-fold CSS, defer rest
File: THEME_NAME.info.yml
libraries:
- THEME_NAME/critical-inline # Inline critical CSS
- THEME_NAME/main-deferred # Defer non-critical CSS
File: THEME_NAME.libraries.yml
critical-inline:
css:
theme:
css/critical.css:
preprocess: false
inline: true # Inline in <head>
main-deferred:
css:
theme:
css/main.css:
preprocess: true # Aggregated and minified
WHAT GOES IN CRITICAL CSS: - Header styles - Above-the-fold layout - Logo, navigation - Hero section styles
WHAT GOES IN DEFERRED: - Footer styles - Below-the-fold components - Rarely-used components
PERFORMANCE IMPACT: - Without critical CSS: 2-3 second blank page (waiting for CSS) - With critical CSS: <1 second visible content
Image Performance: Responsive Images, Lazy Loading, WebP/AVIF
Pattern: Responsive Images (Built-in Drupal)
DRUPAL 10+ DEFAULT: Responsive images and lazy loading enabled
Verify Configuration: 1. Admin → Configuration → Media → Responsive image styles 2. Ensure image styles defined (thumbnail, medium, large) 3. Set responsive image style on image field display
GENERATED HTML:
<img
src="/files/styles/large/image.jpg"
srcset="
/files/styles/thumbnail/image.jpg 320w,
/files/styles/medium/image.jpg 768w,
/files/styles/large/image.jpg 1200w
"
sizes="(min-width: 1200px) 1200px, (min-width: 768px) 768px, 320px"
loading="lazy"
alt="Description"
>
PERFORMANCE IMPACT: - Desktop: Loads 1200px image - Tablet: Loads 768px image (smaller file) - Mobile: Loads 320px image (smallest file) - Data savings: up to 70% on mobile
Pattern: WebP and AVIF Support
DRUPAL 10+: Native WebP support
Configuration: 1. Admin → Configuration → Media → Image toolkit 2. Enable WebP generation 3. Image styles automatically generate WebP versions
GENERATED HTML:
<picture>
<source type="image/avif" srcset="/files/styles/large/image.avif">
<source type="image/webp" srcset="/files/styles/large/image.webp">
<img src="/files/styles/large/image.jpg" alt="Description">
</picture>
FILE SIZE COMPARISON: - JPEG (original): 500KB - WebP: 175KB (65% smaller) - AVIF: 125KB (75% smaller)
BROWSER SUPPORT: - AVIF: Modern browsers (fallback to WebP) - WebP: 95%+ browsers (fallback to JPEG)
RECOMMENDED MODULES: - ImageAPI Optimize WebP: Automatic WebP conversion - Image Optimize: Compression with TinyPNG, Kraken
Pattern: Lazy Loading (Default in Drupal 10+)
AUTOMATIC: Drupal adds loading="lazy" to images
PERFORMANCE IMPACT: - Without lazy loading: 50 images load immediately (slow) - With lazy loading: Only visible images load (fast) - Initial load time: 40% faster
TO DISABLE (rare cases):
{# Disable lazy loading for above-the-fold images #}
<img src="{{ image.url }}" loading="eager" alt="{{ image.alt }}">
WHEN TO DISABLE: Above-the-fold hero image (prevents layout shift)
Asset Aggregation and Minification
Pattern: Production Settings
File: sites/default/settings.php
// Development
$config['system.performance']['css']['preprocess'] = FALSE;
$config['system.performance']['js']['preprocess'] = FALSE;
// Production
$config['system.performance']['css']['preprocess'] = TRUE;
$config['system.performance']['js']['preprocess'] = TRUE;
$config['system.performance']['css']['gzip'] = TRUE;
$config['system.performance']['js']['gzip'] = TRUE;
AGGREGATION: - Combines multiple CSS/JS files into one - Reduces HTTP requests
MINIFICATION: - Removes whitespace, comments - Reduces file size
GZIP: - Compresses files before sending - 70-80% size reduction
PERFORMANCE IMPACT: - Development (no aggregation): 50+ CSS/JS requests - Production (aggregation): 2-3 CSS/JS requests - Page load: 60% faster
Common Performance Mistakes
1. No cache metadata on render arrays
// BAD: No caching
return ['#markup' => $expensive_content];
CORRECT:
return [
'#markup' => $expensive_content,
'#cache' => [
'tags' => ['node:123'],
'contexts' => ['url.path'],
'max-age' => Cache::PERMANENT,
],
];
2. Loading all libraries globally
# BAD: Everything on every page
libraries:
- THEME_NAME/everything
CORRECT:
# Only site-wide essentials
libraries:
- THEME_NAME/base
# Component-specific libraries load with components
3. Not using responsive images
{# BAD: Full-size image on mobile #}
<img src="/files/original/huge-image.jpg" alt="...">
CORRECT:
{# GOOD: Responsive image #}
{{ content.field_image }}
{# Uses responsive image formatter #}
4. Twig debug/cache disabled in production
# BAD: Debug on in production
twig.config:
debug: true
cache: false
CORRECT:
# Production settings
twig.config:
debug: false
cache: true
auto_reload: false
PERFORMANCE IMPACT: 50-70% slower with debug on.
5. Blocking JavaScript in
<!-- BAD: Blocks page rendering -->
<script src="/js/large-library.js"></script>
CORRECT:
# THEME_NAME.libraries.yml
main:
js:
js/large-library.js:
defer: true # Or async
See Also
- Drupal Cache API: https://www.drupal.org/docs/develop/drupal-apis/cache-api
- Drupal Responsive Images: https://www.drupal.org/docs/8/mobile-guide/responsive-images-in-drupal-8
- Image Optimize Module: https://www.drupal.org/project/imageapi_optimize