Knockout.js는 준 대형 데이터 세트에서 엄청나게 느립니다.


86

이제 막 Knockout.js를 시작하고 있습니다 (항상 사용해보고 싶었지만 이제는 변명 할 수 있습니다!)-그러나 테이블을 상대적으로 작은 집합에 바인딩 할 때 정말 나쁜 성능 문제가 발생합니다. 데이터 (약 400 행 정도).

내 모델에는 다음 코드가 있습니다.

this.projects = ko.observableArray( [] ); //Bind to empty array at startup

this.loadData = function (data) //Called when AJAX method returns
{
   for(var i = 0; i < data.length; i++)
   {
      this.projects.push(new ResultRow(data[i])); //<-- Bottleneck!
   }
};

문제는 for위 의 루프가 약 400 행으로 약 30 초 정도 걸린다는 것입니다. 그러나 코드를 다음과 같이 변경하면

this.loadData = function (data)
{
   var testArray = []; //<-- Plain ol' Javascript array
   for(var i = 0; i < data.length; i++)
   {
      testArray.push(new ResultRow(data[i]));
   }
};

그런 다음 for눈 깜짝 할 사이에 루프가 완료됩니다. 즉, pushKnockout의 observableArray개체 방법 이 엄청나게 느립니다.

내 템플릿은 다음과 같습니다.

<tbody data-bind="foreach: projects">
    <tr>
       <td data-bind="text: code"></td>
       <td><a data-bind="projlink: key, text: projname"></td>
       <td data-bind="text: request"></td>
       <td data-bind="text: stage"></td>
       <td data-bind="text: type"></td>
       <td data-bind="text: launch"></td>
       <td><a data-bind="mailto: ownerEmail, text: owner"></a></td>
    </tr>
</tbody>

내 질문 :

  1. 이것이 내 데이터 (AJAX 메서드에서 가져온)를 관찰 가능한 컬렉션에 바인딩하는 올바른 방법입니까?
  2. push바인딩 된 DOM 개체를 다시 빌드하는 것과 같이 호출 할 때마다 무거운 재 계산을 수행 할 것으로 예상 합니다. 이 재 계산을 지연하거나 한 번에 모든 항목을 푸시 할 수있는 방법이 있습니까?

필요한 경우 더 많은 코드를 추가 할 수 있지만 이것이 관련성이 있다고 확신합니다. 대부분의 경우 사이트에서 Knockout 자습서를 따랐습니다.

최신 정보:

아래 조언에 따라 코드를 업데이트했습니다.

this.loadData = function (data)
{
   var mappedData = $.map(data, function (item) { return new ResultRow(item) });
   this.projects(mappedData);
};

그러나 this.projects()여전히 400 행의 경우 약 10 초가 걸립니다. Knockout (DOM을 통해 행 추가) 없이 이것이 얼마나 빠를 지 잘 모르겠지만 10 초보다 훨씬 빠를 것 같습니다.

업데이트 2 :

아래의 다른 조언에 따라 jQuery.tmpl ( KnockOut 에서 기본적으로 지원됨)을 제공 했으며이 템플릿 엔진은 3 초 만에 약 400 개의 행을 그릴 것입니다. 스크롤 할 때 더 많은 데이터를 동적으로로드하는 솔루션이 부족한 가장 좋은 방법 인 것 같습니다.


1
녹아웃 foreach 바인딩 또는 foreach와 템플릿 바인딩을 사용하고 있습니까? 템플릿을 사용하고 기본 템플릿 엔진 대신 jquery tmpl을 포함하는 것이 차이를 만들 수 있는지 궁금합니다.
madcapnmckay 2012 년

1
@MikeChristensen-Knockout에는 (foreach, with) 바인딩과 관련된 자체 기본 템플릿 엔진이 있습니다. 또한 다른 템플릿 엔진, 즉 jquery.tmpl을 지원합니다. 읽기 여기에 자세한 내용은. 다른 엔진으로 벤치마킹을하지 않았으므로 도움이 될지 모르겠습니다. 이전 의견을 읽으면 IE7에서 원하는 성능을 얻는 데 어려움을 겪을 수 있습니다.
madcapnmckay 2012 년

2
몇 달 전에 IE7이 나온 것을 고려하면 IE9는 2019 년 여름 쯤에 출시 될 것이라고 생각합니다. 오, 우리도 모두 WinXP를 사용하고 있습니다 .. Blech.
Mike Christensen

