Přestože nejsem zastáncem předčasných optimalizací vůči neznámé budoucnosti aplikace, výchozí výkonnost MVC aplikací není zdaleka optimální. Defaultně vytvořená MVC aplikace se snaží uspokojit širokou škálu vývojářů bez ohledu na jejich skutečný záměr. Stačí přitom vynaložit relativně málo úsilí k tomu, aby se výchozí výkonnost MVC aplikací znásobila. Výkonnost příkladů v tomto článku byla hodnocena měřením počtu requestů, které byla aplikace schopna před a po úpravě obsloužit.
Zobrazovací engine
MVC aplikace napsané na platformě .NET v současné době umožňují využití různých zobrazovacích engines. Většina vývojářů podvědomě zná WebFormViewEngine z prvních verzí MVC, případně oblíbenější Razor, který je aktuálně ve verzi 3.0.
Ve výchozím stavu nabízí MVC aplikace možnost vybrat si z obou zmíněných engines, které jsou již zaregistrovány.
Samozřejmě každý registrovaný ViewEngine má vliv na výkonnost celé aplikace a to i přesto, že fakticky při vývoji používáte například jen Razor. Přehled engines použitelných pro ASP.NET MVC je důkladně popsán na v článku na StackOverflow. Není-li tedy nezbytně nutné, je vhodné použít jen jeden engine a ten také v Global.asax zaregistrovat.
protected void Application_Start()
{
ViewEngines.Engines.Clear();
ViewEngines.Engines.Add(new RazorViewEngine());
}
Závadný kód se zbytečnými engines |
2276 REQ/S |
Kód registrující pouze Razor |
3288 REQ/S |
NULL hodnoty v Action metodách
Většina ukázkových aplikací postavených na MVC obsahuje příklady, ve kterých se volá na konci Action metod metoda View() bez předaných parametrů (modelu). Často pak následně ve View vytváříme nový objekt (například Article), který samozřejmě pracuje s hodnotou NULL. Příklad závadného kódu může vypadat následovně:
ppublic class HomeController : Controller
{
public ActionResult Index()
{
return View();
}
}
public class Article
{
public string Name { get; set; }
}
@model Performance.Controllers.Article
@{
ViewBag.Title = "Index";
}
@Html.TextBoxFor(m=> m.Name)
Pokud bychom tento kód debugovali až na úroveň tříd .NET frameworku, zjistíme, že způsobuje NullReferenceException, která je sice zachycena, ale její zpracování stojí čas a prostředky. Je-li View komplikovanější, výjimek vznikne celá řada a výkonnost dané stránky se rapidně sníží. Přitom stačí předat instanci objektu a výkonnost se opět zvýší:
public ActionResult Index()
{
return View(new Article());
}
Závadný kód |
1651 REQ/S |
Správný kód |
1900 REQ/S |
Volba Debug/Release módu
Aplikaci bychom měli před publikací na produkční prostředí přepnout do Release módu. Mnoho začínajících vývojářů publikuje v Debug módu s tím, že většinou nevidí rozdíl. Kromě využití Debug/Release módu pro extrahování různých konfigurací pomocí transformací konfiguračního souboru Web.Config používá nastavení i .NET Framework, konkrétně například třída VirtualPathProviderViewEngine.cs při nastavování cachování (v releasu módu 15 minut).
public class DefaultViewLocationCache : IViewLocationCache
{
private static readonly TimeSpan _defaultTimeSpan = new TimeSpan(0, 15, 0);
public static readonly IViewLocationCache Null =
(IViewLocationCache) new NullViewLocationCache();
Snippet z assembly Systém.Web.MVC, který pracuje s módem:
protected VirtualPathProviderViewEngine()
{
if (HttpContext.Current == null || HttpContext.Current.IsDebuggingEnabled)
this.ViewLocationCache = DefaultViewLocationCache.Null;
else
this.ViewLocationCache = (IViewLocationCache) new DefaultViewLocationCache();
}
Závadný kód |
1852 REQ/S |
Správný kód |
1896 REQ/S |
Předání cest k View v rámci volání Html.Partial()
Bez většího zkoumání tříd a metod ASP.NET MVC frameworku používají vývojáři při volání Html.Partial() odkaz na view v té podobě, jak jej vidí v solution exploreru.
@{
ViewBag.Title = "Index";
}
Statický obsah
@Html.Partial("_MyPartial")
Elegance tohoto funkčního kódu je však opět vykoupena výkonností stránky. Interní metoda FindPartialView třídy HtmlHelper totiž nejprve zkouší, zda je uvedena celá cesta k View. Pokud není uvedena cesta ale pouze název View, jako v tomto případě, spouští se prohledávání různých lokací, viz. snippet z třídy HtmlHelper:
StringBuilder stringBuilder = new StringBuilder();
foreach (string str in partialView.SearchedLocations)
{
stringBuilder.AppendLine();
stringBuilder.Append(str);
}
throw new InvalidOperationException(string.Format((IFormatProvider) CultureInfo.CurrentCulture, MvcResources.Common_PartialViewNotFound, new object[2]
{
(object) partialViewName,
(object) stringBuilder
}));
Pokud se View nenajde, uvidíme Exception, která mimo jiné vypisuje seznam všech míst, kde došlo k hledání.
Počet prohledávaných míst samozřejmě narůstá, pokud máme stále registrovaný starší WebFormViewEngine:
Řešením problému je předávat celou cestu k View:
@Html.Partial(@"~/Views/Shared/_MyPartial.cshtml")
Volání Html.Partial("_View") - Razor + WebFormViewEngine |
1387 REQ/S |
Volání Html.Partial("_View") - pouze Razor aktivní |
2276 REQ/S |
Volání Html.Partial(@"Path/_View.cshtml") |
2887 REQ/S |
Output Cache
O OutputCache by se dal napsat samostatný rozsáhlý článek. Obecně OutputCache umožňuje cachování obsahu vráceného Action metodou Controlleru, čímž se dá významně zvýšit výkonnost aplikace. Spíše než problém použití je otázka, kdy a jakým způsobem OutputCache nastavit. Speciálně v případě personalizovaných stránek (komunitní servery, eshopy) je nasazení komplikovanější. U běžných stránek, které se často nemění je ale její použití snadné a užitečné.
Statická stránka s HTML obsahem |
3266 REQ/S |
Statická stránka s HTML obsahem a OutputCache (15 s) |
8682 REQ/S |
Naměřené hodnoty jsou lákavé. Problém nasazení u stránek s personalizovaných obsahem (například zmíněné eshopy) se dá řešit více způsoby. Dvě řešení, které pokrývají běžné potřeby jsou ASP.NET DonutCaching nebo použití JavaScriptu.
Extension: ASP.NET Donut Caching
ASP.NET Donut Caching (http://mvcdonutcaching.codeplex.com/) je užitečný modul, který umí cachovat obsah stejně jako standardní OutputCache ale kromě toho lze přímo ve Views nastavit odlišnou cachovací politiku při volání @Html.Action() a tím určité části stránky z cachování vyloučit. Donut Caching lze nainstalovat přes NuGet a používá se prakticky stejně jako OutputCache.
AJAX
Pracnější ale v mnoha případech efektivní řešení je cachovat HTML obsah, který je společný, zatímco personalizovaný obsah načítat AJAXem po načtení DOMu. V případě zmíněného e-shopu je toto řešení často ideální, protože informace o produktech se příliš často nemění, zatímco obsah košíku ano.
Závěr
Způsobů, jak zvýšit výkonnost MVC aplikace je celá řada. V článku jsem se záměrně věnoval příkladům, které jsou snadné na implementaci a takřka okamžitě umožní aplikaci obsloužit mnohem více requestů za čas. Moje osobní doporučení je věnovat občas pozornost třídám a metodám, které denně používáme a čas od času si prohlédnout jejich vnitřní implementaci.