天天看點

ASP.NET頁面性能提升8倍的優化方法

今天與大家分享:一種優化頁面執行速度的方法。采用這個方法,可以使用頁面的執行速度獲得【8倍】的提升效果。為了讓您對優化的效果有個直覺的了解,我準備了下面的測試結果截圖:

  測試環境:

  1. Windows Server 2003 SP2

  2. Viaual Studio 2008,使用自帶的WebDev.WebServer.EXE運作網站程式。

  3. (ThinkPad SL510):Core2 T6670 2.2GHz, 4G記憶體

  二個紅框中的數字反映了優化前後的執行時間。

  數字表明:優化前後,執行時間有了8倍多的差别。

  本文的測試結果也僅僅隻是一個參考數字,這個結果也隻是根據我所設計的測試頁面得出的。優化的過程中,如果不使用伺服器控件,那麼給GC減少的壓力其實也是無法測試到的.在測試過程中,我還發現測試結果并不是很穩定,是以截圖具有一定的偶然性。

  測試頁面或許在某些方面存在一些片面性,是以,結果僅供參考。看過了優化結果,再來介紹一下:這個測試到底是在測試什麼東西?

  現在有很多做ASP.NET的開發人員,應該都是從ASP.NET的WebForm程式設計模型開始學習的。 大家都很喜歡用伺服器控件,不管輸出什麼,都會使用伺服器控件。 有時候為了讓頁面呈現幹淨的HTML代碼,有些人會選擇使用Repeater,Literal這類簡單的伺服器控件。 或許有些人認為:我已不使用GridView這樣強大複雜的控件,頁面執行速度已經很快了。

  真是這樣嗎?今天測試的起點就從使用簡單的伺服器開始,我會分二次對它做一系列的性能優化。

  最終就是上圖中的3個結果,它們反映了二次優化的改進過程。在繼續介紹之前,有一點我想有必要說明一下:

  優化的過程涉及到ASP.NET伺服器控件的使用,測試結果也僅僅隻是一個參考數字。

  如果您認為您的開發工作非常依賴于伺服器控件的使用,

  那麼測試結果對您來說其實是無意義的,請不要在意這個結果。

  測試方法

  在這次優化過程中,我并沒有設計很複雜的測試頁面,而是一個很簡單的測試頁面,頁面顯示效果如下:

  這個頁面其實就是顯示了一堆超連結,它們來自于我的部落格側邊欄的【推薦排行榜】,總共有20條記錄, 我讓頁面重複5次輸出,也就是生成了100個超連結。

  測試的資料是這樣擷取的:

  我複制了我的部落格側邊欄的【推薦排行榜】的那段HTML代碼,儲存到一個檔案中:

  然後,網站在初始化時,從這段HTML代碼提取連結位址以及顯示文字,儲存到一個BlogInfo的清單中,代碼如下:

public class BlogInfo

  {

  public string Title;

  public string Href;

  }

  public static class XmlDb

  public static List Blogs { get; private set; }

  public static void LoadBlogs()

  string filePath = Path.Combine(HttpRuntime.AppDomainAppPath, @"App_Data\RecommendList.html");

  XElement html = XElement.Parse(System.IO.File.ReadAllText(filePath));

  Blogs =

  (from a in html.Elements("li").Elements("a")

  select new BlogInfo { Title = a.Value, Href = a.Attribute("href").Value }).ToList();

  測試時,就把XmlDb.Blogs的内容顯示在網頁中。

  我想這個測試還是比較接近于現實開發的。

  這裡又有一個問題:我如何測試頁面的執行速度?

  雖然說建立HttpWebRequest通路頁面是個很簡單的方法,但我并不打算這樣做。

  因為從HttpWebRequest發起調用到擷取結果,這其中除了有頁面的執行時間,還混雜較多的額外調用開銷。 最終,我選擇了在一次HTTP請求中,循環調用Server.Execute來執行頁面,并統計時間的方式。 其實如何選擇測試方法,對于二個測試對象還說,都是公平的。 隻是說:盡量減少一些額外的調用開銷,會讓測試結果的差異更大,也更明顯。

  說明:為了測試代碼寫起來簡單,我使用了MyMVC架構。

  測試用例1:WebFromPage.aspx

  前面介紹了測試背景以及測試方法。現在就來介紹第1個測試用例,它采用了WebForm程式設計模型中最經典的寫法。

  頁面代碼:

