HTTP/2 Server Push and ASP.NET MVC

One of the new features in HTTP/2 is Server Push. It allows the server to send resources to the browser without having to wait for the browser to request it. Normally the browser requests needed resources after receiving and parsing the HTML. That creates cost of additional Round Trip Times. If we push critical CSS and JS during the initial request the cost of additional Round Trip Times can be minimized. Also the pushed resources can be cached (which was impossible in cases where inlining was being used for the same purpose).

I don't intend to provide comprehensive information regarding HTTP/2 Server Push here as there is a lot of resources on the web doing that already, all I want is to play a little with Server Push in context of ASP.NET MVC powered by IIS. For that purposes I have created a demo application consisting of following model, view and controller.

public interface IStarWarsCharacters
{
    IEnumerable<string> GetCharacters();
}

public class LazyStarWarsCharacters : IStarWarsCharacters
{
    public IEnumerable<string> GetCharacters()
    {
        foreach(Character character in StarWarsContext.Characters)
        {
            yield return String.Format("{0} ({1})", character.Name,
                character.HomeworldId.HasValue ? StarWarsContext.Planets.First(p => p.Id == character.HomeworldId.Value).Name : "N/A");
            System.Threading.Thread.Sleep(10);
        }
    }
}
@model IStarWarsCharacters
<!DOCTYPE html>
<html>
    <head>
        <link rel="stylesheet" href="~/content/normalize.css">
        <link rel="stylesheet" href="~/content/site.css">
    </head>
    <body>
        <ul>
            @foreach (string starWarsCharacter in Model.GetCharacters())
            {
                <li>@starWarsCharacter</li>
            }
        </ul>
        ...
    </body>
</html>
public class DemoController : Controller
{
    [ActionName("server-push")]
    public ActionResult ServerPush()
    {
        return View("ServerPush", new LazyStarWarsCharacters());
    }
}

For this demo application I've captured the basic timings as visible on screen shot below.

Chrome Developer Tools Network Tab - No Server Push

Proof of Concept HtmlHelper

The IIS built in support for Server Push is exposed to the ASP.NET through HttpResponse.PushPromise method which takes virtual path to the resource which is supposed to be pushed. My first idea for using this method in ASP.NET MVC was HtmlHelper. The idea is simple, whenever a link element is being rendered the helper registers the push as well.

public static class PushPromiseExtensions
{
    public static IHtmlString PushPromiseStylesheet(this HtmlHelper htmlHelper, string contentPath)
    {
        UrlHelper urlHelper = new UrlHelper(htmlHelper.ViewContext.RequestContext);

        htmlHelper.ViewContext.RequestContext.HttpContext.Response.PushPromise(contentPath);

        TagBuilder linkTagBuilder = new TagBuilder("link");
        linkTagBuilder.Attributes.Add("rel", "stylesheet");
        linkTagBuilder.Attributes.Add("href", urlHelper.Content(contentPath));

        return new HtmlString(linkTagBuilder.ToString());
    }
}

The only thing which needs to be done is replacing the links to the resources which should be pushed with calls to the helper.

@model IStarWarsCharacters
<!DOCTYPE html>
<html>
    <head>
        @Html.PushPromiseStylesheet("~/content/normalize.css")
        @Html.PushPromiseStylesheet("~/content/site.css")
    </head>
    <body>
        <ul>
            @foreach (string starWarsCharacter in Model.GetCharacters())
            {
                <li>@starWarsCharacter</li>
            }
        </ul>
        ...
    </body>
</html>

Below screen shot represents timings after the change.

Chrome Developer Tools Network Tab - Server Push During Render (Lazy Data Access)

Even with local connection and small resources the difference is visible. The browser receives initial part of push early which results in very short resource retrieval time.

"When to push?" and "What to push?" are very important

To be honest I've cheated - I've prepared the demo application in a way which supported my initial idea. The model has been prepared so the time expensive processing is being moved to the render phase. Building web applications like that is very good practice but often not possible. Usually the data are being grabbed from database, processed and then passed to the view. Below model is closer to that scenario.

public class EagerStarWarsCharacters : IStarWarsCharacters
{
    IEnumerable<string> _characters;

    public EagerStarWarsCharacters()
    {
        List<string> characters = new List<string>();

        foreach (Character character in StarWarsContext.Characters)
        {
            characters.Add(String.Format("{0} ({1})", character.Name,
                character.HomeworldId.HasValue ? StarWarsContext.Planets.First(p => p.Id == character.HomeworldId.Value).Name : "N/A"));
            System.Threading.Thread.Sleep(10);
        }

        _characters = characters;
    }