1
추신, 느린 것처럼 보이는 이유는 관찰 가능한 배열에 개별적으로 400 개의 항목을 추가하기 때문 입니다. 관찰 가능 항목이 변경 될 때마다 해당 배열에 의존하는 모든 항목에 대해 뷰를 다시 렌더링해야합니다. 복잡한 템플릿과 추가 할 많은 항목의 경우 배열을 다른 인스턴스로 설정하여 한 번에 모두 업데이트 할 수있는 경우 많은 오버 헤드가 발생합니다. 적어도 다시 렌더링은 한 번만 수행됩니다.
Jeff Mercado 2012 년

1
나는 더 빠르고 깔끔한 방법을 찾았습니다. 사용 valueHasMutated은 그것을합니다. 시간이 있으면 답을 확인하십시오.
슈퍼 냉각

답변:


16

의견에서 제안한대로.

Knockout에는 바인딩 (foreach, with)과 관련된 고유 한 기본 템플릿 엔진이 있습니다. 또한 다른 템플릿 엔진, 즉 jquery.tmpl을 지원합니다. 읽기 여기에 자세한 내용은. 다른 엔진으로 벤치마킹을하지 않았으므로 도움이 될지 모르겠습니다. 이전 의견을 읽으면 IE7에서 원하는 성능을 얻는 데 어려움을 겪을 수 있습니다.

제쳐두고 KO는 누군가가 어댑터를 작성한 경우 모든 js 템플릿 엔진을 지원합니다. JQuery와 tmpl에 의해 대체 될 예정이로에서 당신은 다른 사람을 시도 할 수 있습니다 JsRender .


나는 성능이 훨씬 좋아지고 jquery.tmpl있으므로 그것을 사용할 것입니다. 여유 시간이 있으면 다른 엔진을 조사하고 직접 작성할 수도 있습니다. 감사!
Mike Christensen

1
@MikeChristensen-여전히 data-bindjQuery 템플릿에서 문을 사용하고 있습니까, 아니면 $ {code} 구문을 사용하고 있습니까?
ericb

@ericb-새 코드로 ${code}구문을 사용 하고 있으며 훨씬 빠릅니다. 또한 Underscore.js를 작동 시키려고 노력했지만 아직 운이 없었고 ( <% .. %>구문이 ASP.NET을 방해 함) 아직 JsRender 지원이없는 것 같습니다.
Mike Christensen

1
@MikeChristensen-좋아, 그럼 말이 돼. KO의 기본 템플릿 엔진이 반드시 비효율적 인 것은 아닙니다. $ {code} 구문을 사용하면 해당 요소에 대한 데이터 바인딩을 얻지 못하므로 성능이 향상됩니다. 따라서의 속성을 변경 ResultRow하면 UI가 업데이트되지 않습니다 ( projects테이블 다시 렌더링을 강제하는 observableArray 를 업데이트 해야합니다). $ {} 데이터가 읽기 전용 인 경우 확실히 유리할 수 있습니다.
ericb

4
점! jquery.tmpl는 개발에 더 이상 없다
알렉스 Larzelere


13

$ .map을 사용하는 것 외에도 KO로 페이지 매김사용하십시오 .

녹아웃과 함께 페이징을 사용할 때까지 1400 레코드의 대규모 데이터 세트에서 동일한 문제가 발생했습니다. 사용 $.map기록을로드하는 것은 큰 차이를 만들 않았다하지만 DOM은 시간이 아직도 끔찍한했다 렌더링합니다. 그런 다음 페이지 매김을 사용하여 데이터 세트 조명을 빠르고 사용자 친화적으로 만들었습니다. 페이지 크기가 50이면 데이터 세트의 부담이 훨씬 줄어들고 DOM 요소 수가 크게 감소했습니다.

KO로 매우 쉽게 할 수 있습니다.

http://jsfiddle.net/rniemeyer/5Xr2X/


11

KnockoutJS에는 특히 데이터로드 및 저장에 관한 훌륭한 튜토리얼이 있습니다.

그들의 경우에는 getJSON()매우 빠른 데이터를 사용 합니다. 그들의 예에서 :

