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
- Navigate to PageSpeed Insights
- Enter your Umbraco site URL
- Click Analyze
- Review Largest Contentful Paint metric
- 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:
- Open IIS Manager
- Select your site
- Click Advanced Settings
- Ensure binding uses HTTPS
- 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
- CLS Optimization - Fix layout shift issues
- Tracking Issues - Debug tracking problems
- Umbraco Performance - General performance optimization