SaaS CMS has officially launched! Learn more now.

Stephan Lonntorp
Oct 26, 2018
(3 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 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;
                    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

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

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


Johan Petersson
Johan Petersson Oct 26, 2018 08:32 PM

Nice solution. Maybe you want to consider just sending these on html requests? Now these headers are apended to ALL requests.

Stephan Lonntorp
Stephan Lonntorp Oct 26, 2018 08:37 PM

Thanks for the feedback, I readily admit that I haven’t tested it that thoroughly.

My assumption was that requests for resources won’t have any required resources in the list, so there shouldn’t be any resources to send, but I’ll have to test that to verify. 

valdis Oct 30, 2018 12:52 PM

hmm.. interesting. this might be a good addition to upcoming package ;)

Please login to comment.
Latest blogs
How to have a link plugin with extra link id attribute in TinyMce

Introduce Optimizely CMS Editing is using TinyMce for editing rich-text content. We need to use this control a lot in CMS site for kind of WYSWYG...

Binh Nguyen Thi | Jul 13, 2024

Create your first demo site with Optimizely SaaS/Visual Builder

Hello everyone, We are very excited about the launch of our SaaS CMS and the new Visual Builder that comes with it. Since it is the first time you'...

Patrick Lam | Jul 11, 2024

Integrate a CMP workflow step with CMS

As you might know Optimizely has an integration where you can create and edit pages in the CMS directly from the CMP. One of the benefits of this i...

Marcus Hoffmann | Jul 10, 2024

GetNextSegment with empty Remaining causing fuzzes

Optimizely CMS offers you to create partial routers. This concept allows you display content differently depending on the routed content in the URL...

David Drouin-Prince | Jul 8, 2024 | Syndicated blog