From drupal-core
Drupal performance optimization expertise. Use when working on caching, database queries, asset optimization, lazy loading, BigPipe, or profiling.
npx claudepluginhub ajv009/drupal-devkitThis skill uses the workspace's default tool permissions.
You are an expert in Drupal performance optimization across all layers.
Implements Drupal 10/11 caching with bins, tags, contexts, invalidation strategies, external backends, lazy builders, and CacheableMetadata. For performance optimization and cache configuration.
Optimizes WordPress performance via Query Monitor profiling, autoload audits, N+1 query fixes, Redis object caching, and frontend improvements like Vite bundles and critical CSS. Use for slow loads, poor Core Web Vitals, or high query counts.
Profiles and optimizes frontend, backend, and database performance for scalable apps. Use for slow response times, scaling bottlenecks via Chrome DevTools, React Profiler, Node/Python profilers, Postgres/Mongo queries.
Share bugs, ideas, or general feedback.
You are an expert in Drupal performance optimization across all layers.
Drupal has multiple cache layers. Optimize from outermost to innermost:
Browser Cache
└─ CDN / Reverse Proxy (Varnish)
└─ Page Cache (anonymous users)
└─ Dynamic Page Cache (authenticated users)
└─ Render Cache (blocks, entities, views)
└─ Internal Cache (config, discovery, etc.)
For anonymous users, entire pages are cached. Configure at /admin/config/development/performance:
// settings.php — max-age for page cache
$config['system.performance']['cache']['page']['max_age'] = 86400; // 24 hours
When it works: Anonymous users, no session, no personalization. When it breaks: Any per-user content, CSRF tokens, form tokens.
Caches render arrays for authenticated users using cache contexts to vary output.
Key cache contexts:
user — per useruser.permissions — per permission set (most efficient for auth users)user.roles — per role combinationurl.query_args — varies by query stringurl.path — varies by pathsession — per sessionlanguages — per languageAll render arrays MUST include cache metadata:
$build = [
'#markup' => $content,
'#cache' => [
'tags' => ['node:123', 'node_list'],
'contexts' => ['user.permissions'],
'max-age' => 3600,
],
];
Cache tag conventions:
node:123, user:456, taxonomy_term:789node_list, user_listconfig:my_module.settingsmy_module:custom_tagInvalidation:
// Invalidate specific tags
\Drupal\Core\Cache\Cache::invalidateTags(['node:123']);
// Entity saves auto-invalidate their tags
$node->save(); // Invalidates node:{nid} and node_list
BigPipe sends the page shell immediately and streams personalized content later.
Use lazy builders for personalized or uncacheable content in an otherwise cacheable page:
$build['user_greeting'] = [
'#lazy_builder' => [
'my_module.greeting_builder:build',
[$uid],
],
'#create_placeholder' => TRUE,
];
// Service class
class GreetingBuilder {
public function build(int $uid): array {
return [
'#markup' => $this->t('Hello, @name', ['@name' => $user->getDisplayName()]),
'#cache' => [
'contexts' => ['user'],
'tags' => ['user:' . $uid],
],
];
}
}
Register as a service with #[AutowireLocator] or in *.services.yml.
// BAD: N+1 — loads each entity individually
foreach ($nids as $nid) {
$node = $this->entityTypeManager->getStorage('node')->load($nid);
}
// GOOD: Single query, bulk load
$nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($nids);
$query = $this->entityTypeManager->getStorage('node')->getQuery()
->accessCheck(TRUE)
->condition('type', 'article')
->condition('status', 1)
->range(0, 50) // Always limit results
->sort('created', 'DESC');
$nids = $query->execute();
// Bulk load results
$nodes = $this->entityTypeManager->getStorage('node')->loadMultiple($nids);
// Use specific fields instead of loading full entities
$query = $this->database->select('node_field_data', 'n');
$query->fields('n', ['nid', 'title']); // Only needed columns
$query->condition('n.type', 'article');
$query->range(0, 50);
$query->addTag('node_access'); // Respect access control
$results = $query->execute();
// In hook_schema() or hook_update_N()
$schema['my_table'] = [
'fields' => [...],
'indexes' => [
'status_created' => ['status', 'created'],
],
];
Always configure cache on Views:
| Cache Type | Use When |
|---|---|
| Tag-based | Content changes unpredictably (default, recommended) |
| Time-based | Content changes on a schedule |
| None | Never (except during development) |
// Custom Views field plugin with optimized query
public function query() {
// Add only needed joins
$this->ensureMyTable();
$this->field_alias = $this->query->addField($this->tableAlias, $this->realField);
}
ddev get ddev/ddev-redis
ddev restart
// settings.php
$settings['redis.connection']['host'] = 'redis';
$settings['redis.connection']['port'] = 6379;
$settings['cache']['default'] = 'cache.backend.redis';
// Don't cache these bins in Redis
$settings['cache']['bins']['form'] = 'cache.backend.database';
$settings['cache']['bins']['bootstrap'] = 'cache.backend.chainedfast';
$settings['cache']['bins']['discovery'] = 'cache.backend.chainedfast';
ddev redis-cli monitor # Watch all commands
ddev redis-cli info memory # Memory usage
ddev redis-cli dbsize # Number of keys
// settings.php — enable on production
$config['system.performance']['css']['preprocess'] = TRUE;
$config['system.performance']['js']['preprocess'] = TRUE;
# my_module.libraries.yml — attach only what's needed
my_library:
css:
theme:
css/my-styles.css: { minified: true }
js:
js/my-script.js: { minified: true }
dependencies:
- core/drupal
# Don't depend on jQuery unless required
// Attach library only where needed, not globally
$build['#attached']['library'][] = 'my_module/my_library';
ddev composer require drupal/webprofiler
ddev drush en webprofiler -y
Provides toolbar with:
# Enable Xdebug in DDEV
ddev xdebug on
# Generate cachegrind profiles
ddev exec php -d xdebug.mode=profile -d xdebug.output_dir=/tmp vendor/bin/drush cr
Analyze with KCachegrind, QCachegrind, or Webgrind.
# Check cache hit ratio
ddev drush cr && time ddev drush cr # Second run should be fast
# Count database queries on a page
ddev drush ws --severity=Notice | grep -c "query"
# Check for large config objects
ddev drush config:list | wc -l
Before deploying, verify:
loadMultiple())accessCheck() and range()#cache metadata\Drupal:: calls in hot paths (use DI)