Sitecore Performance Part 6: Minifying Rendering Output

Removing whitespace and other “unnecessary” text from HTML markup, CSS, and JavaScript is a time-honored way to squeeze a few kilobytes out of each page request. This post will discuss a quick way to ensure that the contents of any cacheable rendering are minified.

We’re going to assume you’re using Sitecore 8.x and MVC for this solution.

The Sitecore mvc.RenderRendering Pipeline

Here’s the stock pipeline, which can be found in the /App_Config/Include/Sitecore.Mvc.config file:


<mvc.renderRendering>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.InitializeProfiling, Sitecore.Mvc"/>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.ResolveArea, Sitecore.Mvc">
		<param desc="areaResolver" type="Sitecore.Mvc.Pipelines.Response.RenderRendering.ChainedAreaResolveStrategy, Sitecore.Mvc">
			<Resolvers hint="list">
				<resolver type="Sitecore.Mvc.Pipelines.Response.RenderRendering.RenderingDefinitionAreaResolveStrategy, Sitecore.Mvc"/>
				<resolver type="Sitecore.Mvc.Pipelines.Response.RenderRendering.RenderingParametersAreaResolveStrategy, Sitecore.Mvc"/>
				<resolver type="Sitecore.Mvc.Pipelines.Response.RenderRendering.RenderingLayoutAreaResolveStrategy, Sitecore.Mvc"/>
			</Resolvers>
		</param>
	</processor>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.SetCacheability, Sitecore.Mvc"/>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.GenerateCacheKey, Sitecore.Mvc"/>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.RenderFromCache, Sitecore.Mvc"/>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.StartRecordingOutput, Sitecore.Mvc"/>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.EnterRenderingContext, Sitecore.Mvc"/>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.ExecuteRenderer, Sitecore.Mvc"/>
	<processor type="Sitecore.Mvc.Pipelines.Response.RenderRendering.AddRecordedHtmlToCache, Sitecore.Mvc"/>
</mvc.renderRendering>

We’re interested in the last processor on this list: AddRecordedHtmlToCache.

Effectively, the mvc.RenderRendering pipeline starts with some context information and the TextWriter that represents the output buffer for the response. If the Rendering being processed supports caching, Sitecore adds a second TextWriter which records the output of just the current rendering—this is what is added to the Sitecore HTML Cache. The recorder is added in the StartRecordingOutput processor and is called in the AddRecordedHtmlToCache processor. We’re going to descend from Sitecore’s processor and add some minification to the output.

Our AddRecordedHtmlToCache Processor

Here’s the code:

namespace Constellation.Sitecore.Presentation.Mvc.Pipelines.RenderRendering
{
	using global::Sitecore.Diagnostics;
	using global::Sitecore.Mvc.Common;
	using global::Sitecore.Mvc.Pipelines.Response.RenderRendering;
	using System.Text;
	using WebMarkupMin.Core.Minifiers;
	using WebMarkupMin.Core.Settings;

	/// <summary>
	/// Replacement for the stock Sitecore MVC response pipeline step that minifies the output of the Rendering.
	/// </summary>
	/// <remarks>
	/// To replace the stock pipeline handler, override the same-named pipeline handler in the Mvc.RenderRenderings pipeline.
	/// Uses https://webmarkupmin.codeplex.com/ to handle the minification. Settings for WebMarkupMin are stored in the
	/// Web.config file using the traditional custom configuration section technology, *not* Sitecore configuration.
	/// This handler will default to normal behavior if minification fails for some reason.
	/// Minification only occurs in PageMode.Normal, allowing for easier troubleshooting in Preview and Editor modes.
	/// </remarks>
	public class AddRecordedHtmlToCache : global::Sitecore.Mvc.Pipelines.Response.RenderRendering.AddRecordedHtmlToCache
	{
		protected override void UpdateCache(string cacheKey, RenderRenderingArgs args)
		{
			var recordingTextWriter = args.Writer as RecordingTextWriter;
			if (recordingTextWriter == null)
			{
				return;
			}

			if (!global::Sitecore.Context.PageMode.IsNormal)
			{
				this.AddHtmlToCache(cacheKey, recordingTextWriter.GetRecording(), args);
			}

			var recording = Minify(recordingTextWriter.GetRecording());
			this.AddHtmlToCache(cacheKey, recording, args);
		}

		/// <summary>
		/// Safely Attempts to minify the provided string.
		/// </summary>
		/// <param name="recording">The string to minify</param>
		/// <returns>Minified version of the string, or, if errors were encountered, returns the original string.</returns>
		private string Minify(string recording)
		{
			var settings = new HtmlMinificationSettings();
			var cssMinifier = new KristensenCssMinifier();
			var jsMinifier = new CrockfordJsMinifier();

			var minifier = new HtmlMinifier(settings, cssMinifier, jsMinifier);

			MarkupMinificationResult result = minifier.Minify(recording);

			if (result.Errors.Count != 0)
			{
				var builder = new StringBuilder("Attempt to minify rendering failed");

				foreach (var error in result.Errors)
				{
					builder.AppendLine(error.Category + " - " + error.Message);
				}

				Log.Warn(builder.ToString(), this);
				return recording;
			}

			return result.MinifiedContent;
		}
	}
}