    public IEnumerable<string> GetCharacters()
    {
        return _characters;
    }
}

Small change to the action will show the impact on timings.

[ActionName("server-push")]
public ActionResult ServerPush()
{
    return View("ServerPush", new EagerStarWarsCharacters());
}

Chrome Developer Tools Network Tab - Server Push During Render (Eager Data Access)

Because the helper is being executed after the time consuming processing and nothing interrupts the view during writing to the response stream, the push is being send after the HTML. We can still see gain when it comes to the single resources retrieval times (no Round Trip Times) but the overall improvement depends on what is being pushed. When large resources (especially in high numbers) are being pushed, a situation can be reached where classic approach with browser utilizing multiple parallel connections turns out to be more efficient.

Moving to ActionFilter

Knowing that pushing too late might be an issue I've started thinking on a way to push as soon as possible. An ActionFilter was a natural choice. First I needed some kind of registration table with mappings between actions and resources to be pushed. I've also needed to be able to get all resources for given action quickly. A simple abstraction over nested dictionary seemed good enough to start with.

public class PushPromiseTable
{
    private readonly IDictionary<string, IDictionary<string, ICollection<string>>> _pushPromiseTable =
        new Dictionary<string, IDictionary<string, ICollection<string>>>();

    public void MapPushPromise(string controller, string action, string contentPath)
    {
        if (!_pushPromiseTable.ContainsKey(controller))
        {
            _pushPromiseTable.Add(controller, new Dictionary<string, ICollection<string>>());
        }

        if (!_pushPromiseTable[controller].ContainsKey(action))
        {
            _pushPromiseTable[controller].Add(action, new List<string>());
        }

        _pushPromiseTable[controller][action].Add(contentPath);
    }

    internal IEnumerable<string> GetPushPromiseContentPaths(string controller, string action)
    {
        IEnumerable<string> pushPromiseContentPaths = Enumerable.Empty<string>();

        if (_pushPromiseTable.ContainsKey(controller))
        {
            if (_pushPromiseTable[controller].ContainsKey(action))
            {
                pushPromiseContentPaths = _pushPromiseTable[controller][action];
            }
        }

        return pushPromiseContentPaths;
    }
}

The attribute implementation was pretty straight forward. All that needed to be done was getting resources from the registration table based on controller and action, and then pushing them all.

[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, Inherited = true, AllowMultiple = false)]
public class PushPromiseAttribute : FilterAttribute, IActionFilter
{
    private PushPromiseTable _pushPromiseTable;

    public PushPromiseAttribute(PushPromiseTable pushPromiseTable)
    {
        if (pushPromiseTable == null)
        {
            throw new ArgumentNullException(nameof(pushPromiseTable));
        }

        _pushPromiseTable = pushPromiseTable;
    }

    public void OnActionExecuted(ActionExecutedContext filterContext)
    { }

    public void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (filterContext == null)
        {
            throw new ArgumentNullException(nameof(filterContext));
        }

        IEnumerable<string> pushPromiseContentPaths = _pushPromiseTable.GetPushPromiseContentPaths(
            filterContext.ActionDescriptor.ControllerDescriptor.ControllerName,
            filterContext.ActionDescriptor.ActionName);

        foreach (string pushPromiseContentPath in pushPromiseContentPaths)
        {
            filterContext.HttpContext.Response.PushPromise(pushPromiseContentPath);
        }
    }
}

The only thing left to do was registering the ActionFilter with proper configuration and reverting view to its original form.

PushPromiseTable pushPromiseTable = new PushPromiseTable();
pushPromiseTable.MapPushPromise("Demo", "server-push", "~/content/normalize.css");
pushPromiseTable.MapPushPromise("Demo", "server-push", "~/content/site.css");

GlobalFilters.Filters.Add(new PushPromiseAttribute(pushPromiseTable));
@model IStarWarsCharacters
<!DOCTYPE html>
<html>
    <head>
        <link rel="stylesheet" href="~/content/normalize.css">
        <link rel="stylesheet" href="~/content/site.css">
    </head>
    <body>
        <ul>
            @foreach (string starWarsCharacter in Model.GetCharacters())
            {
                <li>@starWarsCharacter</li>
            }
        </ul>
        ...
    </body>
</html>

Thanks to the usage of ActionFilter the improvement is visible in timings again.

Chrome Developer Tools Network Tab - ActionFilter Server Push

What about ASP.NET Core

Unfortunately similar mechanisms are not available in ASP.NET Core yet as the API is not available. There are issues created for both Kestrel and HTTP abstractions to provide support but there are no information on planed delivery.