Fix Umbraco LCP Issues | OpsBlu Docs

Fix Umbraco LCP Issues

Improve Umbraco LCP by enabling output caching, optimizing Media Picker images with ImageSharp, and tuning IIS/Kestrel response times.

Largest Contentful Paint (LCP) measures how quickly the main content loads on your Umbraco site. This guide provides .NET-specific optimizations, IIS configuration, Razor view improvements, and Umbraco-specific techniques to achieve LCP < 2.5 seconds.

Understanding LCP in Umbraco

What Counts as LCP in Umbraco

Common LCP elements on Umbraco sites:

  • Hero Images - Feature images from Media Picker
  • Product Images - E-commerce product photos
  • Article Headers - Large header images on blog posts
  • Custom Blocks - Block List Editor content with images
  • Background Images - CSS backgrounds from Umbraco properties

Target LCP Performance

  • Good: LCP < 2.5 seconds
  • Needs Improvement: LCP 2.5 - 4.0 seconds
  • Poor: LCP > 4.0 seconds

Measure Current LCP

Using Google PageSpeed Insights

  1. Navigate to PageSpeed Insights
  2. Enter your Umbraco site URL
  3. Click Analyze
  4. Review Largest Contentful Paint metric
  5. Identify the LCP element

Using Visual Studio Performance Profiler

// Add to Startup.cs or Program.cs
builder.Services.AddApplicationInsightsTelemetry();

// Track LCP server-side metrics
builder.Services.AddSingleton<IPerformanceMonitor, PerformanceMonitor>();

Umbraco Image Optimization

Configure Umbraco Image Processor

appsettings.json:

{
  "Umbraco": {
    "CMS": {
      "Imaging": {
        "Cache": {
          "BrowserMaxAge": "7.00:00:00",
          "CacheMaxAge": "365.00:00:00"
        },
        "Resize": {
          "MaxWidth": 2000,
          "MaxHeight": 2000
        }
      }
    }
  }
}

Use Umbraco Image Cropper

In Razor view:

@using Umbraco.Cms.Core.Models
@{
    var heroImage = Model?.Value<IPublishedContent>("heroImage");
}

@if (heroImage != null)
{
    var imageCropper = heroImage.Value<ImageCropperValue>("umbracoFile");
    var crops = imageCropper?.Crops ?? Enumerable.Empty<ImageCropperValue.ImageCropperCrop>();

    var smallUrl = heroImage.GetCropUrl("umbracoFile", "small");
    var mediumUrl = heroImage.GetCropUrl("umbracoFile", "medium");
    var largeUrl = heroImage.GetCropUrl("umbracoFile", "large");

    <img srcset="@smallUrl 600w,
                 @mediumUrl 1200w,
                 @largeUrl 2000w"
         sizes="(max-width: 600px) 600px,
                (max-width: 1200px) 1200px,
                2000px"
         src="@largeUrl"
         alt="@heroImage.Name"
         width="2000"
         height="1000"
         fetchpriority="high"
         loading="eager" />
}

Preload LCP Image

@{
    var heroImage = Model?.Value<IPublishedContent>("heroImage");
}

@section head {
    @if (heroImage != null)
    {
        var imageUrl = heroImage.GetCropUrl("umbracoFile", "large");

        <link rel="preload"
              as="image"
              href="@imageUrl"
              imagesrcset="@heroImage.GetCropUrl("umbracoFile", "small") 600w,
                           @heroImage.GetCropUrl("umbracoFile", "medium") 1200w,
                           @imageUrl 2000w"
              imagesizes="100vw" />
    }
}

Use WebP Format

Create image format helper:

using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.Routing;

namespace YourProject.Helpers
{
    public static class ImageHelper
    {
        public static string GetOptimizedImageUrl(
            this IPublishedContent image,
            int width,
            string format = "webp")
        {
            if (image == null) return string.Empty;

            return image.GetCropUrl(
                propertyAlias: "umbracoFile",
                width: width,
                furtherOptions: $"&format={format}&quality=80"
            );
        }
    }
}

In Razor:

@using YourProject.Helpers

<picture>
    <source type="image/webp"
            srcset="@heroImage.GetOptimizedImageUrl(600, "webp") 600w,
                    @heroImage.GetOptimizedImageUrl(1200, "webp") 1200w,
                    @heroImage.GetOptimizedImageUrl(2000, "webp") 2000w" />
    <source type="image/jpeg"
            srcset="@heroImage.GetOptimizedImageUrl(600, "jpeg") 600w,
                    @heroImage.GetOptimizedImageUrl(1200, "jpeg") 1200w,
                    @heroImage.GetOptimizedImageUrl(2000, "jpeg") 2000w" />
    <img src="@heroImage.GetOptimizedImageUrl(2000, "jpeg")"
         alt="@heroImage.Name" />
</picture>

.NET and Server Optimization

Enable Response Compression

Program.cs (Umbraco 10+):

using Microsoft.AspNetCore.ResponseCompression;
using System.IO.Compression;

var builder = WebApplication.CreateBuilder(args);

