前幾天調查KendoGrid+Angular效能變差三倍的,推測問題根源不在KendoGrid,而在於Angular建立2000個具有獨立$scope的DOM元素,本身就是重度耗用資源工作。換句話說,就算不用KendoGrid,改以ng-repeat實作產生2000個頁面元素,對效能一樣是嚴峻考驗。光憑想像永遠不知真相,實地測上一回自然會有方向!

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>KendoGrid + NG</title>
    <script src="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-repeathtml()appendTo()
IE1164826786
Edge73635788
Chrome26513155
Firefox31825172

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畫!


Comments

Be the first to post a comment

Post a comment