Take the community feedback survey now.

Stephan Lonntorp
Oct 26, 2018
  69
(0 votes)

Performance Optimization: Preloading required client resources and pushing them to the browser with HTTP/2 Server Push

Today, a colleague of mine asked me if we could utilize the new HTTP/2 Server Push feature in Cloudflare, for one of our DXC-Service clients. I thought it wouldn't be too hard to accomplish, and I was right.

The HTTP/2 Server Push feature in Cloudflare relies on the Link HTTP Header, that can be used instead of a regular <link rel="preload"> element in the <head> of your page, and works like this: Any resources you specify in the Links header, will be checked if it is local, and if it is, pushed along with the page directly to the client, and removed from the HTTP header. Resources that aren't local are left in the header value, so your clients can still benefit from preloading. Read more about this here. Please, note, that even if you don't use DXC Service, you can still use Cloudflare, and even if you don't use Cloudflare, your visitors can still benefit from asset preloading.

If you aren't using ClientResources for your own styles and scripts, either implement that, or just add them manually in the module.

Step 1: If you are using DXC-Service, email support@episerver.com and kindly ask them to enable the HTTP/2 Server Push feature in Cloudflare for your subscription.

Step 2: Implement an IHttpModule like this:

using System.Collections.Generic;
using System.Linq;
using System.Web;
using EPiServer.Framework.Web.Resources;
using EPiServer.ServiceLocation;

namespace My.Fully.Qualified.Namespace
{
    public class AssetPreloadModule : IHttpModule
    {
        public void Init(HttpApplication context)
        {
            context.PreSendRequestHeaders += SendLinkHeaderForRequiredResources;
        }

        private static void SendLinkHeaderForRequiredResources(object sender, System.EventArgs e)
        {
            if (application.Context.Response.ContentType == "text/html")
            {
                var requiredResources = ServiceLocator.Current.GetInstance<IRequiredClientResourceList>().GetRequiredResourcesSettings().ToArray();
                var requiredResourceService = ServiceLocator.Current.GetInstance<IClientResourceService>();
                var assets = requiredResources.SelectMany(
                    setting => requiredResourceService
                        .GetClientResources(setting.Name, new[] { ClientResourceType.Script, ClientResourceType.Style })
                        .Where(x => x.IsStatic), (setting, clientResource) => GetPreloadLink(clientResource)).ToList();
                if (assets.Any())
                {
                    AddLinkTag(application.Context, assets);
                }
            }
        }

        private static AssetPreloadLink GetPreloadLink(ClientResource clientResource)
        {
            var link = new AssetPreloadLink
            {
                Type = ConvertType(clientResource.ResourceType),
                Url = clientResource.Path
            };
            return link;
        }

        private static AssetType ConvertType(ClientResourceType resourceType)
        {
            switch (resourceType)
            {
                case ClientResourceType.Script:
                    return AssetType.Script;
                case ClientResourceType.Style:
                    return AssetType.Style;
                default:
                    return AssetType.Unknown;
            }
        }

        private static void AddLinkTag(HttpContext context, IEnumerable<AssetPreloadLink> links)
        {
            context.Response.AppendHeader("Link", string.Join(",", links));
        }

        public void Dispose()
        {
        }
    }

    internal class AssetPreloadLink
    {
        private const string Format = "<{0}>; rel=preload; as={1}";
        public string Url { get; set; }
        public AssetType Type { get; set; }
        public bool NoPush { get; set; }

        public override string ToString()
        {
            if (Type != AssetType.Unknown)
            {
                var output = string.Format(Format, Url, Type.ToString().ToLowerInvariant());
                if (NoPush)
                {
                    return output + "; nopush";
                }
                return output;
            }
            return string.Empty;
        }
    }

    internal enum AssetType
    {
        Unknown = 0,
        Script = 100,
        Style = 200,
        Image = 300
    }
}

Step 3: Add the module to your web.config

<configuration>
  <system.webServer>
    <modules runAllManagedModulesForAllRequests="true">
      <add name="AssetPreloadModule" type="My.Fully.Qualified.Namespace.AssetPreloadModule, My.Assemblyname" />
    </modules>   
  </system.webServer>
</configuration>

Step 4: Build, run, and watch your conversion rates soar!

Note: Thanks to Johan Petersson for alerting me to the fact that I appended the header to every request. Although my assumption was correct, in that requests for resources other than html requests won't have any required resources in the IRequiredClientResourceList, executing the lookup for those requests was unnecessary. If you manually add resources that are not in the list, the check with assets.Any() can be removed.

Oct 26, 2018

Comments

Please login to comment.
Latest blogs
A day in the life of an Optimizely OMVP - Opticon London 2025

This installment of a day in the life of an Optimizely OMVP gives an in-depth coverage of my trip down to London to attend Opticon London 2025 held...

Graham Carr | Oct 2, 2025

Optimizely Web Experimentation Using Real-Time Segments: A Step-by-Step Guide

  Introduction Personalization has become de facto standard for any digital channel to improve the user's engagement KPI’s.  Personalization uses...

Ratish | Oct 1, 2025 |

Trigger DXP Warmup Locally to Catch Bugs & Performance Issues Early

Here’s our documentation on warmup in DXP : 🔗 https://docs.developers.optimizely.com/digital-experience-platform/docs/warming-up-sites What I didn...

dada | Sep 29, 2025

Creating Opal Tools for Stott Robots Handler

This summer, the Netcel Development team and I took part in Optimizely’s Opal Hackathon. The challenge from Optimizely was to extend Opal’s abiliti...

Mark Stott | Sep 28, 2025

Integrating Commerce Search v3 (Vertex AI) with Optimizely Configured Commerce

Introduction This blog provides a technical guide for integrating Commerce Search v3, which leverages Google Cloud's Vertex AI Search, into an...

Vaibhav | Sep 27, 2025

A day in the life of an Optimizely MVP - Opti Graph Extensions add-on v1.0.0 released

I am pleased to announce that the official v1.0.0 of the Opti Graph Extensions add-on has now been released and is generally available. Refer to my...

Graham Carr | Sep 25, 2025