// Add response compression
builder.Services.AddResponseCompression(options =>
{
    options.EnableForHttps = true;
    options.Providers.Add<BrotliCompressionProvider>();
    options.Providers.Add<GzipCompressionProvider>();
    options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
        new[] { "image/svg+xml", "application/json" });
});

builder.Services.Configure<BrotliCompressionProviderOptions>(options =>
{
    options.Level = CompressionLevel.Fastest;
});

builder.Services.Configure<GzipCompressionProviderOptions>(options =>
{
    options.Level = CompressionLevel.Fastest;
});

var app = builder.Build();

// Use compression
app.UseResponseCompression();

// ... rest of configuration

Configure Output Caching

builder.Services.AddOutputCache(options =>
{
    options.AddBasePolicy(builder => builder.Cache());

    // Cache static content aggressively
    options.AddPolicy("StaticAssets", builder =>
        builder.Cache()
            .Expire(TimeSpan.FromDays(365))
            .SetVaryByQuery("*"));

    // Cache pages with shorter TTL
    options.AddPolicy("Pages", builder =>
        builder.Cache()
            .Expire(TimeSpan.FromMinutes(10))
            .Tag("umbraco-pages"));
});

var app = builder.Build();

app.UseOutputCache();

In Razor view:

@attribute [OutputCache(PolicyName = "Pages")]

Optimize Database Queries

Use IPublishedContentQuery efficiently:

using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PublishedCache;

namespace YourProject.ViewComponents
{
    public class ProductListViewComponent : ViewComponent
    {
        private readonly IPublishedContentQuery _contentQuery;

        public ProductListViewComponent(IPublishedContentQuery contentQuery)
        {
            _contentQuery = contentQuery;
        }

        public IViewComponentResult Invoke()
        {
            // Efficient query - only load needed properties
            var products = _contentQuery
                .ContentAtRoot()
                .FirstOrDefault()?
                .DescendantsOfType("product")
                .Where(x => x.Value<bool>("isPublished"))
                .OrderByDescending(x => x.CreateDate)
                .Take(10)
                .ToList();

            return View(products);
        }
    }
}

IIS Configuration

Enable IIS Static Content Compression

Web.config:

<configuration>
  <system.webServer>
    <!-- Enable static and dynamic compression -->
    <urlCompression doStaticCompression="true"
                    doDynamicCompression="true" />

    <httpCompression directory="%SystemDrive%\inetpub\temp\IIS Temporary Compressed Files">
      <scheme name="gzip" dll="%Windir%\system32\inetsrv\gzip.dll" />
      <scheme name="br" dll="%ProgramFiles%\IIS\IIS Compression\iisbrotli.dll" />

      <dynamicTypes>
        <add mimeType="text/*" enabled="true" />
        <add mimeType="message/*" enabled="true" />
        <add mimeType="application/javascript" enabled="true" />
        <add mimeType="application/json" enabled="true" />
      </dynamicTypes>

      <staticTypes>
        <add mimeType="text/*" enabled="true" />
        <add mimeType="message/*" enabled="true" />
        <add mimeType="application/javascript" enabled="true" />
        <add mimeType="image/svg+xml" enabled="true" />
      </staticTypes>
    </httpCompression>
  </system.webServer>
</configuration>

Set Aggressive Cache Headers

<configuration>
  <system.webServer>
    <staticContent>
      <clientCache cacheControlMode="UseMaxAge"
                   cacheControlMaxAge="365.00:00:00" />

      <!-- Specific rules for different file types -->
      <remove fileExtension=".jpg" />
      <mimeMap fileExtension=".jpg"
               mimeType="image/jpeg" />

      <remove fileExtension=".webp" />
      <mimeMap fileExtension=".webp"
               mimeType="image/webp" />
    </staticContent>

    <!-- Add cache headers -->
    <httpProtocol>
      <customHeaders>
        <add name="Cache-Control"
             value="public, max-age=31536000, immutable" />
      </customHeaders>
    </httpProtocol>

    <!-- URL rewrite rules for cache busting -->
    <rewrite>
      <outboundRules>
        <rule name="Add Vary Header">
          <match serverVariable="RESPONSE_Vary"
                 pattern=".*" />
          <action type="Rewrite"
                  value="Accept-Encoding" />
        </rule>
      </outboundRules>
    </rewrite>
  </system.webServer>
</configuration>

Enable HTTP/2

In IIS Manager:

  1. Open IIS Manager
  2. Select your site
  3. Click Advanced Settings
  4. Ensure binding uses HTTPS
  5. HTTP/2 is automatically enabled for HTTPS

Or via Web.config:

<configuration>
  <system.webServer>
    <serverRuntime enabled="true"
                   frequentHitThreshold="1"
                   frequentHitTimePeriod="00:00:10" />
  </system.webServer>
</configuration>

Razor View Optimization

Minimize Razor Compilation Time

Create compiled views assembly:

<!-- In .csproj file -->
<PropertyGroup>
  <MvcRazorCompileOnPublish>true</MvcRazorCompileOnPublish>
  <MvcRazorExcludeRefAssembliesFromPublish>false</MvcRazorExcludeRefAssembliesFromPublish>
