Analyze performance patterns in Umbraco projects
Analyzes Umbraco projects for performance anti-patterns, caching issues, and optimization opportunities.
/plugin marketplace add twofoldtech-dakota/claude-marketplace/plugin install twofoldtech-dakota-umbraco-analyzer-plugins-umbraco-analyzer@twofoldtech-dakota/claude-marketplaceIdentify performance anti-patterns, caching issues, and optimization opportunities.
Detect recursive content access without limits:
// Bad: Loads entire tree
var allContent = rootContent.Descendants();
var deepChildren = content.DescendantsOrSelf();
// Good: Limited traversal
var topLevel = content.Children.Take(100);
// Better: Use Examine for large queries
var results = _examineManager.GetIndex("ExternalIndex")
.Searcher.CreateQuery("content")
.ParentId(rootId)
.Execute();
Check for modern caching pattern:
// Good: HybridCache (v15+)
public class CachedContentService
{
private readonly HybridCache _cache;
public async Task<ContentModel> GetContentAsync(Guid key, CancellationToken ct)
{
return await _cache.GetOrCreateAsync(
$"content_{key}",
async token => await FetchContentAsync(key, token),
cancellationToken: ct
);
}
}
// Old: IAppPolicyCache
public object GetContent(Guid key)
{
return _appCaches.RuntimeCache.GetCacheItem(
$"content_{key}",
() => FetchContent(key)
);
}
Detect context access in singleton services:
// Bad: Singleton holding scoped dependency
public class MySingletonService // Registered as Singleton
{
private readonly IUmbracoContext _umbracoContext; // Scoped!
public MySingletonService(IUmbracoContext umbracoContext)
{
_umbracoContext = umbracoContext; // Captured reference goes stale
}
}
// Good: Use IUmbracoContextFactory
public class MySingletonService
{
private readonly IUmbracoContextFactory _contextFactory;
public void DoWork()
{
using var cref = _contextFactory.EnsureUmbracoContext();
var content = cref.UmbracoContext.Content;
}
}
Check for custom index configuration:
// Good: Custom index for specific queries
public class ProductIndexCreator : IConfigureNamedOptions<LuceneDirectoryIndexOptions>
{
public void Configure(string name, LuceneDirectoryIndexOptions options)
{
if (name == "ProductIndex")
{
options.FieldDefinitions = new FieldDefinitionCollection(
new FieldDefinition("price", FieldDefinitionTypes.Double),
new FieldDefinition("category", FieldDefinitionTypes.Raw)
);
}
}
}
Check for image processor cache:
// Good: Image cache configured
{
"Umbraco": {
"CMS": {
"Imaging": {
"Cache": {
"BrowserMaxAge": "7.00:00:00",
"CacheMaxAge": "365.00:00:00"
}
}
}
}
}
Check for efficient content queries:
// Bad: Multiple separate queries
var page = _contentQuery.ContentSingleAtXPath("/root/home");
var nav = _contentQuery.ContentSingleAtXPath("/root/navigation");
var footer = _contentQuery.ContentSingleAtXPath("/root/footer");
// Good: Single traversal
var root = _contentQuery.ContentAtRoot().First();
var page = root.Descendant("home");
var nav = root.Descendant("navigation");
var footer = root.Descendant("footer");
Check for expensive operations in handlers:
// Bad: Slow operation in sync handler
public void Handle(ContentPublishedNotification notification)
{
foreach (var content in notification.PublishedEntities)
{
_httpClient.PostAsync(...).Wait(); // Slow + blocking!
}
}
// Good: Queue for background processing
public void Handle(ContentPublishedNotification notification)
{
foreach (var content in notification.PublishedEntities)
{
_backgroundQueue.QueueWork(() => ProcessContentAsync(content.Key));
}
}
| Code | Severity | Issue | Detection |
|---|---|---|---|
| PERF-001 | Critical | Unbounded content traversal | .Descendants() without Take() |
| PERF-002 | Warning | Not using HybridCache | v15+ without HybridCache |
| PERF-003 | Warning | IUmbracoContext in singleton | Context in singleton constructor |
| PERF-004 | Warning | Missing image cache config | No Imaging.Cache settings |
| PERF-005 | Info | No custom Examine index | Complex queries without custom index |
| PERF-006 | Info | Slow notification handler | HTTP/DB calls in handler |
Grep: \.Descendants\(\)
Grep: \.DescendantsOrSelf\(\)
Check for .Take() nearby
Grep: HybridCache
Grep: IAppPolicyCache
If v15+ and no HybridCache, flag
Find singleton registrations
Check constructor parameters for IUmbracoContext
Read: appsettings.json
Look for Imaging.Cache section
Grep: IExamineManager
Grep: CreateQuery
Check for custom index definitions
## Performance Analysis
### Performance Score: B+
### Critical Issues
#### [PERF-001] Unbounded Content Traversal
**Location**: `src/Web/Services/NavigationService.cs:34`
**Code**:
```csharp
var allPages = rootContent.Descendants().Where(x => x.IsVisible());
Impact: Loads entire content tree into memory Fix: Use Examine or limit depth:
// Option 1: Limit depth
var pages = rootContent.Descendants()
.Where(x => x.Level <= 3 && x.IsVisible())
.Take(100);
// Option 2: Use Examine
var results = _examineManager.GetIndex("ExternalIndex")
.Searcher.CreateQuery("content")
.Field("isVisible", "true")
.Execute();
Issue: Umbraco 15.x detected but using IAppPolicyCache
Location: src/Web/Services/CacheService.cs
Fix: Migrate to HybridCache for improved performance:
public class CacheService
{
private readonly HybridCache _cache;
public async Task<T> GetOrCreateAsync<T>(string key, Func<Task<T>> factory)
{
return await _cache.GetOrCreateAsync(key, async _ => await factory());
}
}
Location: src/Web/Services/ContentService.cs
Issue: IUmbracoContext injected into singleton
Fix: Use IUmbracoContextFactory instead
| Type | Status | Recommendation |
|---|---|---|
| HybridCache | Not used | Migrate from IAppPolicyCache |
| Image Cache | Configured | Good |
| Output Cache | Not used | Consider for static pages |