Luke Latham
@guardrex
the guardrex chew
TweetShare on TwitterFacebookShare on FacebookGoogle+Share on Google+LinkedinShare on Linkedin

Razor Pages Compile-time Minification with ASP.NET Core 2.0

8/13/2017 By: Luke Latham

Learn how to use the extensibility features of the Razor engine in ASP.NET Core 2.0 to minify Razor pages at compile time.

In this article:

Introduction

Compile-time minification of markup with ASP.NET Core 1.x has been possible but only with difficulty. There's no easy way to plug in a third-party minification library so that markup from a View file can be intercepted prior to the Razor engine processing it. Because there isn't a simple extensibility point to intercept View content, you're left the the challenge of replacing a large amount of the Razor engine. To minify the content, you work with the "chunks" of markup that the engine uses, which are convienant for the Razor engine to process but difficult to manage for markup minification.

With the ASP.NET Core 2.0 Razor engine, the situation is much improved. An extensibility point is present that permits you to simply intercept View file content, minfify the content, and pass the content on to the Razor engine for processing.

Prerequisites

Sample App

Download a fully working sample to accompany this post from the guardrex/RazorMinification repo at GitHub.

The Setup

First, choose a 3rd party library that's capable of markup minification. This demo uses WebMarkupMin.Core (GitHub) by Andrey Taritsyn (GitHub, Blog, Twitter). WebMarkupMin.Core is a good choice because it minifies both the markup and inlined styles and scripts.

Replace the RazorTemplateEngine overriding the CreateCodeDocument method (ref source) with an implementation that performs the following work:

Here's a complete sample implementation:

public class CustomRazorTemplateEngine : RazorTemplateEngine
{
	private HtmlMinifier _htmlMinifier = new HtmlMinifier();

	public CustomRazorTemplateEngine(RazorEngine engine, RazorProject project) : 
		base(engine, project)
	{
		Options.ImportsFileName = "_ViewImports.cshtml";
	}

	public override RazorCodeDocument CreateCodeDocument(RazorProjectItem projectItem)
	{
		if (projectItem == null)
		{
			throw new ArgumentNullException($"{nameof(projectItem)} is null!");
		}

		if (!projectItem.Exists)
		{
			throw new InvalidOperationException($"{nameof(projectItem)} doesn't exist!");
		}

		Console.WriteLine();
		Console.WriteLine($"File: {projectItem.FileName}");

		using (var inputStream = projectItem.Read())
		{
			using (var reader = new StreamReader(inputStream))
			{
				var text = reader.ReadToEnd();

				var markupStart = text.IndexOf("<!DOCTYPE");
				var directives = text.Substring(0, markupStart);
				var markup = text.Substring(markupStart);

				text = directives + Minify(markup);

				var byteArray = Encoding.UTF8.GetBytes(text);
				var minifiedInputStream = new MemoryStream(byteArray);

				var source = RazorSourceDocument.ReadFrom(minifiedInputStream, projectItem.PhysicalPath);
				var imports = GetImports(projectItem);

				return RazorCodeDocument.Create(source, imports);
			}
		}
	}

	private string Minify(string markup)
	{
		MarkupMinificationResult result = 
			_htmlMinifier.Minify(markup, string.Empty, Encoding.UTF8, true);
		
		if (result.Errors.Count == 0)
		{
			MinificationStatistics statistics = result.Statistics;
			if (statistics != null)
			{
				Console.WriteLine();
				Console.WriteLine($"Original size: {statistics.OriginalSize:N0} Bytes | Minified size: {statistics.MinifiedSize:N0} Bytes | Saved: {statistics.SavedInPercent:N2}%");
			}
			//Console.WriteLine($"{Environment.NewLine}Minified content:{Environment.NewLine}{Environment.NewLine}{result.MinifiedContent}");

			return result.MinifiedContent;
		}
		else
		{
			IList<MinificationErrorInfo> errors = result.Errors;

			Console.WriteLine();
			Console.WriteLine($"Found {errors.Count:N0} error(s):");

			foreach (var error in errors)
			{
				Console.WriteLine($" - Line {error.LineNumber}, Column {error.ColumnNumber}: {error.Message}");
			}

			return markup;
		}
	}
}

Finally, wire up the Razor engine replacement when you build the host:

public class Program
{
	public static void Main(string[] args)
	{
		BuildWebHost(args).Run();
	}

	public static IWebHost BuildWebHost(string[] args) =>
		WebHost.CreateDefaultBuilder(args)
			.ConfigureServices(services =>
			{
				services.AddMvc();
				services.AddSingleton<RazorTemplateEngine, CustomRazorTemplateEngine>();
			})
			.Configure(app =>
			{
				app.UseDeveloperExceptionPage();
				app.UseMvcWithDefaultRoute();
				app.UseStaticFiles();
			})
			.Build();
}

WebMarkupMin.Core isn't so good at parsing Razor in a Razor page, so see the Index.cshtml page below to see how to escape sections of Razor code from WebMarkupMin.Core by placing the code inside of escape tags (<!--wmm:ignore-->...<!--/wmm:ignore-->).

Also note that the use of <text> tags prevents extra spaces and CR's from those Razor line transitions from making their way into the rendered markup.

@page
@model IndexModel
@inject Microsoft.AspNetCore.Hosting.IHostingEnvironment Host
<!DOCTYPE html>
<html lang="en">
<head>
	<title>Razor Pages Minification App</title>
</head>
<body>
	<h1>Razor Pages Minification App</h1>
	<p>@Host.ApplicationName @Model.DT</p>
	<figure style="text-align:center;width:400px">
		<img alt="Jack Burton" src="image.png">
		<figcaption>
			Jack Burton (Kurt Russell)<br>
			<em>Big Trouble in Little China</em><br>
			©1986 
			<a href="http://www.foxmovies.com/">
				20th Century Fox
			</a>
		</figcaption>
	</figure>
	<p>
		People:
		<ol>
			<!--wmm:ignore-->@for (var i = 0; i < Model.People.Count; i++)
			{
				var person = Model.People[i];
				<text><li>@person</li></text>
			}<!--/wmm:ignore-->
		</ol>
	</p>
	<p>
		<!--wmm:ignore-->@if (Model.People.Count < 10)
		{
			<text>There are less than ten people.</text>
		}<!--/wmm:ignore-->
	</p>
</body>
</html>

The final rendered output is minified:

<!DOCTYPE html><html lang=en><head><title>Razor Pages Minification App</title><body><h1>Razor Pages Minification App</h1><p>RazorMinification 8/14/2017 4:24:29 PM</p><figure style=text-align:center;width:400px><img alt="Jack Burton" src=image.png><figcaption>Jack Burton (Kurt Russell)<br><em>Big Trouble in Little China</em><br>©1986 <a href="http://www.foxmovies.com/">20th Century Fox</a></figcaption></figure><p>People:<ol><li>Susan</li><li>Catalina</li><li>Diego</li><li>Bob</li></ol><p>There are less than ten people.</p>
Rendered Razor page

Additional Resources

Acknowledgements


Project Badge
TweetShare on TwitterFacebookShare on FacebookGoogle+Share on Google+LinkedinShare on Linkedin