<%@ Page Language="C#" CodeFile="WebFromPage.aspx.cs" Inherits="TestPage_WebFromPage" %>

<head>

</head>

<body>

<p>This is WebFromPage.aspx</p>

<asp:Repeater ID="repeater1" runat="server" onitemdatabound="repeater1_ItemDataBound">

<ItemTemplate>

    <asp:HyperLink ID="link1" runat="server"></asp:HyperLink><br />

</ItemTemplate>

<FooterTemplate><hr /></FooterTemplate>

</asp:Repeater>

<asp:Repeater ID="repeater2" runat="server" onitemdatabound="repeater1_ItemDataBound">

<asp:Repeater ID="repeater3" runat="server" onitemdatabound="repeater1_ItemDataBound">

<asp:Repeater ID="repeater4" runat="server" onitemdatabound="repeater1_ItemDataBound">

<asp:Repeater ID="repeater5" runat="server" onitemdatabound="repeater1_ItemDataBound">

</body>

</html>

  頁面的CodeFile代碼:

public partial class TestPage_WebFromPage : System.Web.UI.Page

{

    protected override void OnLoad(EventArgs e)

    {

        base.OnLoad(e);

        repeater1.DataSource = XmlDb.Blogs;

        repeater1.DataBind();

        repeater2.DataSource = XmlDb.Blogs;

        repeater2.DataBind();

        repeater3.DataSource = XmlDb.Blogs;

        repeater3.DataBind();

        repeater4.DataSource = XmlDb.Blogs;

        repeater4.DataBind();

        repeater5.DataSource = XmlDb.Blogs;

        repeater5.DataBind();

    }

    protected void repeater1_ItemDataBound(object sender, RepeaterItemEventArgs e)

        if( e.Item.ItemType == ListItemType.Item ) {

            BlogInfo blog = e.Item.DataItem as BlogInfo;

            HyperLink link1 = e.Item.FindControl("link1") as HyperLink;

            link1.NavigateUrl = blog.Href;

            link1.Text = blog.Title;

        }

}

  測試代碼:

[Action]

public object Test1(string callTimes)

    int count = 0;

    int.TryParse(callTimes, out count);

    if( count <= 0 )

        return count;

    HttpContext context = HttpContext.Current;

    // 先執行一次,排除編譯時間

    string html = MyMVC.PageExecutor.Render(context, "/TestPage/WebFromPage.aspx", null);

    Stopwatch watch = Stopwatch.StartNew();

    for( int i = 0; i < count; i++ )

        html = MyMVC.PageExecutor.Render(context, "/TestPage/WebFromPage.aspx", null);

    watch.Stop();

    return watch.Elapsed.ToString();

  當我測試執行10000次時,耗時:00:00:07.5607229

  測試用例2:InlinePage.aspx

  與測試用例1不同,測試用例2則完全不使用伺服器控件。

<%@ Page Language="C#" %>

<p>This is InlinePage.aspx</p>

<% foreach( BlogInfo b in XmlDb.Blogs ) { %>

    <a href="<%= b.Href %>" target="_blank"><%= b.Title %></a><br />

<% } %>

<hr />

public object Test2(string callTimes)

    string html = MyMVC.PageExecutor.Render(context, "/TestPage/InlinePage.aspx", null);

        html = MyMVC.PageExecutor.Render(context, "/TestPage/InlinePage.aspx", null);

  當我測試執行10000次時,耗時:00:00:01.2345842

  分析優化結果1

  測試用例1執行相同次數所花費的時間是測試用例2的6倍,為什麼會這樣呢?

  為了回答這個問題,我們首先要知道前面二個頁面在執行時,它們是如何運作的。

  說到這裡,就不得不談ASP.NET的頁面編譯方式了。ASP.NET的頁面編譯過程是個複雜的操作,其實我們可以不用關心頁面是如何編譯的,

  但要知道:頁面編譯後是什麼樣的。

  為了能直覺地了解頁面編譯後的樣子,我編譯了整個網站,并生成到一個DLL檔案中, 然後使用Reflector.exe來分析這個DLL的源代碼。

  将網站編譯成一個DLL檔案有二個方法:

  1. 安裝WebDeployment插件。

  2. 使用我的工具:FishAspnetTool

  本文将使用FishAspnetTool來編譯測試網站獲得編譯後的DLL檔案。

  FishAspnetTool是什麼?

  FishAspnetTool是我在使用Visual Web Developer 2005時,為了友善編譯網站而寫的一個小工具。

  注意:下載下傳的是一個工具包,安裝後,從開始菜單中運作FishTools\FishAspnetTool即可。

  下面是工具的運作截圖。

  操作方法:

  1. 點選粉色按鈕,選擇網站路徑。

  2. 單選按鈕選擇第2項。

  3. 點選【釋出網站】按鈕。

  在編譯網站之後,我就可以知道網站在運作時如何運作頁面了。

  測試用例1的頁面,最後被編譯成這樣了:

namespace ASP

    using System;

    using System.Diagnostics;

    using System.Runtime.CompilerServices;

    using System.Web;

    using System.Web.UI;

    using System.Web.UI.WebControls;

    [CompilerGlobalScope]

    public class testpage_webfrompage_aspx : TestPage_WebFromPage, IHttpHandler

        private static object __fileDependencies;

        private static bool __initialized;

        [DebuggerNonUserCode]

        public testpage_webfrompage_aspx()

        {

            base.AppRelativeVirtualPath = "~/TestPage/WebFromPage.aspx";

            if (!__initialized)

            {

                string[] virtualFileDependencies = new string[] { "~/TestPage/WebFromPage.aspx", "~/TestPage/WebFromPage.aspx.cs" };

                __fileDependencies = base.GetWrappedFileDependencies(virtualFileDependencies);

                __initialized = true;

            }

            base.Server.ScriptTimeout = 0x1c9c380;

        private void __BuildControl__control10(Control __ctrl)

            IParserAccessor accessor = __ctrl;

            accessor.AddParsedSubObject(new LiteralControl("<hr />"));

        private void __BuildControl__control11(Control __ctrl)

            accessor.AddParsedSubObject(new LiteralControl("\r\n\t"));

            HyperLink link = this.__BuildControl__control12();

            accessor.AddParsedSubObject(link);

            accessor.AddParsedSubObject(new LiteralControl("<br />\r\n"));

        private HyperLink __BuildControl__control12()

            HyperLink link = new HyperLink {

                TemplateControl = this

            };

            link.ApplyStyleSheetSkin(this);

            link.ID = "link1";

            return link;

        private void __BuildControl__control13(Control __ctrl)

        private void __BuildControl__control14(Control __ctrl)

            HyperLink link = this.__BuildControl__control15();

        private HyperLink __BuildControl__control15()

        private void __BuildControl__control16(Control __ctrl)

        private void __BuildControl__control2(Control __ctrl)

            HyperLink link = this.__BuildControl__control3();

        private HyperLink __BuildControl__control3()

        private void __BuildControl__control4(Control __ctrl)

        private void __BuildControl__control5(Control __ctrl)

            HyperLink link = this.__BuildControl__control6();

        private HyperLink __BuildControl__control6()

        private void __BuildControl__control7(Control __ctrl)

        private void __BuildControl__control8(Control __ctrl)

            HyperLink link = this.__BuildControl__control9();

        private HyperLink __BuildControl__control9()

        private Repeater __BuildControlrepeater1()

            Repeater repeater = new Repeater();

            base.repeater1 = repeater;

            repeater.ItemTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control2));

            repeater.FooterTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control4));

            repeater.ID = "repeater1";

            repeater.ItemDataBound += new RepeaterItemEventHandler(this.repeater1_ItemDataBound);

            return repeater;

        private Repeater __BuildControlrepeater2()

            base.repeater2 = repeater;

            repeater.ItemTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control5));

            repeater.FooterTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control7));

            repeater.ID = "repeater2";

        private Repeater __BuildControlrepeater3()

            base.repeater3 = repeater;

            repeater.ItemTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control8));

            repeater.FooterTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control10));

            repeater.ID = "repeater3";

        private Repeater __BuildControlrepeater4()

            base.repeater4 = repeater;

            repeater.ItemTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control11));

            repeater.FooterTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control13));

            repeater.ID = "repeater4";

        private Repeater __BuildControlrepeater5()

            base.repeater5 = repeater;

            repeater.ItemTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control14));

            repeater.FooterTemplate = new CompiledTemplateBuilder(new BuildTemplateMethod(this.__BuildControl__control16));

            repeater.ID = "repeater5";

        private void __BuildControlTree(testpage_webfrompage_aspx __ctrl)

            __ctrl.EnableViewState = false;

            __ctrl.EnableViewStateMac = false;

            this.InitializeCulture();

            Repeater repeater = this.__BuildControlrepeater1();

            accessor.AddParsedSubObject(repeater);

            accessor.AddParsedSubObject(new LiteralControl("\r\n\r\n"));

            Repeater repeater2 = this.__BuildControlrepeater2();

            accessor.AddParsedSubObject(repeater2);

            Repeater repeater3 = this.__BuildControlrepeater3();

            accessor.AddParsedSubObject(repeater3);

            Repeater repeater4 = this.__BuildControlrepeater4();

            accessor.AddParsedSubObject(repeater4);

            Repeater repeater5 = this.__BuildControlrepeater5();

            accessor.AddParsedSubObject(repeater5);

            accessor.AddParsedSubObject(new LiteralControl("\r\n\r\n\r\n</body>\r\n</html>\r\n"));

        protected override void FrameworkInitialize()

            base.FrameworkInitialize();

            this.__BuildControlTree(this);

            base.AddWrappedFileDependencies(__fileDependencies);

            base.Request.ValidateInput();

        public override int GetTypeHashCode()

            return -781896338;

        public override void ProcessRequest(HttpContext context)

            base.ProcessRequest(context);

        protected override bool SupportAutoEvents

            get

                return false;

  從這個編譯結果我們可以看出:頁面上的所有文字最後也被包裝到LiteralControl中。

  頁面中呈現時,就是循環調用每個控件的Render方法來最終生成HTML結果。

  測試用例2的頁面被編譯成這個樣了:

    using System.Web.Profile;

    public class testpage_inlinepage_aspx : Page, IHttpHandler

        public testpage_inlinepage_aspx()

            base.AppRelativeVirtualPath = "~/TestPage/InlinePage.aspx";

                string[] virtualFileDependencies = new string[] { "~/TestPage/InlinePage.aspx" };

        private void __BuildControlTree(testpage_inlinepage_aspx __ctrl)

            __ctrl.SetRenderMethodDelegate(new RenderMethod(this.__Render__control1));

        private void __Render__control1(HtmlTextWriter __w, Control parameterContainer)

            foreach (BlogInfo info in XmlDb.Blogs)

                __w.Write("\r\n\t<a href=\"");

                __w.Write(info.Href);

                __w.Write("\" target=\"_blank\">");

                __w.Write(info.Title);

                __w.Write("</a><br />\r\n");

            __w.Write("\r\n<hr />\r\n\r\n");

            foreach (BlogInfo info2 in XmlDb.Blogs)

                __w.Write(info2.Href);

                __w.Write(info2.Title);

            foreach (BlogInfo info3 in XmlDb.Blogs)

                __w.Write(info3.Href);

                __w.Write(info3.Title);

            foreach (BlogInfo info4 in XmlDb.Blogs)

                __w.Write(info4.Href);

                __w.Write(info4.Title);

            foreach (BlogInfo info5 in XmlDb.Blogs)

                __w.Write(info5.Href);

                __w.Write(info5.Title);

            __w.Write("\r\n<hr />\r\n\r\n</body>\r\n</html>\r\n");

            return -1307842476;

        protected global_asax ApplicationInstance

                return (global_asax) this.Context.ApplicationInstance;

        protected DefaultProfile Profile

                return (DefaultProfile) this.Context.Profile;

  請注意下面這段關鍵的代碼:它們實在太重要了。