function TaskListViewModel() {
    // ... leave the existing code unchanged ...

    // Load initial state from server, convert it to Task instances, then populate self.tasks
    $.getJSON("/tasks", function(allData) {
        var mappedTasks = $.map(allData, function(item) { return new Task(item) });
        self.tasks(mappedTasks);
    });    
}

1
확실히 크게 개선되었지만 실행하는 self.tasks(mappedTasks)데 약 10 초가 걸립니다 (400 행 포함). 아직 받아 들일 수 없다고 생각합니다.
Mike Christensen

10 초는 허용되지 않는다는 데 동의합니다. knockoutjs를 사용하면지도보다 더 나은 것이 무엇인지 잘 모르겠으므로이 질문을 좋아하고 더 나은 답변을 주시하겠습니다.
deltree 2012 년

1
확인. 대답은 확실히 +1내 코드를 단순화하고 속도를 크게 향상시키는 데 적합합니다. 아마도 누군가가 병목 현상에 대해 더 자세한 설명을 가지고있을 것입니다.
Mike Christensen

9

KoGrid 제공 봐. 행 렌더링을 지능적으로 관리하여 성능을 향상시킵니다.

다음을 사용하여 400 행을 테이블에 바인딩하려는 경우 foreach바인딩을 KO를 통해 DOM으로 많은 것을 푸시하는 데 어려움이 있습니다.

KO는 foreach바인딩을 사용하여 매우 흥미로운 작업을 수행하며 대부분은 매우 좋은 작업이지만 배열의 크기가 커짐에 따라 성능이 저하되기 시작합니다.

나는 큰 데이터 세트를 테이블 / 그리드에 바인딩하려는 길고 어두운 길을 걸어 왔고 결국 데이터를 로컬에서 분리 / 페이지 화해야합니다.

KoGrid 가이 모든 작업을 수행합니다. 뷰어가 페이지에서 볼 수있는 행만 렌더링 한 다음 필요할 때까지 다른 행을 가상화하도록 구축되었습니다. 나는 당신이 경험하는 것보다 훨씬 더 나은 400 항목에 대한 성능을 발견 할 것이라고 생각합니다.


1
이것은 IE7에서 완전히 깨진 것처럼 보입니다 (샘플이 작동하지 않음), 그렇지 않으면 이것은 훌륭 할 것입니다!
Mike Christensen

살펴 보게되어 기쁩니다. KoGrid는 아직 개발 중입니다. 그러나 이것이 적어도 성능에 관한 귀하의 질문에 대답합니까?
ericb

1
예! 기본 KO 템플릿 엔진이 매우 느리다는 원래 의심을 확인합니다. 기니피그 KoGrid에 누군가가 필요하면 기꺼이 할 것입니다. 정확히 우리가 필요로하는 것 같습니다!
Mike Christensen

꿰매다. 정말 맛있어 보여요! 불행히도 내 애플리케이션 사용자의 50 % 이상이 IE7을 사용합니다!
Jim G.

흥미롭게도 요즘 우리는 IE11을 마지 못해 지원해야합니다. 지난 7 년 동안 상황이 개선되었습니다.
MrBoJangles

5

매우 큰 배열을 렌더링 할 때 브라우저가 잠기는 것을 방지하는 해결책은 배열을 '조절'하여 한 번에 몇 개의 요소 만 추가되고 그 사이에 휴면 상태를 유지하는 것입니다. 이를 수행하는 함수는 다음과 같습니다.

function throttledArray(getData) {
    var showingDataO = ko.observableArray(),
        showingData = [],
        sourceData = [];
    ko.computed(function () {
        var data = getData();
        if ( Math.abs(sourceData.length - data.length) / sourceData.length > 0.5 ) {
            showingData = [];
            sourceData = data;
            (function load() {
                if ( data == sourceData && showingData.length != data.length ) {
                    showingData = showingData.concat( data.slice(showingData.length, showingData.length + 20) );
                    showingDataO(showingData);
                    setTimeout(load, 500);
                }
            })();
        } else {
            showingDataO(showingData = sourceData = data);
        }
    });
    return showingDataO;
}

사용 사례에 따라 사용자가 스크롤하기 전에 첫 번째 행 배치 만 볼 수 있으므로 UX가 크게 향상 될 수 있습니다.


