One of the many reasons a web page can be slow is merely the quantity of information that must be sent to a user’s browser; pictures, video, maps, and huge quantities of text can lead to poor response times, even though the server and all processes behind it are running efficiently.
ASP.NET has a page state management mechanism known as “ViewState”, by which changes to page markup (altered text, colors, dynamic formatting etc.) are retained. On a postback that encoded ViewState data is made available to the server, and the .NET environment automatically updates the state of web control properties to represent the combination of the original markup and the ViewState-retained changes. ASP.NET provides an optional SessionPageStatePersister to retain the ViewState in the user’s Session data, and other persisters can be written whereby the ViewState may be stored anywhere you desire. By default ASP.NET persists ViewState as base-64 encoded text in a hidden input field in the HTML delivered to the browser.
Developers need to understand how ViewState works, and what it does (and does not do), to suss how their web pages actually function. For an excellent and detailed writeup on ViewState, see Dave Reed’s article Truly Understanding ViewState.
One issue we experienced on a current project in Calgary was with excessive ViewState size. For example, we frequently use a custom control to generate a customized scrollable table of data on a web page, and these tables frequently contain hundreds of rows and many columns of data. Unless switched off, ViewState is maintained automatically for all this generated data, in effect causing the same data to be transmitted twice to the user’s browser. Often we want the ViewState retained as a necessary evil, however, to support client-side sorting of the data rows.
Sometimes these large tables are inside an UpdatePanel or an AJAX TabContainer, both of which retain their own ViewState plus all the ViewState of all the controls inside them. Now we have even more ViewState to cope with, and for tables contained in multiple tabs in a TabContainer, the actual ViewState size can get unexpectedly large; we have seen a relatively benign popup page with almost 6MB of ViewState! Response time was rather unpleasant for locally connected users, and downright unacceptable for those at remote sites.
Rather than refactoring individual pages, we took a more global approach: compressing the ViewState before delivery. This approach was very successful, primarily because of what the ViewState contains. Remember that it represents all data not explicitly coded in the HTML markup, which includes all dynamic formatting and placement specifications. For a large table, there is massive repetition of various property settings, ideal data for compression algorithms.
Our implementation was simplified by the fact that all our code-behind partial classes inherit a project-wide BasePage, which is where we placed the compression and decompression logic. In ASP.NET, the System.Web.UI.Page class contains two overrideable methods that were ideal for our compression logic: SavePageStateToPersistenceMedium() and LoadPageStateFromPersistenceMedium().
Sample compression code is shown below, and requires the SharpZipLib compression library.
using ICSharpCode.SharpZipLib.Zip.Compression; using ICSharpCode.SharpZipLib.Zip.Compression.Streams; ... private const int BUFFER_SIZE = 65536; private int viewStateCompression = Deflater.NO_COMPRESSION; public int ViewStateCompression { get { return viewStateCompression; } set { viewStateCompression = value; } } protected override void SavePageStateToPersistenceMedium(Object state) { if (ViewStateCompression == Deflater.NO_COMPRESSION) { base.SavePageStateToPersistenceMedium(state); return; } Object viewState = state; if (state is Pair) { Pair statePair = (Pair) state; PageStatePersister.ControlState = statePair.First; viewState = statePair.Second; } using (StringWriter writer = new StringWriter()) { new LosFormatter().Serialize(writer, viewState); string base64 = writer.ToString(); byte[] compressed = Compress(Convert.FromBase64String((base64))); PageStatePersister.ViewState = Convert.ToBase64String(compressed); } PageStatePersister.Save(); } private byte[] Compress(byte[] bytes) { using (MemoryStream memoryStream = new MemoryStream(BUFFER_SIZE)) { Deflater deflater = new Deflater(ViewStateCompression); using (Stream stream = new DeflaterOutputStream(memoryStream, deflater, BUFFER_SIZE)) { stream.Write(bytes, 0, bytes.Length); } return memoryStream.ToArray(); } }
Looking at the “Save” logic, you’ll see that the default setting for compression is to not do any compression at all. Other deflater settings include DEFAULT_COMPRESSION, BEST_SPEED and BEST_COMPRESSION. The ViewStateCompression property must be set before ViewState is retrieved, in the Page_PreInit() or Page_Init() method. ViewState is available as the Second part of a Pair object (the first part references ControlState, which is different than the page’s ViewState. We decided not to compress it due to its limited use and small size. See Bean Software’s ControlState Property Demystified). We grab the ViewState object hierarchy, serialize it using the same System.Web.UI.LosFormatter that ASP.NET uses to serialize ViewState, compress it using SharpZipLib, System.Convert it to a base-64 string again, and hand it to the PageStatePersister to be written out. Use the PageStatePersister to write to the normal __VIEWSTATE hidden field; the AJAX toolkit gets upset if you manually write it to any other field.
The reverse is done on a PostBack:
protected override Object LoadPageStateFromPersistenceMedium() { if (viewStateCompression == Deflater.NO_COMPRESSION) return base.LoadPageStateFromPersistenceMedium(); PageStatePersister.Load(); String base64 = PageStatePersister.ViewState.ToString(); byte[] state = Decompress(Convert.FromBase64String(base64)); string serializedState = Convert.ToBase64String(state); object viewState = new LosFormatter().Deserialize(serializedState); return new Pair(PageStatePersister.ControlState, viewState); } private byte[] Decompress(byte[] bytes) { using (MemoryStream byteStream = new MemoryStream(bytes)) { using (Stream stream = new InflaterInputStream(byteStream)) { using (MemoryStream memory = new MemoryStream(BUFFER_SIZE)) { byte[] buffer = new byte[BUFFER_SIZE]; while (true) { int size = stream.Read(buffer, 0, BUFFER_SIZE); if (size <= 0) break; memory.Write(buffer, 0, size); } return memory.ToArray(); } } } }
If no compression were originally applied, we call the base method to do its thing. Otherwise, the state information is derived from the hidden HTML field, and the ViewState portion of it is converted from base-64, decompressed, reconverted to base-64, and deserialized into its original object hierarchy.
Some experimentation should be done to determine the optimal sizes of the various buffers; here we used our elite programming skills to pick a workable size (we guessed). Likewise, likely no single type of compression (default, max or fast) is optimal in all circumstances.
So how does a developer determine whether ViewState compression is required at all? One could view the page source, copy the value of the __VIEWSTATE hidden input field, paste it into an editor and determine the column width. A better approach is to display the size of the ViewState (during development) as part of a FooterInfoControl on the page itself. Our MasterPage.Master displays the footer control, which contains other controls, one of which is the ViewstateSizeControl itself:
public class ViewstateSizeControl : Label { private const string SCRIPT = "$('{0}').innerText = document.forms[0].__VIEWSTATE.value.length;"; protected override void OnLoad(EventArgs e) { if (Visible) { Page.ClientScript.RegisterStartupScript( typeof(Page), UniqueID, string.Format(SCRIPT, ClientID), true); } base.OnLoad(e); } }
This allows the developers and testers to see how big the ViewState is, both before and after compression, with no extra effort. Quite handy! And how well does this compression mechanism work? In most cases, you should expect at least a 90% reduction in ViewState size. For example, the aforementioned 6MB of data actually compressed to less than 60K. Quite effective!
ViewStateCompressionDemo
No comments:
Post a Comment