Performance Best Practices
When to Use
Any plugin system handling more than a handful of plugins or processing external API calls.
Decision
| Issue | Solution | Why |
|---|---|---|
| Slow plugin discovery | Use CachedDiscovery (default in DefaultPluginManager) |
Discovery scans filesystem on every request without caching |
| Provider API latency | Implement proxy caching with cache tags | External API calls add 100-500ms per request |
| Large plugin definitions | Use service_id_collector for lazy loading |
Loading all services on every request wastes memory |
| Service collector overhead | Aggregate during container compilation, not runtime | Container rebuild is rare; runtime collection is every request |
| Plugin instantiation cost | Use MapFactory for lightweight instantiation |
Full DI container resolution per plugin is expensive |
Pattern
Plugin discovery caching - DefaultPluginManager implements CachedDiscoveryInterface, but only caches once you give it a backend:
class MyPluginManager extends DefaultPluginManager {
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
parent::__construct(
'Plugin/MyPluginType',
$namespaces,
$module_handler,
'Drupal\my_module\Plugin\MyPluginTypeInterface',
'Drupal\my_module\Attribute\MyPluginType',
'Drupal\my_module\Annotation\MyPluginType'
);
$this->setCacheBackend($cache_backend, 'my_plugin_type_plugins');
}
}
The fifth argument is the attribute class and the sixth the annotation class kept for backward compatibility. Passing an annotation class in the attribute position triggers a deprecation in Drupal 11.2 and stops working in Drupal 12.
Provider response caching - tag the entry so a provider change can invalidate it:
public function executeWithCache($operation, $data) {
$cid = 'provider:' . $this->providerId . ':' . md5(serialize($data));
if ($cached = $this->cache->get($cid)) {
return $cached->data;
}
$result = $this->provider->execute($operation, $data);
$this->cache->set($cid, $result, Cache::PERMANENT, [
'provider:' . $this->providerId,
'provider_operation:' . $operation,
]);
return $result;
}
Check before instantiate - hasDefinition() costs a lookup, a failed createInstance() costs an exception:
public function getPlugin($plugin_id, array $config = []) {
if (!$this->pluginManager->hasDefinition($plugin_id)) {
$this->logger->warning('Plugin @id not found', ['@id' => $plugin_id]);
return NULL;
}
return $this->pluginManager->createInstance($plugin_id, $config);
}
Common Mistakes
- Calling
getDefinitions()in hot paths → WHY: Even cached, deserialization of large definition arrays is expensive - Not using
hasDefinition()beforecreateInstance()→ WHY: Failed instantiation throws exceptions which are expensive - Loading all providers to find one → WHY: Use capability-based discovery (
getProvidersByCapability()) to filter first - No timeout on external provider calls → WHY: One slow provider blocks the entire request