이 솔루션이 마음에 들지만 매 반복마다 setTimeout을 실행하는 대신 매번로드하는 데 시간이 너무 오래 걸리기 때문에 20 회 이상 반복마다 setTimout 만 실행하는 것이 좋습니다. 나는 당신이 +20으로 그것을하고 있음을 알지만 언뜻보기에는 분명하지 않았습니다.
charlierlee

5

가변 인수를 받아들이는 push ()를 활용하면 제 경우에는 최상의 성능을 얻을 수있었습니다. 5973ms (~ 6 초) 동안 1300 개의 행이로드되었습니다. 이 최적화를 통해로드 시간은 914ms (<1 초)로 줄었습니다.
이는 84.7 % 향상되었습니다!

observableArray에 항목 푸시에 대한 추가 정보

this.projects = ko.observableArray( [] ); //Bind to empty array at startup

this.loadData = function (data) //Called when AJAX method returns
{
   var arrMappedData = ko.utils.arrayMap(data, function (item) {
       return new ResultRow(item);
   });
   //take advantage of push accepting variable arguments
   this.projects.push.apply(this.projects, arrMappedData);
};

4

나는 내게 들어오는 엄청난 양의 데이터를 다루고 있었는데 valueHasMutated매력처럼 작동했다.

모델보기 :

this.projects([]); //make observableArray empty --(1)

var mutatedArray = this.projects(); -- (2)

this.loadData = function (data) //Called when AJAX method returns
{
ko.utils.arrayForEach(data,function(item){
    mutatedArray.push(new ResultRow(item)); -- (3) // push to the array(normal array)  
});  
};
 this.projects.valueHasMutated(); -- (4) 

(4)배열 데이터를 호출 한 후 필요한 observableArray에로드됩니다.this.projects 자동으로 됩니다.

시간이 있으면 이걸보고 문제가 있으면 알려주세요

트릭 : 이렇게함으로써 의존성 (계산, 구독 등)의 경우 푸시 수준에서 피할 수 있고을 호출 한 후 한 번에 실행할 수 있습니다 (4).


1
문제는를 너무 많이 호출하는 push것이 아니라 push를 한 번만 호출해도 렌더링 시간이 길다는 것입니다. 배열에 foreach.
약간

1

jQuery.tmpl 사용과 결합하여 가능한 해결 방법은 setTimeout을 사용하여 비동기 방식으로 관찰 가능한 배열에 항목을 한 번에 푸시하는 것입니다.

var self = this,
    remaining = data.length;

add(); // Start adding items

function add() {
  self.projects.push(data[data.length - remaining]);

  remaining -= 1;

  if (remaining > 0) {
    setTimeout(add, 10); // Schedule adding any remaining items
  }
}

이렇게하면 한 번에 하나의 항목 만 추가 할 때 브라우저 / knockout.js가 몇 초 동안 완전히 차단되지 않고 그에 따라 DOM을 조작하는 데 시간을 할애하여 사용자가 목록을 동시에 스크롤 할 수 있습니다.


2
이렇게하면 N 개의 DOM 업데이트가 강제 실행되어 한 번에 모든 작업을 수행하는 것보다 훨씬 더 긴 총 렌더링 시간이 발생합니다.
Fredrik C

물론 맞습니다. 그러나 요점은 N이 큰 숫자이고 항목을 프로젝트 배열에 밀어 넣어 상당한 양의 다른 DOM 업데이트 또는 계산을 트리거하면 브라우저가 멈추고 탭을 종료하도록 제안 할 수 있다는 것입니다. 항목 당 또는 10 개, 100 개 또는 기타 항목 수에 대해 시간 제한을 설정하면 브라우저가 계속 응답합니다.

2
나는 이것이 전체 업데이트가 브라우저를 멈추지 않는 일반적인 경우 잘못된 접근 방식이라고 말하고 싶지만 다른 모든 것이 실패 할 때 사용할 것입니다. 나에게 그것은 성능 문제가 멈추지 않도록하는 대신 해결되어야하는 잘못 작성된 응용 프로그램처럼 들립니다.
Fredrik C

