Wednesday 13 April 2011

Removing Unnecessary CSS

I've been recently looking at​ performance of public-facing websites built with SharePoint 2010 and, following on from Chris O'Brien's blog on removing unnecessary JavaScript files for anonymous users that are loaded as part of the Script-on-Demand framework, I was browsing through the kinds of files requested and whether they were important or not.

One thing I noticed was that there are several CSS files also loaded by SharePoint that are not always necessary for anonymous users. Things like controls.css or forms.css and maybe even corev4.css in some cases may not be needed. Therefore, I decided to approach the problem with the same mindset already adopted for the additional JavaScripts which made some sense to be able to have a control which would be able to strip out unnecessary or superfluous JavaScript and CSS files. In addition to Chris O'Brien's code example, here's mine:

private List _cssFiles = new List();
private List _cssFileIndicesToBeRemoved = new List();
private List _foundCssFiles = new List();

public string CSSFiles { get; set; }

protected override void OnInit(EventArgs e)
{
    base.OnInit(e);

    // store the css files property into the class private List variables
    if (String.IsNullOrEmpty(this.CSSFiles) == false)
    {
        string[] css = this.CSSFiles.Split(new char[] { ';' });
        this._cssFiles.AddRange(css);
    }
}

protected override void OnPreRender(EventArgs e)
{
    base.OnPreRender(e);

    // remove files but only for anonymous users
    if (HttpContext.Current.User.Identity.IsAuthenticated == false)            
    {
        // this.RemoveScripts();
        this.RemoveCSS();
    }
}

private void RemoveCSS()
{
    int index = 0;
    for (int iteration = 0; iteration < 2; iteration++)
    {
        IList files = null;
        FieldInfo prop = null;
        this._cssFileIndicesToBeRemoved = new List();

        if (iteration == 0)
            prop = this.GetFieldInfo(SPContext.Current, "m_css");
        else
            prop = this.GetFieldInfo(SPContext.Current, "m_orderedCss");
                
        if (prop != null)
        {
            files = (IList)prop.GetValue(SPContext.Current);
            foreach (var file in files)
            {
                FieldInfo cssRegisterProp = this.GetFieldInfo(file, "CssReference");
                if (cssRegisterProp != null)
                {
                    string cssFileName = cssRegisterProp.GetValue(file).ToString();

                    // add the file to the list of indices for removal
                    if (string.IsNullOrEmpty(this._cssFiles.Find(delegate(string sFound)
                    {
                        return cssFileName.ToLower().Contains(sFound.ToLower());
                    })) == false)
                    {
                        this._cssFileIndicesToBeRemoved.Add(index);
                    }
                    else
                    {
#if DEBUG
                        this._foundCssFiles.Add(cssFileName);
#endif
                    }
                }
                index++;
            }
                    
            // reset the index counter and remove the required files
            index = 0;
            foreach (int j in this._cssFileIndicesToBeRemoved)
            {
                files.RemoveAt(j - index);
                index++;
            }
            prop.SetValue(SPContext.Current, files);                    
        }
    }
}

private FieldInfo GetFieldInfo(object Type, string FieldName)
{
    Type t = Type.GetType();
    FieldInfo prop = t.GetField(FieldName, BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
    return prop;
}
 
To explain a little. The CSS files are "registered" using the CSSRegistration control. When a CSS file is registered it gets stored in a CSSReferences property of the current SPContext which is an internal property of type ICollection<CssRegistrationRecord>. This property is read-only which means that while we can look at the collection we cannot write any changes back through this property. Fortunately, by looking at the Get for this property, we can see that it uses two private variables - m_css and m_orderedCss - to work out the ICollection contents. By accessing these two variables directly and changing them using reflection we can trim out the unnecessary files in the same way that we can trim out unwanted JavaScript files for anonymous users.

In the example above I haven't included Chris's code or class-level variables to make it slightly easier to read. Suffice to say that I've added his code into a separate method which is called from the OnPreRender event (see the commented out this.RemoveScripts(); line). I've also changed the OnInit slightly to check for empty strings prior to attempting to extract the entered value into the internal List variable.

2 comments:

  1. As an additional note: I added the #if DEBUG section to display the CSS files that would be output which is probably not that useful in it's current position, but this could be moved around to capture all the output files which may be more useful to see when you're first implementing this so you can determine which files you can remove.

    ReplyDelete
  2. Dear Stephen,
    Thanks for this post,I put this control in master page but the corev4.css is still being rendered , although if i put other custom css file in property CSSFiles it will be removed,so would you please help me to find the reason for this.
    Thanks

    ReplyDelete