ntwo

导航

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,访问:

压缩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,并允许在流中放置一个过滤程序。过滤程序可以移除空白。

完成这个功能,需要三步:

  1. 创建过滤程序。
  2. 编写一个HTTP Module安装过滤程序。HTTP Module是一个可以插入到ASP.NET页面处理流程中的类。在HTTP Module中,可以插入处理页面事件的处理程序。
  3. 更新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中使用的短名称。

更多资源

posted on 2011-01-04 17:39  9527  阅读(1034)  评论(1编辑  收藏  举报