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>元素:
1 | <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>节加入一行:
1 | <trace enabled= "true" /> |
打开页面,点击View Details链接,查看Control Tree节。
禁用ViewState
禁用整个网站的ViewState,修改web.config的<system.web>的<pages>元素:
1 | <pages enableViewState= "false" > |
在页面指令中可以重载:
1 | <%@ Page Language= "C#" EnableViewState= "true" ... %> |
如果页面是启用ViewState的,可以禁用控件的ViewState:
1 | <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,然后逐个启用控件:
1 2 | <%@ 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代价较小时是有意义的。如果相反,应该始终重载控件,特别是使用数据库缓存时。
假设有一个下拉菜单,它的值是从数据库加载的,例如,选择国家名称:
1 2 3 | <asp:DropDownList ID= "ddlCountries" runat= "server" DataTextField= "name" DataValueField= "id" > </asp:DropDownList> |
传统的方法是在页面第一次加载时,从数据库获取:
1 2 3 4 5 6 7 8 | protected void Page_Load( object sender, EventArgs e) { if (!Page.IsPostBack) { ddlCountries.DataSource = DataAccess.Country.GetAll(); ddlCountries.DataBind(); } } |
在Page_Init中而不是Page_Load从缓存中加载数据,否则控件会丢失选中的项:
1 2 3 4 5 6 7 | 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。
1 2 3 | <asp:DropDownList ID= "ddlCountries" runat= "server" DataTextField = "name" DataValueField= "id" EnableViewState= "false" > </asp:DropDownList> |
存储属性值的短版本
有时,一个长属性值与一个短值有一对一的关系。在这种情况下,可以存在短值减少ViewState开销。
很多网站需要显示不同语言的信息。为了翻译方便,可以在一个数组中存储信息。每个信息都由一个索引标识。所以,给一个控件赋值:
1 2 | int messageId = ...; lblMessage.Text = Messages[messageId]; |
然而,因为messageId是一个整数,它要比信息本身占用的空间小。因为messageId和message有一对一的关系,在ViewState中存储messageId要比存储message节省空间。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | 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属性:
1 2 3 | <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中保存和加载。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | 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继承:
1 | 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,会在触发过滤器前进行压缩。这样过滤器就不会起作用。这种情况下,可以让压缩算法减少空白。
创建过滤器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | 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
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | 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
1 2 3 4 5 6 7 8 | <system.web> ... <httpModules> <add name= "WhiteSpaceFilter" type= "WhiteSpaceFilter.Module, WhiteSpaceFilter" /> </httpModules> ... </system.web> |
其它措施
以下措施不会产生很大的效果,尤其是使用了动态文件压缩。但是,它们也不会占用很多资源,所以可能仍然是值得的。
事件验证
如果页面上有input元素,那么页面上就会有一个隐藏字段__EVENTVALIDATION。这个字段记录了所有表单上的input元素。这样,在postback以后,页面可以检查输入的数据是否由已知的input元素生成。这使得恶意用户使用伪造的数据攻击网站更困难一些。
事件验证的一个问题是阻止你在客户端使用JavaScript生成新的input元素。另一个问题是它在页面上会占用很多空间,特别是如果页面上很长的下拉框,例如选择国家的下拉框。
可以在每个页面上禁用事件验证:
1 | <%@ Page Language= "C#" EnableEventValidation= "false" ... %> |
在整个网站禁用,编辑web.config:
1 2 3 4 5 | <configuration> <system.web> <pages enableEventValidation= "false" > </system.web> </configuration> |
不管怎么做,确保始终检查浏览器发送的数据,包括POST和cookie。
内联JavaScript和CSS
如果多个页面共享相同的JavaScript或CSS,将它们移动到外部文件。这样,这们可以被浏览器缓存。浏览页面时,如果它们已经在缓存中,就不需要加载。如果不在缓存中,它们也可以与.aspx文件的其它元素并行下载。
避免内联样式
避免使用以下HTML:
1 | <span style= "color: Red; font-weight: bold;" >Error Message</span> |
在CSS文件中创建一个类:
1 2 3 4 5 | .errormsg { color: Red; font-weight: bold; } |
然后,在HTML中使用类:
1 | <span class = "errormsg" >Error Message</span> |
减少ASP.NET IDs占用的空间
当ASP.NET展示服务器控件时,也会输出它们的ASP.NET IDs。这些IDs可能非常长。如果控件不需要post数据到服务器,实际上是不需要这些ID的。
删除IDs
如果不需要从JavaScript或后台代码引用一个控件,可以不赋给它ID,这样,在生成的HTML中就不会有ID。
如果只在后台代码中使用IDs,可以在处理完成后删除IDs:
1 2 | Label2.Text = "abc" ; Label2.ID = null ; |
如果需要删除一个有子控件的控件,例如Repeater的控件,的IDs,使用:
1 2 3 4 5 6 7 8 | private void RemoveIDs(Control c) { c.ID = null ; foreach (Control cc in c.Controls) { RemoveIDs(cc); } } |
1 | 现在,可以删除Repeater中的所有IDs: |
1 | RemoveIDs(Repeater1); |
1 | 也可以在页面的PreRender事件处理器中删除所有控件的ID: |
1 2 3 4 | protected void Page_PreRender( object sender, EventArgs e) { RemoveIDs( this ); } |
注意不能删除postback中包括的控件的ID。这个方法只适用于只读控件,不适用表单。
保持ID短小
如果不能完全删除IDs,尽量保持ID短小。但不要过分,不要因为节省空间,而失去的可维护性。
关注容器控件的IDs,因为它们会附加到它们每个子控件的ID中。
使用ASP.NET注释代替HTML注释
如果使用HTML注释,因为它们会被发送到浏览器:1 | <!--normal html comments get sent to the browser—> |
使用ASP.NET注释:
1 | <%--ASP.NET comments do not get sent to the browser—%> |
使用Literal控件代替Label控件
Label控件包含内容中<span>标签中,而Literal控件只是展示内容,没有额外的东西。
避免重复
一个CSS类重复的例子:
1 2 3 4 5 | <ul> <li class = "country" >Gabon</li> <li class = "country" >Gambia</li> <li class = "country" >Georgia</li> </ul> |
可以这样避免重复:
1 2 3 4 5 | <ul class = "countrylist" > <li>Gabon</li> <li>Gambia</li> <li>Georgia</li> </ul> |
样式表中的countrylist选择符:
1 | 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.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架