最近在重構一個以前團隊開發留下的MVC項目,項目結構堪稱混亂,問題多多,但今天說的是頁面打開速度的問題。項目中包括web后台系統,幾乎隨便點一個頁面都要盯着白屏等待2-5秒之久,體驗很差。通過對頁面性能逐步的分析和判斷,並做相應優化,最終頁面打開速度在1秒內,發布后打開速度約60ms。
一、項目結構
后台系統開發使用了典型的微軟ASP.Net MVC框架,但是版本略舊,.Net Framework4,MVC4,EF5,基本沒有采用其它的技術工具,業務邏輯大致都混亂的分布在Controller和Model層。習慣了ABP框架,相比之下這個就顯得簡陋了。至於View層,摘要中提到了白屏,可以知道前端頁面使用多頁開發,后台服務器渲染,正是Razor模板引擎,版本2.0。
登錄后台系統,隨便點擊左側菜單,右側打開頁面,大概需要白屏等待2-5秒。咦,怎么會如此之慢?!
於是基於一個典型的頁面,開始進行監測分析。我選擇了[所有用戶]菜單,頁面打開速度在3秒左右,頁面結構和功能都相對簡單,上面是一個搜索條件區域,可以按照姓名部門角色等條件組合搜索,下面就是用戶列表和分頁,展示大約10個字段,大致示意圖如下(雖然是測試階段,但數據真實,為避免泄露客戶信息,而且頁面結構簡單易於描述,所以不做截屏展示)。
二、問題分析
系統架構簡單,頁面的大概執行周期如下圖示:
從中基本推斷出耗時操作可能發生在
- 網絡傳輸
- 數據庫操作
- 業務邏輯執行
- Razor渲染
- 瀏覽器頁面渲染
用排除法,我首先檢查了客戶端瀏覽器,Web服務器,數據庫服務器的網絡環境都正常,頁面使用Bundle合並壓縮了JS,CSS,數據庫操作都是簡單的查詢,所以主要跟傳輸耗時相關的1,2兩項可以先排除。
下一步,我檢查了業務執行邏輯,也是很簡單的根據搜索條件查詢數據,並返回View,不存在很復雜的邏輯調用,3項排除。
然后,頁面渲染也沒有特殊之處,雖然也沒有特別優化,但這種簡單的頁面渲染耗時應該大概在50ms,5項也可以排除了。
引用福爾摩斯的一句話“排除所有的不可能,剩下的即使再不可能,那也是真相。”,我已久不用Razor開發多頁應用,雖不是很了解,但以前記得Razor的效率也沒有這么低啊,一個渲染模板引擎的執行至於耗費2秒嗎?
好吧,無論如何目前這項是最可疑的,先用Log4Net查看下視圖渲染用了多少時間,證明下我的猜想。
ActionFilterAttribute中監測記錄View用時經典代碼:
1 //View生成時間監控 2 public override void OnResultExecuting(ResultExecutingContext filterContext) 3 { 4 MonitorLog MonLog = filterContext.Controller.ViewData[Key] as MonitorLog; 5 MonLog.ExecuteStartTime = DateTime.Now; 6 } 7 public override void OnResultExecuted(ResultExecutedContext filterContext) 8 { 9 MonitorLog MonLog = filterContext.Controller.ViewData[Key] as MonitorLog; 10 MonLog.ExecuteEndTime = DateTime.Now; 11 LoggerHelper.Monitor(MonLog.GetLoginfo(MonitorLog.MonitorType.View)); 12 filterContext.Controller.ViewData.Remove(Key); 13 }
結果似乎印證了我的猜想,如下圖。
渲染出視圖居然耗費了2.6秒之多!是因為Razor低版本存在的問題嗎?有配置不合理嗎?Razor本身就這么低效嗎?瞬間隨之幾個想法又冒了出來。
三、改進
隨后進一步查詢了關於Razor的技術信息,做了下面兩個方面的改進。
1.在global.asax文件Application_Start()方法中設置僅使用Razor視圖引擎。由於默認MVC會首先加載WebForm視圖引擎,而本項目中僅用Razor引擎。
1 //只使用Razor引擎 2 ViewEngines.Engines.Clear(); 3 ViewEngines.Engines.Add(new RazorViewEngine());
2.設置Web.Config,設置debug模式為false。
1 <system.web> 2 <compilation debug="false" targetFramework="4.0" /> 3 ......
這會帶來幾個方面的優化,如視圖文件地址緩存,啟用請求超時,Bundle功能生效等。
這些也似乎沒有什么好優化的,然后,我再次運行頁面,發現打開時間幾乎沒有減少,WTF?!
四、繼續改進
再看監測日志,視圖渲染仍然用去大概2秒以上,這其中必有蹊蹺,不達目的不罷休,於是我又引入了性能監測工具,MiniProfiler。
然后再次打開頁面,看到紅色的監測提示時,瞬間明白了問題所在。
這個請求過程中sql請求占用90%以上的時間,居然使用了31條SQL,而且我打開看重復語句達20多條!
按道理說,我在看業務邏輯代碼時,如果代碼中會產生這么多SQL調用,一眼就可以看出來了,然而並沒有,而且日志記錄了時間耗費是計算在視圖渲染中的。
我恍然大悟,如果自己來寫Razor渲染的cshtml代碼,一定會將Model數據完善后,傳過來直接使用,不可能在cshtml代碼中再去請求數據,而這里cshtml中的代碼,卻在反復的去請求數據庫數據。
后台的Linq語句大概是db.Users.ToList(),cshtml頁面Model為@model PagedList<Models.Users>,而嵌在cshtml中的除了 model.UserName這一類,還有大量的model.Employee.EmpNo,即有的用戶有個內部員工身份,大量的延遲加載導航屬性導致反復的數據庫訪問請求。
那么到這里,問題也就明晰了,在后台的Linq語句中,應當把這些導航屬性一次性Include進來,如.Include(x=>x.Employee),還有.Include(x=>x.Roles)等等,修改后再來看打開頁面的監測結果。
可以看到用時從2769ms減少到656ms,頁面打開用時較之前減少了四分之三,控制在1秒之內,發布之后頁面打開用時約60ms,sql僅僅用了5條便完成業務邏輯,這才是asp.net mvc最起碼該有的樣子。
--End