From Friday, 09 April 2010, Google will use page speed to affect ranking of search results. If a page does not load within 2 seconds then the page will be ranked lower on the results index.
Read more about this at the Google Webmaster Central blog at http://bit.ly/c1YdJ1.
The question is – how will this affect public facing websites using SharePoint WCM? I ran Page Speed and YSlow against a staging/UAT server which I new had pretty poor performance and found without any surprise that the core JavaScript files within SharePoint will cause a large amount of load time.
Background: core.js is a file used by SharePoint that contains most of the shared functionality around dynamic behaviour. SharePoint also adds a suffix to scripts and images that looks like this in order to circumvent caching issues:
<script type="text/javascript" language="javascript" src="/_layouts/1033/core.js?rev=F8pbQQxa4zefcW%2BW9E5g8w%3D%3D" defer></script>
We can rectify this issue by either removing the reference all together or by removing the “rev” caching hack.
First, we need to add a HTTP handler that will install a page response filter that alters the HTML sent to the browser. This is done by adding a web application scoped feature that adds a web.config modification. For further information, see http://bit.ly/braiTi. The modification for this solution would be as follows:
private void AddFilterModule(SPWebApplication application)
{AddNodeValue(
"add[@name='CacheFilterModule']", "configuration/system.web/httpModules",@"<add name=""CacheFilterModule"" type=""MyAssembly.CacheFilter.FilterModule, MyAssembly, Version=1.0.0.0, Culture=neutral, PublicKeyToken=986e79c907efcc02""/>");
SaveWebConfig(application);
}
Ensure that the filter module adds the page filter as:
/// <summary>/// Ensures that the <see cref="CacheFilterStream"/> is connected to all documents of type text/html./// </summary>/// <remarks>/// To install, activate the "Enable SharePoint Asset Caching" feature on the web application./// </remarks>public class FilterModule : IHttpModule
{ #region IHttpModule Members /// <summary> /// Method not used as no objects needed to dispose has been created. /// </summary>public void Dispose()
{}
/// <summary> /// Monitor the <see cref="HttpApplication.ReleaseRequestState"/>, which is where we need to hook our filter into. /// </summary> /// <param name="context"> /// The <see cref="HttpApplication"/> containing the event to bind <see cref="InstallResponseFilter"/> to. /// </param>public void Init(HttpApplication context)
{context.ReleaseRequestState += InstallResponseFilter;
}
#endregion #region Methods /// <summary> /// Add the <see cref="CacheFilterStream"/> to all documents of type "text/html". /// </summary> /// <param name="sender"> /// The source of the event. /// </param> /// <param name="e"> /// An <see cref="EventArgs"/> that contains event data. /// </param>private void InstallResponseFilter(object sender,
EventArgs e)
{if (HttpContext.Current != null)
{HttpResponse response = HttpContext.Current.Response;
SPContext context = SPContext.GetContext(HttpContext.Current);
if (response.ContentType == "text/html" &&
context != null&&
context.FormContext.FormMode == SPControlMode.Display)
{CreateFilter(response);
}
}
}
/// <summary> /// Creates the HTTP filter. /// </summary> /// <param name="response"> /// The response stream to add the filter to. /// </param> /// <example> /// Override the method to add custom processing. /// <code> /// protected override void CreateFilter(HttpResponse response) /// { /// response.Filter = new CustomFilterStream(response.Filter); /// } /// </code> /// </example>protected virtual void CreateFilter(HttpResponse response)
{ response.Filter = new CacheFilterStream(response.Filter);}
#endregion}
I can then use a modifying stream to change the HTML output. In this case, I am using a base class that handles the HTML stream as:
/// <summary>/// A base class that provides the basic calls for a response filter./// </summary>/// <example>/// In this case, we create a page filter that is used to remove a copyright statement:/// <code>/// public class CopyrightFilter : ResponseFilterStream/// {/// protected override Transform()/// {/// Html = Html.Replace("Copyright &copy; 2008", string.Empty);/// }/// }/// </code>/// </example>public abstract class ResponseFilterStream : Stream
{ #region Constructor /// <summary> /// Initializes a new instance of the <see cref="ResponseFilterStream"/> class. /// </summary> /// <param name="stream">The stream.</param> protected ResponseFilterStream(Stream stream) {Stream = stream;
}
#endregion #region Properties /// <summary> /// Gets the instance reference of the passed filter stream. /// </summary>protected Stream Stream{get;private set;}
/// <summary> /// Gets or sets the HTML markup that was written to the filter from the stream. /// </summary>protected string Html{get;set;}
/// <summary> /// Gets the length in bytes of the stream. /// </summary>public override long Length
{get
{return Stream == null
? -1
: Stream.Length;
}
}
/// <summary> /// Gets or sets the position within the current stream. /// </summary>public override long Position{get;set;}
/// <summary> /// Gets a value indicating whether the current stream supports reading. /// </summary>public override bool CanRead
{get
{return true;
}
}
/// <summary> /// Gets a value indicating whether the current stream supports seeking. /// </summary>public override bool CanSeek
{get
{return true;
}
}
/// <summary> /// Gets a value indicating whether the current stream supports writing. /// </summary>public override bool CanWrite
{get
{return true;
}
}
#endregion #region Stream members /// <summary> /// Sets the position within the current stream. /// </summary> /// <param name="offset"> /// A byte offset relative to the <paramref name="origin"/> parameter. /// </param> /// <param name="origin"> /// A value of type <see cref="SeekOrigin"/> indicating the reference point used to obtain the new position. /// </param> /// <returns> /// The new position within the current stream. /// </returns>public override long Seek(
long offset,SeekOrigin
origin)
{ return Stream.Seek(offset,origin);
}
/// <summary> /// Sets the length of the current stream. /// </summary> /// <param name="value">The desired length of the current stream in bytes.</param>public override void SetLength(long value)
{ Stream.SetLength(value);}
/// <summary> /// Closes the current stream and releases any resources (such as sockets and file handles) associated with the current stream. /// </summary>public override void Close()
{Transform();
//Write the HTML to the stream. byte[] data = Encoding.UTF8.GetBytes(Html);Stream.Write(data,
0,
data.Length);
Stream.Close();
}
/// <summary> /// Clears all buffers for this stream and causes any buffered data to be written to the underlying device. /// </summary>public override void Flush()
{Stream.Flush();
}
/// <summary> /// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read. /// </summary> /// <param name="buffer"> /// An array of bytes. When this method returns, the buffer contains the specified byte array with the values between <paramref name="offset"/> and <c>(offset + count- 1)</c> replaced by the bytes read from the current source. /// </param> /// <param name="offset"> /// The zero-based byte offset in <paramref name="buffer"/> at which to begin storing the data read from the current stream. /// </param> /// <param name="count"> /// The maximum number of bytes to be read from the current stream. /// </param> /// <returns> /// The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached. /// </returns>public override int Read(
byte[] buffer, int offset, int count) { return Stream.Read(buffer,offset,
count);
}
/// <summary> /// Writes a sequence of bytes to the current stream and advances the current position within this stream by the number of bytes written. /// </summary> /// <param name="buffer"> /// An array of bytes. This method copies <paramref name="count"/> bytes from <paramref name="buffer"/> to the current stream. /// </param> /// <param name="offset"> /// The zero-based byte offset in <c>buffer</c> at which to begin copying bytes to the current stream. /// </param> /// <param name="count"> /// The maximum number of bytes to be written to the current stream. /// </param>public override void Write(
byte[] buffer, int offset, int count) {Html += Encoding.UTF8.GetString(buffer,
offset,
count);
}
/// <summary> /// Abstract method used to transform the contents of the <see cref="Html"/> field. /// </summary>protected abstract void Transform();
#endregion}
The actual transform code to remove the “rev” cache is as follows:
{/// <summary>/// Response filter that modifies the SharePoint output so that assets can be cached by proxy and browser caches./// </summary>public class CacheFilterStream : ResponseFilterStream
{ #region Constructor /// <summary> /// Initializes a new instance of the <see cref="CacheFilterStream"/> class. /// </summary> /// <param name="stream">The stream.</param>public CacheFilterStream(Stream stream) : base(stream)
{}
#endregion #region Overrides of ResponseFilterStream /// <summary> /// Abstract method used to transform the contents of the <see cref="ResponseFilterStream.Html"/> field. /// </summary>protected override void Transform()
{Html = Regex.Replace(Html, @"\?rev=[^""]*""", @"""", RegexOptions.Compiled | RegexOptions.IgnoreCase);
}
#endregion}
When running this in YSlow we can see that the assets are now properly cached. This only helps when the assets are indeed cached and removing them altogether could be a viable solution instead.
0 comments:
Post a Comment
Feel free to add your comment to this post. All comments are moderated and may not appear immediately within the page.