</PropertyGroup>

Optimize Partial View Loading

Bad - Synchronous:

@Html.Partial("~/Views/Partials/Navigation.cshtml")

Good - Asynchronous:

@await Html.PartialAsync("~/Views/Partials/Navigation.cshtml")

Better - View Component:

public class NavigationViewComponent : ViewComponent
{
    private readonly IPublishedContentQuery _contentQuery;

    public NavigationViewComponent(IPublishedContentQuery contentQuery)
    {
        _contentQuery = contentQuery;
    }

    public async Task<IViewComponentResult> InvokeAsync()
    {
        var root = _contentQuery.ContentAtRoot().FirstOrDefault();
        var navItems = root?.Children().Where(x => x.Value<bool>("showInNavigation"));

        return View(navItems);
    }
}

In Razor:

@await Component.InvokeAsync("Navigation")

Defer Non-Critical Razor Sections

@section Scripts {
    @* Defer loading of analytics and tracking *@
    <script>
        window.addEventListener('load', function() {
            // Load tracking scripts after page load
        });
    </script>
}

CDN Integration

Configure Umbraco with CDN

appsettings.json:

{
  "Umbraco": {
    "CMS": {
      "Content": {
        "ContentVersionCleanupPolicy": {
          "EnableCleanup": true
        }
      },
      "Hosting": {
        "Debug": false
      },
      "WebRouting": {
        "UmbracoApplicationUrl": "https://yourdomain.com"
      }
    },
    "Storage": {
      "AzureBlob": {
        "Media": {
          "ConnectionString": "your-connection-string",
          "ContainerName": "media",
          "CdnUrl": "https://cdn.yourdomain.com"
        }
      }
    }
  }
}

Use Azure CDN or Cloudflare

Install Azure Blob Storage package:

dotnet add package Umbraco.StorageProviders.AzureBlob

Configure in Program.cs:

builder.CreateUmbracoBuilder()
    .AddBackOffice()
    .AddWebsite()
    .AddAzureBlobMediaFileSystem()
    .AddComposers()
    .Build();

Critical CSS Optimization

Extract Critical CSS

Create helper:

public class CriticalCssHelper
{
    public static string GetCriticalCss(string pageType)
    {
        return pageType switch
        {
            "homepage" => @"
                body { margin: 0; font-family: Arial, sans-serif; }
                .hero { width: 100%; height: 500px; }
            ",
            "article" => @"
                body { margin: 0; font-family: Arial, sans-serif; }
                article { max-width: 800px; margin: 0 auto; }
            ",
            _ => @"
                body { margin: 0; font-family: Arial, sans-serif; }
            "
        };
    }
}

In Master.cshtml:

<head>
    @* Inline critical CSS *@
    <style>
        @Html.Raw(CriticalCssHelper.GetCriticalCss(Model?.ContentType.Alias ?? "default"))
    </style>

    @* Defer non-critical CSS *@
    <link rel="preload"
          href="/css/main.css"
          as="style"
    <noscript><link rel="stylesheet" href="/css/main.css"></noscript>
</head>

Monitoring and Testing

Application Insights Integration

using Microsoft.ApplicationInsights;

public class PerformanceMonitor : IPerformanceMonitor
{
    private readonly TelemetryClient _telemetryClient;

    public PerformanceMonitor(TelemetryClient telemetryClient)
    {
        _telemetryClient = telemetryClient;
    }

    public void TrackLcp(double lcpValue, string pageUrl)
    {
        _telemetryClient.TrackMetric("LCP", lcpValue, new Dictionary<string, string>
        {
            { "PageUrl", pageUrl }
        });
    }
}

Real User Monitoring (RUM)

<script>
    // Track LCP with Performance Observer
    if ('PerformanceObserver' in window) {
        new PerformanceObserver((list) => {
            const entries = list.getEntries();
            const lastEntry = entries[entries.length - 1];

            console.log('LCP:', lastEntry.renderTime || lastEntry.loadTime);

            // Send to analytics
            if (typeof gtag !== 'undefined') {
                gtag('event', 'web_vitals', {
                    event_category: 'Web Vitals',
                    value: Math.round(lastEntry.renderTime || lastEntry.loadTime),
                    metric_name: 'LCP',
                    page_path: window.location.pathname
                });
            }
        }).observe({entryTypes: ['largest-contentful-paint']});
    }
</script>

Common Issues

Large Hero Images

Problem: 5MB+ feature images Solution:

  • Resize images to max 2000px width in Media section
  • Use Image Cropper with appropriate crops
  • Enable WebP conversion
  • Implement lazy loading for below-fold images

Slow Server Response (TTFB)

Problem: TTFB > 600ms Solution:

  • Enable output caching
  • Optimize database queries
  • Use SQL Server query optimization
  • Increase IIS application pool resources

IIS Configuration Issues

Problem: Compression not working Solution:

# Enable compression in IIS
Install-WindowsFeature Web-Stat-Compression
Install-WindowsFeature Web-Dyn-Compression

# Restart IIS
iisreset

Next Steps