1
물론 일반적인 경우에는 잘못된 접근 방식이며 아무도 동의하지 않을 것입니다. 이것은 많은 DOM 작업을 수행해야하는 경우 브라우저 정지를 방지하기위한 해킹이자 개념 증명입니다. 몇 년 전 셀당 여러 개의 바인딩이있는 여러 개의 큰 HTML 테이블을 나열 할 때 필요했습니다. 결과적으로 수천 개의 바인딩이 평가되고 각각이 DOM의 상태에 영향을 미칩니다. 이 기능은 Excel 기반 데스크톱 응용 프로그램을 웹 응용 프로그램으로 다시 구현할 때의 정확성을 확인하기 위해 일시적으로 필요했습니다. 그런 다음이 솔루션이 완벽하게 작동했습니다.

댓글은 대부분 다른 사람들이 이것이 선호되는 방법이라고 가정하지 않기 위해 읽었습니다. 나는 당신이 무엇을하고 있는지 알고 있다고 생각했습니다.
Fredrik C

1

저는 성능을 실험 해 왔으며 유용 할 수 있기를 바라는 두 가지 공헌을했습니다.

내 실험은 DOM 조작 시간에 중점을 둡니다. 따라서 여기에 들어가기 전에 관찰 가능한 배열 등을 만들기 전에 JS 배열로 푸시하는 것에 대해 위의 요점을 따르는 것이 확실히 가치가 있습니다.

그러나 DOM 조작 시간이 여전히 방해가되는 경우 다음이 도움이 될 수 있습니다.


1 : 느린 렌더링 주위에 로딩 스피너를 감싼 다음 afterRender를 사용하여 숨기는 패턴

http://jsfiddle.net/HBYyL/1/

이것은 실제로 성능 문제에 대한 수정은 아니지만 수천 개의 항목을 반복하고 긴 KO 작업 전에 로딩 스피너가 나타나도록 한 다음 숨길 수있는 패턴을 사용하는 경우 지연이 불가피하다는 것을 보여줍니다. 나중에. 그래서 적어도 UX를 향상시킵니다.

스피너를로드 할 수 있는지 확인하십시오.

// Show the spinner immediately...
$("#spinner").show();

// ... by using a timeout around the operation that causes the slow render.
window.setTimeout(function() {
    ko.applyBindings(vm)  
}, 1)

스피너 숨기기 :

<div data-bind="template: {afterRender: hide}">

트리거 :

hide = function() {
    $("#spinner").hide()
}

2 : HTML 바인딩을 해킹으로 사용

저는 Opera로 셋톱 박스에서 작업하면서 DOM 조작을 사용하여 UI를 구축 할 때의 오래된 기술을 기억했습니다. 너무 느려서 해결책은 큰 HTML 덩어리를 문자열로 저장하고 innerHTML 속성을 설정하여 문자열을로드하는 것이 었습니다.

html 바인딩과 테이블에 대한 HTML을 큰 텍스트 청크로 파생하는 계산을 사용하여 비슷한 작업을 수행 한 다음 한 번에 적용 할 수 있습니다. 이렇게하면 성능 문제가 해결되지만 큰 단점은 각 테이블 행 내에서 바인딩으로 수행 할 수있는 작업을 심각하게 제한한다는 것입니다.

모호하게 KO와 같은 방식으로 항목을 삭제하기 위해 테이블 ​​행 내부에서 호출 할 수있는 함수와 함께이 접근 방식을 보여주는 바이올린이 있습니다. 분명히 이것은 적절한 KO만큼 좋지는 않지만 타오르는 성능이 정말로 필요한 경우 가능한 해결 방법입니다.

http://jsfiddle.net/9ZF3g/5/


1

IE를 사용하는 경우 개발 도구를 닫으십시오.

IE에서 개발자 도구를 열면이 작업이 상당히 느려집니다. 배열에 ~ 1000 개의 요소를 추가하고 있습니다. 개발 도구를 열면 약 10 초가 걸리며 IE가 실행되는 동안 중단됩니다. 개발 도구를 닫으면 작업이 즉시 이루어지며 IE에서 속도가 느려지지 않습니다.


0

또한 Knockout js 템플릿 엔진이 IE에서 느리게 작동하고 underscore.js로 대체하고 훨씬 빠르게 작동한다는 것을 알았습니다.


제발 어떻게 한거야?
Stu Harper

@StuHarper 나는 밑줄 라이브러리를 가져온 다음 main.js에서 knockoutjs.com/documentation/template-binding.html의
Marcello

이 개선이 발생한 IE 버전은 무엇입니까?
bkwdesign

내가 IE 10, 11 사용하고 있었다 @bkwdesign
마르첼로
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.