ASP.NET站点性能提升-减少最后一字节的时间
最后一字节的时间是生成.aspx并将它通过互联网传送到浏览器的时间。
查明瓶颈
压缩
.aspx文件和它使用的css、JavaScript文件都是文本文件。因为它们包含很多空白和重复关键字,它们可以使用例如GZIP的算法进行高度压缩。压缩一个HTML文件到原来大小的三分之一或更小是很平常的。
所以,IIS从5开始就支持对文本文件的压缩,根据浏览器的不同可能使用deflate或GZIP压缩算法。然而,默认IIS5和IIS6不会开启压缩,IIS7默认只会压缩静态文件,例如CSS和JavaScript,但不会压缩.aspx文件。
使用两个网站检测是否压缩:
如果想更进一步了解网站的哪些网页压缩了,压缩比率是多少,使用Firefox的Web Developer插件。
ViewState
禁用整个网站的ViewState,修改web.config文件中的<system.web>的<pages>元素:
<pages enableViewState="false">
确保ViewState在页面上没有被打开。
在状态栏显示ViewState大小
使用FireFox的Viewstate Size插件查看ViewState大小:
https://addons.mozilla.org/en-US/firefox/addon/5956/?src=api
优化表单
在传统的ASP.NET网站,表单提交要比实际需要的更昂贵,因为ViewState,并且每次提交都需要页面的完全刷新。
空白
如果使用了动态压缩,删除空白就不重要了。
修复瓶颈
ViewState
查看每一个控件生成的ViewState
在web.config文件中的<system.web>节加入一行:
<trace enabled="true" />
打开页面,点击View Details链接,查看Control Tree节。
禁用ViewState
禁用整个网站的ViewState,修改web.config的<system.web>的<pages>元素:
<pages enableViewState="false">
在页面指令中可以重载:
<%@ Page Language="C#" EnableViewState="true" ... %>
如果页面是启用ViewState的,可以禁用控件的ViewState:
<asp:Literal ID="Literal1" runat="server" EnableViewState="false" />
直到ASP.NET 3.5,不能禁用页面的ViewState,然后开启控件的ViewState。这意味着如果需要启用页面上的一个控件的ViewState,必须启用整个页面的ViewState,然后禁用所有不需要启用ViewState的控件的ViewState。
ASP.NET 4引入了一个新属性可以应用于页面和控件—ViewStateMode解决这个问题。它可以取值Enabled、Disabled和Inherit。Inherit是控件的默认值,使得它们继承页面的ViewStateMode。页面的默认值是Enabled。所以,在ASP.NET 4中,可以禁用整个页面的ViewState,然后逐个启用控件:
<%@ Page Language="C#" ViewStateMode="Disabled" ... %> <asp:Literal ID="Literal1" runat="server" ViewStateMode="Enabled" />
识别不需要ViewState的控件
可以禁用以下控件的ViewState:
- 永远不会postback的控件,例如只有文本和图片的页面。如果一个页面不postback,它就不会回传ViewState。
- 没有设置不会postback属性的控件。如果只设置了TextBox的Text控件,可以禁用TextBox的ViewState,因为Text属性会回传。
- 在每次页面加载时设置属性的控件。如果Literal控件的Text属性没有保存在ViewState中,就会丢失。如果在每次请求过程中,都会赋给一个新值,就不需要在ViewState中保存旧值。
- 不需要保存属性的控件。例如,如果使用一个Literal显示临时值,这个值在下次postback时需要移除,就没有必要在ViewState中保存这个值。
从数据缓存中重载
开发者经常启用控件的ViewState,这样就不需要从数据库再次载入。这只在数据库负载较大,而ViewState代价较小时是有意义的。如果相反,应该始终重载控件,特别是使用数据库缓存时。
假设有一个下拉菜单,它的值是从数据库加载的,例如,选择国家名称:
<asp:DropDownList ID="ddlCountries" runat="server" DataTextField="name" DataValueField="id"> </asp:DropDownList>
传统的方法是在页面第一次加载时,从数据库获取:
protected void Page_Load(object sender, EventArgs e) { if (!Page.IsPostBack) { ddlCountries.DataSource = DataAccess.Country.GetAll(); ddlCountries.DataBind(); } }
在Page_Init中而不是Page_Load从缓存中加载数据,否则控件会丢失选中的项:
protected void Page_Init(object sender, EventArgs e) { // GetAll caches the country list, // instead of accessing the database on each call. ddlCountries.DataSource = DataAccess.Country.GetAll(); ddlCountries.DataBind(); }
如果不使用数据访问层,使用数据源控件,例如SqlDataSource,设置EnableCaching属性为true启用缓存。
现在,可以禁用drop-down控件的ViewState。
<asp:DropDownList ID="ddlCountries" runat="server" DataTextField ="name" DataValueField="id" EnableViewState="false"> </asp:DropDownList>
存储属性值的短版本
有时,一个长属性值与一个短值有一对一的关系。在这种情况下,可以存在短值减少ViewState开销。
很多网站需要显示不同语言的信息。为了翻译方便,可以在一个数组中存储信息。每个信息都由一个索引标识。所以,给一个控件赋值:
int messageId = ...; lblMessage.Text = Messages[messageId];
然而,因为messageId是一个整数,它要比信息本身占用的空间小。因为messageId和message有一对一的关系,在ViewState中存储messageId要比存储message节省空间。
int messageId = ...; ViewState["messageId"] = messageId; lblMessage.Text = Messages[messageId]; protected void Page_Load(object sender, EventArgs e) { if (Page.IsPostBack) { // Retrieve messageId from ViewState int messageId = 0; Int32.TryParse(ViewState["messageId"].ToString(), out messageId); // Restore Text property lblMessage.Text = Messages[messageId]; } }
最后,假设lblMessage的Text属性是唯一使用的属性,可以禁用lblMessage的ViewState属性:
<asp:Label ID="lblMessage" EnableViewState="false" runat="server"> </asp:Label>
注意:不要在ViewState中存储枚举。这要比整数花费更多的空间,因为ASP.NET存储额外信息,包括类型名称和程序集名。
在服务器上存储ViewState
可以在服务器上存储ViewState。有这些方法和它们的问题:
- 在缓存或session对象中存储:占用很多内存。如果缓存管理器回收缓存,或session过期,或应用程序池或web服务器重启,ViewState会丢失。
- 在session中存储:在缓存中存储不能在服务器园时使用,因为下一次postback可能到达不同的服务器。如果使用在数据库或state服务器上存储session,可以在session存储ViewState。
- 在Application State或Application静态变量中存储:与使用缓存或session有相同的问题,除了它们不会过期。但当应用程序池或web服务器重启,也会丢失。并且,需要实现代码删除ViewState。
- 在数据库存储:ViewState不会突然消失,也可以在服务器园中使用。然而,这会产生很多数据库更新或插入,需要实现代码删除ViewState。
ViewState在Page类的两个方法SavePageStateToPersistenceMedium和LoadPageStateFromPersistenceMedium中保存和加载。
public class ViewStateInCachePage : System.Web.UI.Page { private const string viewStateKeyId = "__vsk"; protected override void SavePageStateToPersistenceMedium(object viewState) { string viewStateKey = Guid.NewGuid().ToString(); Cache.Add(viewStateKey, viewState, null, // store viewstate in cache DateTime.Now.AddMinutes(Session.Timeout), TimeSpan.Zero, CacheItemPriority.Default, null); ClientScript.RegisterHiddenField(viewStateKeyId, viewStateKey); } protected override object LoadPageStateFromPersistenceMedium() { string viewStateKey = Request.Form[viewStateKeyId]; if (viewStateKey == null) throw new InvalidOperationException("Invalid viewstate key:" + viewStateKey); object viewState = Cache[viewStateKey]; if (viewState == null) throw new InvalidOperationException("ViewState not in cache"); return viewState; } }
有一些方法可以改善这段代码。可能加密ViewState key防篡改。如果内存不足,但有足够的CPU资源,可以在缓存前使用GZipStream压缩。如果使用服务器园,在session或数据库中存储。
除了使用页面基类,还可以使用页面适配器。这可以更改所有页面的功能,而不用修改它们的基类。如果希望所有页面的ViewState都保存在服务器,这是一个好方法,但是如果只希望存储那些ViewState很大的页面,这就不是一个好方法了。如何使用PageAdapter类的GetStatePersister方法存储所有页面的ViewState,访问:
- PageStatePersister Class http://msdn.microsoft.com/en-us/library/system.web.ui.pagestatepersister.aspx.
- PageAdapter Class http://msdn.microsoft.com/en-us/library/system.web.ui.adapters.pageadapter.aspx.
压缩ViewState
除了在服务器上存储ViewState,你可以在页面上使用压缩的形式存储它们。
因为这个方法在页面上存储ViewState,所以不会导致内存增长或数据库访问。但是,如果使用了服务器压缩,压缩ViewState不会减少.aspx文件大小。另一方面,因为ViewState中存储在__VIEWSTATE隐藏字段中,它会包含在每一次浏览器对服务器的请求中,而请求不会压缩,所以这里也不能减小大小。
记住压缩是非常CPU密集的任务。并且只能在ViewState很大时使用。否则,压缩算法可能实际上增加ViewState的大小。
实现压缩
实现压缩需要考虑三件事:
- 切入ASP.NET framework:需要从Page派生一个类,重载LoadPageStateFromPersistenceMedium和SavePageStateToPersistenceMedium方法。
- 序列化/反序列化需要存储的对象:使用LoadPageStateFromPersistenceMedium和SavePageStateToPersistenceMedium默认使用的LosFormatter对象(System.Web.UI命名空间)。
- 压缩:使用System.IO.Compression命名空间中的GZipStream对象。
public class CompressedViewStatePage : System.Web.UI.Page { private const string viewStateFormId = "__vsk"; protected override void SavePageStateToPersistenceMedium(object viewState) { LosFormatter losFormatter = new LosFormatter(); StringWriter stringWriter = new StringWriter(); losFormatter.Serialize(stringWriter, viewState); string uncompressedViewStateString = stringWriter.ToString(); string compressedViewStateString = CompressBase64(uncompressedViewStateString); ClientScript.RegisterHiddenField(viewStateFormId, compressedViewStateString); } private static string CompressBase64(string uncompressedBase64) { MemoryStream compressedStream = new MemoryStream(); GZipStream gzipStream = new GZipStream(compressedStream, CompressionMode.Compress, true); byte[] uncompressedData = Convert.FromBase64String(uncompressedBase64); gzipStream.Write(uncompressedData, 0, uncompressedData.Length); gzipStream.Close(); byte[] compressedData = compressedStream.ToArray(); string compressedBase64 = Convert.ToBase64String(compressedData); return compressedBase64; } protected override object LoadPageStateFromPersistenceMedium() { string compressedViewState = Request.Form[viewStateFormId]; string decompressedViewState = DecompressBase64(compressedViewState); LosFormatter losFormatter = new LosFormatter(); return losFormatter.Deserialize(decompressedViewState); } private static string DecompressBase64(string compressedBase64) { byte[] compressedData = Convert.FromBase64String(compressedBase64); MemoryStream compressedStream = new MemoryStream(); compressedStream.Write(compressedData, 0, compressedData.Length); compressedStream.Position = 0; GZipStream gzipStream = new GZipStream (compressedStream,CompressionMode.Decompress, true); MemoryStream uncompressedStream = new MemoryStream(); CopyStream(gzipStream, uncompressedStream); gzipStream.Close(); return Convert.ToBase64String(uncompressedStream.ToArray()); } }
在页面上使用ViewState压缩
从CompressedViewStatePage继承页面,不要从Page继承:
public partial class ViewStateCompressed : CompressedViewStatePage
上面的代码有一些可以改善的地方。可以检查压缩后的ViewState是否小于原来的,如果不是,使用原来的。如果这样做,就需要在ViewState中存储一个标识位指出是否进行了压缩。另一个改进是检查Accept-Encoding请求头,只在服务器没有压缩输出的.aspx文件的情况下进行压缩。
减少空白
如果查看web页面的源代码,就会发现经常会有大量的空白。如果服务器使用了动态文件压缩,不需要担心这个问题,因为压缩算法会大量减少空白。否则,需要自己清除空白。
一个方法在页面定义的HTML中移除空白。但是这使得代码很难读。
另一个解决方案是利用ASP.NET会生成页面的HTML,然后将它们发送到浏览器,它会通过输出流发送HTML,并允许在流中放置一个过滤程序。过滤程序可以移除空白。
完成这个功能,需要三步:
- 创建过滤程序。
- 编写一个HTTP Module安装过滤程序。HTTP Module是一个可以插入到ASP.NET页面处理流程中的类。在HTTP Module中,可以插入处理页面事件的处理程序。
- 更新web.config,插入HTTP Module到页面处理流程中。
这种方法的一个好处是不需要修改已有代码。所要做的就是在项目中添加过滤器和HTTP Module,然后更新web.config。
这种方法的一个缺点就是如果使用了动态文件压缩,并且开启了dynamicCompressionBeforeCache,会在触发过滤器前进行压缩。这样过滤器就不会起作用。这种情况下,可以让压缩算法减少空白。
创建过滤器
using System; using System.IO; namespace WhiteSpaceFilter { class WhiteSpaceFilterStream : Stream { private Stream outputStream = null; public WhiteSpaceFilterStream(Stream outputStream) { this.outputStream = outputStream; } private bool inWhiteSpaceRun = false; private bool whiteSpaceRunContainsLineBreak = false; public override void Write(byte[] buffer, int offset, int count) { int dataLimit = offset + count; for (int i = offset; i < dataLimit; i++) { char c = (char)buffer[i]; if (Char.IsWhiteSpace(c)) { inWhiteSpaceRun = true; if ((c == '\r') || (c == '\n')) { whiteSpaceRunContainsLineBreak = true; } } else { if (inWhiteSpaceRun) { if (whiteSpaceRunContainsLineBreak) { outputStream.WriteByte((byte)'\r'); outputStream.WriteByte((byte)'\n'); } else { outputStream.WriteByte((byte)' '); } } outputStream.WriteByte((byte)c); inWhiteSpaceRun = false; whiteSpaceRunContainsLineBreak = false; } } } // boilerplate implementation of other Stream methods and properties. } }
创建HTTP Module
using System; using System.Web; namespace WhiteSpaceFilter { public class Module: IHttpModule { private void OnPostRequestHandlerExecute(Object sender, EventArgs e) { HttpApplication httpApplication = (HttpApplication)sender; HttpResponse httpResponse = httpApplication.Context.Response; if (httpResponse.ContentType == "text/html") { httpResponse.Filter = new WhiteSpaceFilterStream(httpResponse.Filter); } } public void Init(HttpApplication httpApplication) { httpApplication.PostRequestHandlerExecute +=new EventHandler(OnPostRequestHandlerExecute); } public void Dispose() {} } }
在web.config中加入HTTP Module
<system.web> ... <httpModules> <add name="WhiteSpaceFilter" type="WhiteSpaceFilter.Module, WhiteSpaceFilter" /> </httpModules> ... </system.web>
其它措施
以下措施不会产生很大的效果,尤其是使用了动态文件压缩。但是,它们也不会占用很多资源,所以可能仍然是值得的。
事件验证
如果页面上有input元素,那么页面上就会有一个隐藏字段__EVENTVALIDATION。这个字段记录了所有表单上的input元素。这样,在postback以后,页面可以检查输入的数据是否由已知的input元素生成。这使得恶意用户使用伪造的数据攻击网站更困难一些。
事件验证的一个问题是阻止你在客户端使用JavaScript生成新的input元素。另一个问题是它在页面上会占用很多空间,特别是如果页面上很长的下拉框,例如选择国家的下拉框。
可以在每个页面上禁用事件验证:
<%@ Page Language="C#" EnableEventValidation="false" ... %>
在整个网站禁用,编辑web.config:
<configuration> <system.web> <pages enableEventValidation="false"> </system.web> </configuration>
不管怎么做,确保始终检查浏览器发送的数据,包括POST和cookie。
内联JavaScript和CSS
如果多个页面共享相同的JavaScript或CSS,将它们移动到外部文件。这样,这们可以被浏览器缓存。浏览页面时,如果它们已经在缓存中,就不需要加载。如果不在缓存中,它们也可以与.aspx文件的其它元素并行下载。
避免内联样式
避免使用以下HTML:
<span style="color: Red; font-weight: bold;">Error Message</span>
在CSS文件中创建一个类:
.errormsg { color: Red; font-weight: bold; }
然后,在HTML中使用类:
<span class="errormsg">Error Message</span>
减少ASP.NET IDs占用的空间
当ASP.NET展示服务器控件时,也会输出它们的ASP.NET IDs。这些IDs可能非常长。如果控件不需要post数据到服务器,实际上是不需要这些ID的。
删除IDs
如果不需要从JavaScript或后台代码引用一个控件,可以不赋给它ID,这样,在生成的HTML中就不会有ID。
如果只在后台代码中使用IDs,可以在处理完成后删除IDs:
Label2.Text = "abc"; Label2.ID = null;
如果需要删除一个有子控件的控件,例如Repeater的控件,的IDs,使用:
private void RemoveIDs(Control c) { c.ID = null; foreach (Control cc in c.Controls) { RemoveIDs(cc); } }
现在,可以删除Repeater中的所有IDs:
RemoveIDs(Repeater1);
也可以在页面的PreRender事件处理器中删除所有控件的ID:
protected void Page_PreRender(object sender, EventArgs e) { RemoveIDs(this); }
注意不能删除postback中包括的控件的ID。这个方法只适用于只读控件,不适用表单。
保持ID短小
如果不能完全删除IDs,尽量保持ID短小。但不要过分,不要因为节省空间,而失去的可维护性。
关注容器控件的IDs,因为它们会附加到它们每个子控件的ID中。
使用ASP.NET注释代替HTML注释
如果使用HTML注释,因为它们会被发送到浏览器:<!--normal html comments get sent to the browser—>
使用ASP.NET注释:
<%--ASP.NET comments do not get sent to the browser—%>
使用Literal控件代替Label控件
Label控件包含内容中<span>标签中,而Literal控件只是展示内容,没有额外的东西。
避免重复
一个CSS类重复的例子:
<ul> <li class="country">Gabon</li> <li class="country">Gambia</li> <li class="country">Georgia</li> </ul>
可以这样避免重复:
<ul class="countrylist"> <li>Gabon</li> <li>Gambia</li> <li>Georgia</li> </ul>
样式表中的countrylist选择符:
ul.countrylist li { ... }
使用更短的URLs
长URL不仅占用页面的空间,还会占用Http请求的空间。
可以使用相对地址缩短它们。不仅可以省略域名,还可以省略文件夹甚至协议。
如果当前页面是http://www.myserver.com/folder/mypage.aspx,下表是一些可以使用的相对地址:
全路径 | 相对路径 | 说明 |
http://otherserver.com | //otherserver.com | 相同的协议 |
http://www.myserver.com/default.aspx |
/default.aspx | 相同服务器,不同文件夹 |
http://www.myserver.com/folder/mypage2.aspx |
mypage2.aspx | 相同服务器,相同文件夹 |
http://www.myserver.com/folder/mypage.aspx?id=5 |
?id=5 | 相同页面,不同查询字符串 |
虽然相对地址更短,但绝对地址也有一些好处:
- SEO:如果域名是富于关键字的,通过使用绝对路径的方法在HTML中重复域名可能会影响搜索引擎页面评分。并且这样也使搜索引擎更容易跟踪链接。
- 防止被盗用:如果网站内容包括有用的信息,其它人可以拷贝HTML到他们自己的网站上。但是,很多人不会是更新他们盗用的内容。所以如果使用绝对链接,它们还是会指向原来的网站。
一般情况下,尽可能保持URL短小。避免长文件夹名和深文件夹层次结构。
如果必须在服务器上使用长文件夹名或层次结构,可以在服务器上使用虚拟目录缩短URL。在IIS管理器中,右击网站,添加一个虚拟目录。在别名字段中,指定在URL中使用的短名称。
更多资源
- W3compiler: http://www.w3compiler.com/.
- Gzip algorithm: http://www.gzip.org/algorithm.txt.
- Using Visual Studio 2008 with IIS 7: http://learn.iis.net/page.aspx/387/using-visual-studio-2008-with-iis-7/.
- Using Visual Studio 2005 with IIS 7.0: http://learn.iis.net/page.aspx/431/using-visual-studio-2005-with-iis-70/.
- Understanding ASP.NET View State: http://msdn.microsoft.com/en-us/library/ms972976.aspx.
- ViewState Provider - an implementation using Provider Model Design Pattern: http://www.codeproject.com/KB/viewstate/ViewStateProvider.aspx.