前幾天調查KendoGrid+Angular效能變差三倍的,推測問題根源不在KendoGrid,而在於Angular建立2000個具有獨立$scope的DOM元素,本身就是重度耗用資源工作。換句話說,就算不用KendoGrid,改以ng-repeat實作產生2000個頁面元素,對效能一樣是嚴峻考驗。光憑想像永遠不知真相,實地測上一回自然會有方向!
<!DOCTYPEhtml>
<html>
<head>
<metacharset="utf-8">
<title>KendoGrid + NG</title>
<scriptsrc="https://kendo.cdn.telerik.com/2015.3.930/js/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.4.5/angular.js"></script>
<style>
.list > span {
display: block; float: left; text-align: center;
border: 1px solid gray;
margin: 2px; padding: 2px; width: 110px;
}
</style>
</head>
<body>
<div id="example" ng-app="KendoDemos">
<div ng-controller="MyCtrl">
<button ng-click="loadData()">Load Data(NG)</button>
<button id="btnLoadDataHtml">Load Data(jQuery html)</button>
<button id="btnLoadDataDOM">Load Data(jQuery DOM)</button>
<div class="list">
<span ng-repeat="item in items">
{{item.FirstName}} {{item.LastName}}
{{$parent.calcDura($last)}}
</span>
</div>
</div>
</div>
<script>
var data = [];
for (var i = 0; i < 2000; i++) {
data.push({
FirstName: "FN" + i,
LastName: "LN" + i,
Country: "CN" + i,
City: "CT" + i,
Title: "T" + i
});
}
angular.module("KendoDemos", [])
.controller("MyCtrl", function ($scope) {
var st;
$scope.calcDura = function (last) {
if (last && st) {
var dura = (new Date() - st) + "ms";
st = null;
setTimeout(function () {
alert(dura);
}, 1);
}
return"";
};
$scope.loadData = function () {
st = new Date();
$scope.items = data;
};
});
$("#btnLoadDataHtml").click(function () {
var st = new Date();
var h = $.map(data, function (item, i) {
return"<span>" + item.FirstName + " " + item.LastName + "</span>";
}).join("\n");
$(".list").html(h);
alert((new Date() - st) + "ms");
});
$("#btnLoadDataDOM").click(function () {
var st = new Date();
var $list = $(".list");
$.each(data, function (i, item) {
$("<span>" + item.FirstName + " " + item.LastName + "</span>").appendTo($list);
});
alert((new Date() - st) + "ms");
});
</script>
</body>
</html>
設計了以上的實驗,分別使用ngRepeat、jQuery組HTML字串以及jQuery逐一加入DOM三種做法,測試產生2000個<span>所需的時間。耗時毫秒數以alert顯示,ngRepeat要抓DOM生成時機有點挑戰,我想到的解法是檢查$last變數,在<span>多埋一個{{$parent.calcDura($last)}}偵測最後一筆。擔心額外機制可能干擾效數字,經Profiler觀察,加入前後執行時間差異極小,可忽略其影響。
依理而論,組合HTML字串再一次反應到DOM是最有效率的做法,至於逐筆產生<span>加入DOM跟ngRepeat誰比較快,則有待實驗證明。
將程式放上JSBin,測試方法為每次重新載入網頁,按鈕,取得耗時數據,反覆數次取最低值。
ng-repeat | html() | appendTo() | |
IE11 | 648 | 26 | 786 |
Edge | 736 | 35 | 788 |
Chrome | 265 | 13 | 155 |
Firefox | 318 | 25 | 172 |
Chrome及IE效能分析工具顯示,ng-repeat的瓶頸出現在ngRepeatAction / controllerBoundTransclude / publicLinkFn
IE11的效能分析報告又更精確些,publicLinkFn跑了1963次,boundTransclude 37次,佔用絕大部分時間。
結論:針對動態產生大量DOM元素的應用情境,若後續不需繫結連動,組裝HTML再一次產生DOM仍是王道,其效能表現讓ngRepeat及逐筆新増DOM望塵莫及,甚至可快上數十倍。在Chrome及Firefox使用ng-repeat比逐筆加DOM更慢,在IE及Edge,ng-repeat則比逐筆加DOM快。
最後補充Hina大人的寶貴經驗:面對20MB JSON、8000筆資料使用Agular產生DOM元素,下場只能用一個慘字形容,嘗試改用React也難有起色,依據一些NG、React高手的看法-或許該考慮自己用Canvas畫!