Appearance
Are you an LLM? You can read better optimized documentation at /advanced_topics/performance.md for this page in Markdown format
Performance optimization
This document provides a comprehensive guide to performance optimization strategies for the TheExampleApp application. The focus is on techniques relevant to its specific technology stack, which includes ASP.NET Core 2.1, server-side rendering with Razor, and Contentful as a headless CMS.
Optimization strategies
Improving the performance of TheExampleApp involves a multi-faceted approach targeting different layers of the application. As a server-rendered application heavily reliant on an external API (Contentful) for its content, the key optimization areas are:
- Data Retrieval: Minimizing the latency and payload size of requests to the Contentful Delivery API.
- Server-Side Caching: Reducing redundant computation and data fetching by caching generated page responses.
- Client-Side Assets: Optimizing the delivery of static files like CSS, JavaScript, and fonts to the end-user's browser.
- Session Management: Ensuring the session state mechanism is efficient and scalable.
Each of the following sections will delve into these areas, providing specific implementation details and recommendations based on the current codebase.
Contentful query optimization
All content for the application is sourced from Contentful. Inefficient queries to the Contentful API can be a significant performance bottleneck. The primary interaction with the API is managed via the contentful.aspnetcore SDK.
For more information on the basic setup, see the Contentful Integration documentation.
Include Depth Considerations
The Contentful include parameter is used to resolve and embed linked entries in a single API response, avoiding the "N+1" problem. The application currently uses a high include level on the homepage.
Analysis: The Index.cshtml.cs page model fetches the homepage layout with an include level of 4.
csharp
// File: TheExampleApp/Pages/Index.cshtml.cs
public async Task OnGet()
{
var queryBuilder = QueryBuilder<Layout>.New
.ContentTypeIs("layout")
.FieldEquals(f => f.Slug, "home")
.Include(4) // <-- High include level
.LocaleIs(HttpContext?.Session?.GetString(Startup.LOCALE_KEY) ?? CultureInfo.CurrentCulture.ToString());
var indexPage = (await _client.GetEntries(queryBuilder)).FirstOrDefault();
// ...
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
Recommendations:
- Audit Include Levels: While
Include(4)might be necessary for a complex page with deeply nested content modules, it can significantly increase response time and payload size from Contentful. Audit each page and use the lowest possibleincludelevel required to render the content. - Avoid Over-fetching: A high include level can fetch far more data than is actually displayed. For pages with simpler requirements, reduce this value accordingly (e.g.,
.Include(1)or.Include(2)).
Selective Field Fetching
By default, a Contentful query retrieves all fields for the requested entries and any included linked entries. The select operator can be used to limit the response to only the fields that are necessary for rendering.
Recommendations:
Use the
SelectOperator: When fetching lists of entries (e.g., for a course listing page) where only a few fields like title, slug, and a summary are needed, use theselectoperator to reduce the data transfer size.csharp// Example: Fetching only specific fields for a list of courses var builder = QueryBuilder<Course>.New .ContentTypeIs("course") .Select(new[] { "fields.title", "fields.slug", "fields.shortDescription" }) .Include(0); var courses = await _client.GetEntries(builder);1
2
3
4
5
6
7Impact: This technique is most effective for pages that display many entries at once. It reduces network latency, deserialization overhead, and memory usage on the application server.
Caching Strategies
The application currently fetches content from Contentful on every request. For content that does not change frequently, this is highly inefficient. Implementing a server-side cache for Contentful responses is critical for performance.
Recommendations:
- Implement In-Memory Caching: Use ASP.NET Core's
IMemoryCacheto store the results of Contentful queries. This is a simple and effective way to reduce API calls for frequently accessed, semi-static content. - Cache Invalidation: A cache is only useful if its data is fresh. The best way to manage cache invalidation is to use Contentful webhooks. Configure a webhook in Contentful to call a secure endpoint in your application whenever content is published or unpublished. This endpoint should then clear the relevant cache keys.
Response caching
ASP.NET Core Response Caching middleware can be used to cache entire page responses on the server. This prevents the entire Razor Page or MVC action from executing on subsequent requests, offering a significant performance boost for pages that are identical for multiple users.
The application does not currently have response caching configured in Startup.cs.
Implementing Response Caching
Add Services: Register the response caching services in
ConfigureServices.csharp// File: TheExampleApp/Startup.cs (in ConfigureServices) public void ConfigureServices(IServiceCollection services) { // ... other services services.AddResponseCaching(); // ... }1
2
3
4
5
6
7
8Add Middleware: Add the middleware to the request pipeline in
Configure. It should be placed beforeUseMvc.csharp// File: TheExampleApp/Startup.cs (in Configure) public void Configure(IApplicationBuilder app, IHostingEnvironment env) { // ... app.UseStaticFiles(); app.UseSession(); app.UseResponseCaching(); // <-- Add response caching middleware // ... app.UseMvc(); }1
2
3
4
5
6
7
8
9
10
11
12
13Apply Cache Profiles: Use the
[ResponseCache]attribute on Razor Page models or MVC controllers to define caching behavior.csharp// Example for a course details page [ResponseCache(Duration = 600, Location = ResponseCacheLocation.Any, VaryByQueryKeys = new[] { "locale" })] public class CourseModel : BasePageModel { // ... }1
2
3
4
5
6
Vary By Parameters
It is crucial to vary the cache based on parameters that change the rendered output. For this application, the most important parameter is the locale.
VaryByQueryKeys: As shown in the example above, useVaryByQueryKeys = new[] { "locale" }to ensure that different language versions of a page (e.g.,?locale=en-USand?locale=de-DE) are cached as separate entries.VaryByRouteData: For pages whose content depends on a route parameter (e.g.,/courses/{slug}), use theVaryByRouteDataproperty to create a unique cache entry for each slug.
Static file optimization
Efficient delivery of static assets (CSS, JS, images, fonts) is essential for a fast front-end experience.
Bundling and Minification
The project uses bundling and minification for its CSS and JavaScript files to reduce the number of HTTP requests and the total file size. Stylesheets are located in wwwroot/stylesheets/ (e.g., style.css) and should be processed through a minification pipeline.
If using bundleconfig.json, a typical configuration would look like:
json
// Example: TheExampleApp/bundleconfig.json
[
{
"outputFileName": "wwwroot/stylesheets/style.min.css",
"inputFiles": [
"wwwroot/stylesheets/style.css"
]
},
{
"outputFileName": "wwwroot/js/site.min.js",
"inputFiles": [
"wwwroot/js/site.js"
],
"minify": {
"enabled": true,
"renameLocals": true
},
"sourceMap": false
}
]1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Note: This configuration relies on the BuildBundlerMinifier NuGet package. Developers should ensure this tool is run as part of the build process to keep the .min files updated.
Font Optimization
The application loads custom Roboto fonts in multiple weights and styles (regular, italic, bold, bold italic, medium) using WOFF2 and WOFF formats from the /fonts/ directory.
Recommendations:
Font Subsetting: Generate subsets of font files that include only the character ranges needed for your application's supported languages (Latin, German, etc.). This can significantly reduce font file sizes.
WOFF2 Priority: The application correctly prioritizes WOFF2 format (which offers better compression) with WOFF as a fallback. Ensure all fonts are available in WOFF2 format.
Font Loading Strategy: Consider using
font-display: swapin your@font-facedeclarations to prevent invisible text while fonts are loading. Add this tostyle.css:css@font-face { font-family: roboto; src: url(/fonts/roboto-regular-webfont.woff2) format("woff2"), url(/fonts/roboto-regular-webfont.woff) format("woff"); font-weight: 400; font-style: normal; font-display: swap; /* Add this line */ }1
2
3
4
5
6
7
8Preload Critical Fonts: For fonts used above the fold, add
<link rel="preload">tags in your layout to start loading fonts earlier in the page lifecycle.
CDN Integration
Currently, static files are served directly by the application server via app.UseStaticFiles(). For production environments, offloading asset delivery to a Content Delivery Network (CDN) is highly recommended.
Recommendations:
- Configure a CDN: In your production environment, configure a CDN (e.g., Azure CDN, Cloudflare, AWS CloudFront) to pull assets from your web application's origin.
- Update Asset Links: Modify the Razor layouts (
_Layout.cshtml) to reference the static assets via the CDN's URL. This can be managed with a configuration value that changes based on the environment. - Benefits: A CDN reduces latency for users globally, decreases the load on the application server, and provides an additional layer of caching. For more details on deployment strategies, see the Deployment Overview.
Session optimization
The application uses ASP.NET Core sessions to store user-specific information, primarily the selected locale.
Session Timeout Configuration
The session timeout is configured in Startup.cs with an unusually long duration.
csharp
// File: TheExampleApp/Startup.cs
services.AddSession(options => {
// IdleTimeout is set to a high value to confirm to requirements for this particular application.
// In your application you should use an IdleTimeout that suits your application needs or stick to the default of 20 minutes.
options.IdleTimeout = TimeSpan.FromDays(2);
});1
2
3
4
5
6
7
2
3
4
5
6
7
Analysis and Risks:
- Memory Consumption: The default session provider is in-memory. A long timeout means that session data for every visitor will be held in the server's memory for up to two days, even if the user is inactive. This can lead to high memory consumption under load.
- Recommendation: Review the requirement for a 2-day timeout. If it is not a hard requirement, reduce it to a more conventional value (e.g., 20-60 minutes) to conserve server memory.
Distributed Cache Options
The default in-memory session provider is not suitable for a scaled-out (multi-server) environment. If the application is deployed to more than one server instance, a user's session will be lost if their subsequent requests are routed to a different server.
Recommendations:
Use a Distributed Cache: For production and staging environments, replace the default in-memory session store with a distributed cache. This ensures session state is shared across all server instances.
Implementation:
- Choose a provider (e.g., Redis, SQL Server).
- Add the corresponding NuGet package (e.g.,
Microsoft.Extensions.Caching.StackExchangeRedis). - Register the service in
ConfigureServices.
csharp// Example using Redis services.AddStackExchangeRedisCache(options => { options.Configuration = Configuration.GetConnectionString("Redis"); options.InstanceName = "TheExampleApp_"; }); services.AddSession(options => { options.IdleTimeout = TimeSpan.FromMinutes(60); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; });1
2
3
4
5
6
7
8
9
10
11
12
This change is critical for ensuring application scalability and reliability. For more details, refer to the Session Management documentation.