Development

Optimizely Batch Loading: Optimize IContentLoader Performance

By Ginbok5 min read

Optimizing internal systems is crucial for maintaining high responsiveness and scalability, especially when dealing with high-traffic components like Optimizely CMS and Commerce. A common bottleneck arises when developers attempt to load multiple pieces of content synchronously using iterative calls to IContentLoader.Get<T>().

In this guide, we explore why synchronous iteration is detrimental to application health and demonstrate the significant performance gains achieved by switching to the batch loading capabilities of IContentLoader.GetItems(), fully integrated with modern .NET 8 asynchronous patterns.

The Performance Trap: Iterative Synchronous Loading

The IContentLoader.Get<T>() method is designed for loading single content items efficiently. However, when you need to load a list of 50 or 100 items (e.g., related articles, product listings, or navigation items), developers often fall back to a simple foreach loop:

public IEnumerable<MyContent> LoadContentIteratively(IEnumerable<ContentReference> references, IContentLoader contentLoader)
{
    var contentList = new List<MyContent>();
    
    // BAD PRACTICE: Synchronous call repeated inside a loop
    foreach (var reference in references)
    {
        if (contentLoader.TryGet(reference, out MyContent content))
        {
            contentList.Add(content);
        }
    }
    return contentList;
}

Why This Causes Bottlenecks

Each synchronous call within the loop might involve accessing the underlying SQL database if the content is not already in the in-memory cache. Even if cached, these synchronous calls block the current thread until the operation completes. If this code runs on a web request thread, it leads to:

The Optimal Solution: IContentLoader.GetItems()

Optimizely provides IContentLoader.GetItems() specifically for handling multiple ContentReference objects in a single, highly optimized batch operation. This method significantly reduces database round trips and optimizes cache lookups.

Implementing Batch Loading

By passing the entire collection of references to GetItems(), Optimizely's internal mechanisms can execute the necessary database query (if required) in a single, efficient transaction, and return the entire result set immediately.

using EPiServer.Core;
using EPiServer.ServiceLocation;

public class ContentLoadingService
{
    private readonly IContentLoader _contentLoader;

    public ContentLoadingService(IContentLoader contentLoader)
    {
        _contentLoader = contentLoader;
    }

    public IEnumerable<T> LoadContentInBatch<T>(IEnumerable<ContentReference> references) where T : IContent
    {
        // GOOD PRACTICE: Use GetItems for performance gains
        return _contentLoader.GetItems(references, new LoaderOptions())
                             .OfType<T>()
                             .ToList();
    }
}

Advanced Optimization: Leveraging Async in .NET 8

While IContentLoader itself does not expose an officially supported GetItemsAsync method, we must ensure our integration points (like Controllers or services that consume this data) handle the synchronization boundary correctly to avoid blocking the main execution path.

For operations that are CPU-bound or utilize other async services alongside content loading (e.g., retrieving external data or calling Commerce APIs), always wrap the content loading in a dedicated async service to maintain non-blocking execution flow throughout your application.

Example Async Service Integration

We often interact with the Commerce layer or other external APIs which are inherently asynchronous. While GetItems is synchronous, integrating it within a larger async scope maintains structure:

public interface IBatchContentService
{
    Task<IEnumerable<T>> GetContentAsync<T>(IEnumerable<ContentReference> references) where T : IContent;
}

public class BatchContentService : IBatchContentService
{
    private readonly IContentLoader _contentLoader;

    public BatchContentService(IContentLoader contentLoader)
    {
        _contentLoader = contentLoader;
    }

    public Task<IEnumerable<T>> GetContentAsync<T>(IEnumerable<ContentReference> references) where T : IContent
    {
        // Run the synchronous GetItems operation on a background thread.
        // This is often necessary when integrating synchronous legacy APIs 
        // into a modern async ecosystem without blocking the main thread.
        return Task.Run(() => 
        {
            return _contentLoader.GetItems(references, new LoaderOptions())
                                 .OfType<T>()
                                 .ToList()
                                 .AsEnumerable();
        });
    }
}

This pattern, while introducing a slight overhead for the thread pool switch via Task.Run, is highly effective in ensuring that synchronous I/O operations from Optimizely do not block the critical request handling threads (like those in your Ginbok.Web layer).

Troubleshooting: Batch Size and Performance

Symptom: High memory usage or timeouts during loading.

Cause: The batch size is excessively large (e.g., thousands of items). While GetItems is efficient, retrieving thousands of objects at once consumes substantial memory and can pressure the garbage collector (GC).

Solution: Implement pagination or chunking. If you need 5000 items, break the request into 5 batches of 1000 references each, processing them sequentially or in limited parallel chunks (using Task.WhenAll cautiously) to manage memory load.

Symptom: Loaded content appears outdated.

Cause: Content items are retrieved correctly, but you might be relying on outdated cache data from another segment of the application, or the items themselves were published outside the current deployment scope.

Solution: Ensure cache invalidation is handled correctly. For specific scenarios requiring fresh data, consider using IContentCacheOwner if you manage custom caching layers, or, if absolutely necessary, verify the LoaderOptions provided to GetItems, though generally, default caching behavior is sufficient.

Summary

Optimizing content retrieval in Optimizely CMS is fundamental to system performance. By replacing synchronous, iterative calls to Get<T>() with the high-performance IContentLoader.GetItems(), you significantly reduce database interaction, minimize thread blocking, and build a more robust, scalable application using modern .NET 8 practices.

#Optimizely#DotNet8#CMSPerformance#ContentLoader#BatchLoading
← Back to Articles