背景
在ASP.NET MVC中,HtmlHelper的擴充方法RenderPartial為我們使用UserControl帶來了極大的方便,當我們指定一個UserControl時,RenderPartial會在當前View檔案夾下尋找相應的UserControl,如果沒有找到則會到Shared檔案夾下尋找。然後在使用RenderPartial方法有一些效能方面的考慮,值得我們去關注。
設想有這樣一個情境,一篇文章有很多個評論,在頁面中我們需要呈現出一個評論列表,自然我們會定義一個評論的UserControl,如下代碼所示:
Code 1:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<Comment>" %><%@ Import Namespace="TerryLee.MvcPerformance01.Models" %><p> ID: <%=Model.ID %> <br /> Author: <%=Model.Author %> <br /> Description: <%= Model.Description %></p><hr />
在頁面中呈現評論列表,代碼非常簡單,僅僅是遍曆所有的評論而已:
Code 2:
<div> <% foreach (var comment in Model.Comments) { Html.RenderPartial("CommentsItem", comment); } %> </div>
運行後效果如下,可以看到正確的輸出了評論:
效能最佳化1
然而當我們同時輸出200條評論的時候, 卻要花費大量的時間,用Stopwatch來測量一下會發現,輸出200條評論花費的時間基本在210ms左右,也就是說平均每條評論的輸出花費了1ms。我們不妨看一下ASP.NET MVC的原始碼,在WebFormViewEngine中當尋找UserControl時,遵循如下尋找模式:
Code 3:
public WebFormViewEngine() { MasterLocationFormats = new[] { "~/Views/{1}/{0}.master", "~/Views/Shared/{0}.master" }; ViewLocationFormats = new[] { "~/Views/{1}/{0}.aspx", "~/Views/{1}/{0}.ascx", "~/Views/Shared/{0}.aspx", "~/Views/Shared/{0}.ascx" }; PartialViewLocationFormats = ViewLocationFormats;}
所以如果我們在RenderPartial方法中,指定了UserControl的完全路徑,是不是可以避免這個尋找過程呢?修改Code 2代碼如下所示:
Code 4:
<div> <% foreach (var comment in Model.Comments) { Html.RenderPartial("~/Views/Shared/CommentsItem.ascx", comment); } %> </div>
現在再測試一下會發現呈現200條評論的時間平均值保持在10ms左右,比前面的方式提升了近200ms。然而我們是否真的找到瞭解決問題的方法?ASP.NET MVC難道不對尋找的View路徑進行緩衝?帶著這樣的疑問,我們在ASP.NET MVC原始碼VirtualPathProviderViewEngine的建構函式中,找到這樣一段代碼:
Code 5:
protected VirtualPathProviderViewEngine() { if (HttpContext.Current == null || HttpContext.Current.IsDebuggingEnabled) { ViewLocationCache = DefaultViewLocationCache.Null; } else { ViewLocationCache = new DefaultViewLocationCache(); }}
這裡的判斷說明如果啟用了Debug模式,將會使用NullViewLocationCache,即不進行緩衝,否則會使用DefaultViewLocationCache對View路徑進行緩衝。所以上面的測試結果都是基於Debug模式:
Code 6:
<compilation debug="true">
如果關閉了Debug模式,測試結果又該如何呢?使用下面代碼關閉Debug模式:
Code 7:
<compilation debug="false">
再次進行測試,會發現使用Code 2代碼呈現200條評論時,花費的時間平均值也是在10ms左右。所以在使用RenderPartial方法時,大可不必為了提升效能而指定UserControl的完全路徑,ASP.NET MVC已經為我們做好了這一切,我們要做的僅僅是在發布到生產環境時,別忘了關閉Debug模式!在本樣本中,開啟Debug和關閉Debug模式在一次調用時的效能差距如所示:
效能最佳化2
現在回過頭來看前面的代碼,其實並沒有做什麼效能最佳化,僅僅時給大家提個醒而已。在Code 2中,我們的遍曆代碼放在了首頁面中,即在每一次迭代中調用RenderPartial方法,儘管ASP.NET MVC在RenderPartial時,對於UserControl路徑做了緩衝,但是200次的調用仍然有不小的開銷。如果我們的遍曆代碼放在UserControl中,而在首頁面中只進行一次調用RenderPartial方法,結果又將如何呢?修改UserControl為下代碼所示:
Code 8:
<%@ Control Language="C#" Inherits="System.Web.Mvc.ViewUserControl<IList<Comment>>" %><%@ Import Namespace="TerryLee.MvcPerformance01.Models" %><% foreach(Comment comment in Model) { %><p> ID: <%= comment.ID%> <br /> Author: <%= comment.Author%> <br /> Description: <%= comment.Description%></p><%} %><hr />
這樣在首頁面中,只進行一次RenderPartial調用,如下代碼所示:
<div> <% Html.RenderPartial("CommentsItem", Model.Comments); %> </div>
此時再次測試,可以看到呈現200條評論所花費的時間不足1ms!為了直觀期間,我們仍然使用圖形表示如下:
從中可以看到,通過在UserControl中進行遍曆,減少RenderPartial方法調用,帶來的效能提升還是非常可觀的。
總結
本文主要討論在ASP.NET MVC中使用RenderPartial方法時的一些效能問題,記住兩點:一是在ASP.NET MVC應用程式發布到生產伺服器時,別忘了關閉Debug模式(對於ASP.NET WebForm應用程式也是一樣);二時儘可能的減少調用RenderPartial方法的次數,如通過在UserControl中進行遍曆等方法。希望對大家有用。