We have to override one protected method: UpdateCache(). Here, we use our old friend WebMarkupMin to adjust the recorded string, then ship it to Sitecore’s HTML cache. The next request will serve a smaller, minified version of the Rendering’s output.

The beauty of WebMarkupMin is that it ships with configuration settings we can adjust through the web.config. Here’s a fairly safe example that probably won’t break your front-end developer’s code:


<webMarkupMin xmlns="http://tempuri.org/WebMarkupMin.Configuration.xsd">
<webExtensions enableMinification="true" disableMinificationInDebugMode="true" enableCompression="true" disableCompressionInDebugMode="true" maxResponseSize="100000" disableCopyrightHttpHeaders="true" />
	<core>
	  <html whitespaceMinificationMode="Medium" removeHtmlComments="true" removeHtmlCommentsFromScriptsAndStyles="true" removeCdataSectionsFromScriptsAndStyles="true" useShortDoctype="true" useMetaCharsetTag="true" emptyTagRenderMode="NoSlash" removeOptionalEndTags="true" removeTagsWithoutContent="false" collapseBooleanAttributes="true" removeEmptyAttributes="true" attributeQuotesRemovalMode="KeepQuotes" removeRedundantAttributes="false" removeJsTypeAttributes="true" removeCssTypeAttributes="true" removeHttpProtocolFromAttributes="false" removeHttpsProtocolFromAttributes="false" removeJsProtocolFromAttributes="true" minifyEmbeddedCssCode="true" minifyInlineCssCode="true" minifyEmbeddedJsCode="true" minifyInlineJsCode="true" processableScriptTypeList="" minifyKnockoutBindingExpressions="false" minifyAngularBindingExpressions="false" customAngularDirectiveList="" />
	  <css>
		<minifiers>
		  <add name="NullCssMinifier" displayName="Null CSS Minifier" type="WebMarkupMin.Core.Minifiers.NullCssMinifier, WebMarkupMin.Core" />
		  <add name="KristensenCssMinifier" displayName="Mads Kristensen's CSS minifier" type="WebMarkupMin.Core.Minifiers.KristensenCssMinifier, WebMarkupMin.Core" />
		</minifiers>
	  </css>
	  <js>
		<minifiers>
		  <add name="NullJsMinifier" displayName="Null JS Minifier" type="WebMarkupMin.Core.Minifiers.NullJsMinifier, WebMarkupMin.Core" />
		  <add name="CrockfordJsMinifier" displayName="Douglas Crockford's JS Minifier" type="WebMarkupMin.Core.Minifiers.CrockfordJsMinifier, WebMarkupMin.Core" />
		</minifiers>
	  </js>
	  <logging>
		<loggers>
		  <add name="NullLogger" displayName="Null Logger" type="WebMarkupMin.Core.Loggers.NullLogger, WebMarkupMin.Core" />
		  <add name="ThrowExceptionLogger" displayName="Throw exception logger" type="WebMarkupMin.Core.Loggers.ThrowExceptionLogger, WebMarkupMin.Core" />
		</loggers>
	  </logging>
	</core>
</webMarkupMin>

Note that the minification process is disabled if your code is running in debug mode, which makes it handy for HTML bug diagnostics on your local machine, or your QC server.

Caveat Emptor

This trick does have flaws:

  • Only Renderings and Sublayouts that run through the MVC pipeline are minified – Controller Renderings, Views, Item Renderings, etc.
  • Minification is in evidence on the second page load, which comes from the Sitecore cache.
  • Only Rendering objects where caching is enabled are minified.

These flaws aside, it’s a great way to passively improve your Sitecore performance without a lot of developer intervention. In all-MVC projects with well factored Renderings, it’s easy to achieve minification of 80% on most pages.

The Last Mile

If you really want to minify 95% of every page regardless of caching, you can use WebMarkupMin’s ASP.NET MVC library. The library includes Attributes you can add to Controller Actions to explicitly minify the output. If your scenario requires a Controller Rendering, these Attributes can help close the gap on un-minified content.

If you’d like to see more of our Sitecore Page Performance series those articles can be found here. In the next post we’ll finish our Sitecore optimization series by improving IIS and ASP.NET responsiveness.

One thought on “Sitecore Performance Part 6: Minifying Rendering Output

  1. Chen Hendrawan

    >> Only Rendering objects where caching is enabled are minified.

    You could overcome that problem by creating new pipeline right after the ExecuteRenderer instead of overriding AddRecordedHtmlToCache, no?

    Reply

Have Something to Say?

Your email address will not be published. Required fields are marked *

20 − 9 =

Recent Posts

Recent Comments

Archives

Categories

Meta