private void __BuildControlTree(testpage_inlinepage_aspx __ctrl)

  // .......

  __ctrl.SetRenderMethodDelegate(new RenderMethod(this.__Render__control1));

  private void __Render__control1(HtmlTextWriter __w, Control parameterContainer)

  {}

testpage_inlinepage_aspx與testpage_webfrompage_aspx的編譯結果完全不同。

  最大的差别在testpage_inlinepage_aspx有個方法:__Render__control1

  在這個方法中,頁面的内容将直接被寫入到HtmlTextWriter對象中。

  還有一點我要告訴您:每個Control的輸出最後還是要将自己的顯示代碼寫入到HtmlTextWriter對象中。

  是以,從這裡就可以明顯地看出testpage_inlinepage_aspx的執行速度要快很多,

  因為:

  1. 它沒有伺服器控件。

  2. 不再需要遞歸循環每個控件,每個控件的生命周期的調用開銷也節省了。

  3. 不用再建立那些伺服器控件對象,GC的壓力會小很多。

  4. 輸出方式更高效。

  通過前面的分析,您現在明白了為什麼二個頁面的執行速度相差6倍了原因了吧。

  好像還有一點沒有解釋:__Render__control1如何被調用?

  我們都知道:以ASPX頁面為代表的WebForm程式設計模型在執行時有一個特點:遞歸循環每個控件。

  頁面是在Render階段輸出的,頁面的HTML代碼也是在那個階段輸出到HtmlTextWriter對象中的。

  可是,testpage_inlinepage_aspx沒有任何控件,那又該如何遞歸呢?

  的确,很多書籍以及技術資料都是說:在Render階段會遞歸循環每個控件并調用控件的Render方法。

  其實這種說法是不準确的。Control的Render方法在運作時,會調用下面這個方法:

internal void RenderChildrenInternal(HtmlTextWriter writer, ICollection children)

if ((this.RareFields != null) && (this.RareFields.RenderMethod != null))

writer.BeginRender();

this.RareFields.RenderMethod(writer, this);

writer.EndRender();

else if (children != null)

foreach (Control control in children)

control.RenderControl(writer);

  這段代碼中,有個重要的if...else...判斷,簡單說來,就是說要不要調用前面所說的__Render__control1方法。

  從代碼可以看出,如果是進入了if語句塊,則不用遞歸循環每個控件并調用控件的Render方法。

  那麼如何能進入if語句塊呢?

  答案是:調用Control.SetRenderMethodDelegate方法。

  testpage_inlinepage_aspx的編譯生成代碼中就有這個調用。

  對于這個方法,MSDN的解釋很含糊:

  此 API 支援 .NET Framework 基礎結構,不适合在代碼中直接使用。

  配置設定事件處理程式委托,以将伺服器控件及其内容呈現到父控件中。

  測試用例3:InlineUserControl.ascx

  在測試用例3中,我将頁面中用于輸出的代碼移到一個使用者控件中。

  使用者控件的代碼此處省略,與測試用例2的代碼基本上一緻。編譯後的結果也基本差不多。

public object Test3(string callTimes)

int count = 0;

int.TryParse(callTimes, out count);

if( count <= 0 )

return count;

// 先執行一次,排除編譯時間

string html = MyMVC.UcExecutor.Render("/UserControl/InlineUserControl.ascx", null);

Stopwatch watch = Stopwatch.StartNew();

for( int i = 0; i < count; i++ )

html = MyMVC.UcExecutor.Render("/UserControl/InlineUserControl.ascx", null);

watch.Stop();

return watch.Elapsed.ToString();

當我測試執行10000次時,耗時:00:00:00.9132738

  又快了一點。

  說明:為了這次的性能優化,MyMVC架構也做了一點調整。如果您以前下載下傳過這個架構,請重新下載下傳。

  分析優化結果2

  經過前面的分析,我們知道:不建立伺服器控件對象以及不調用它們的生命周期,可以讓頁面的執行速度快很多。

  有沒有再想像一下:頁面也有生命周期啊,而且生命周期的步驟更長,省略它,會不會更快呢?

  不過,Render方法并不是個public方法,我們還不能直接調用,但可以調用RenderControl方法來實作這一過程。

  由于跳過頁面的生命周期,任何伺服器控件都不能使用了,包括主機闆頁。是以我選擇将前面測試用的那段代碼移到使用者控件中, 然後将使用者控件加載到Page中來測試。

  測試用例3與測試用例2相比,在測試過程中,由于跳過了頁面的生命周期,是以速度也就更快了。

  注意:事實上,動态加載使用者控件也會有一定的調用開銷。這種方法也僅供參考,可以根據實際情況來選擇。

  嗯,基本上,就是這個簡單的原因吧。

  由于這種方法沒有任何的控件生命周期,是以速度是最快的。

  經過這一系列的優化,頁面的執行時間最終由 00:00:07.5607229 減少到 00:00:00.9132738

  再次申明:測試結果也僅僅隻是一個參考數字。

  事實上,使用伺服器控件産生的對象涉及到GC的回收以及記憶體占用的影響也是不可忽視的。

本文轉自 wws5201985 51CTO部落格,原文連結:http://blog.51cto.com/wws5201985/804118,如需轉載請自行聯系原作者

繼續閱讀