Knockout.js cực kỳ chậm trong tập dữ liệu bán lớn


86

Tôi chỉ mới bắt đầu với Knockout.js (luôn muốn dùng thử, nhưng cuối cùng thì tôi cũng có cớ!) - Tuy nhiên, tôi đang gặp phải một số vấn đề hiệu suất thực sự tồi tệ khi liên kết một bảng với một tập hợp tương đối nhỏ dữ liệu (khoảng 400 hàng hoặc lâu hơn).

Trong mô hình của tôi, tôi có mã sau:

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!
   }
};

Vấn đề là forvòng lặp ở trên mất khoảng 30 giây hoặc lâu hơn với khoảng 400 hàng. Tuy nhiên, nếu tôi thay đổi mã thành:

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

Sau đó, forvòng lặp hoàn thành trong chớp mắt. Nói cách khác, pushphương thức của observableArrayđối tượng Knockout cực kỳ chậm.

Đây là mẫu của tôi:

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

Những câu hỏi của tôi:

  1. Đây có phải là cách phù hợp để liên kết dữ liệu của tôi (đến từ phương pháp AJAX) với một tập hợp có thể quan sát được không?
  2. Tôi mong đợi pushlà thực hiện một số re-calc nặng nề mỗi khi tôi gọi nó, chẳng hạn như có thể xây dựng lại các đối tượng DOM bị ràng buộc. Có cách nào để trì hoãn việc sửa lỗi này hoặc có thể đẩy tất cả các mặt hàng của tôi cùng một lúc?

Tôi có thể thêm nhiều mã nếu cần, nhưng tôi khá chắc rằng đây là những gì có liên quan. Đối với hầu hết các phần, tôi chỉ làm theo hướng dẫn Knockout từ trang web.

CẬP NHẬT:

Theo lời khuyên bên dưới, tôi đã cập nhật mã của mình:

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

Tuy nhiên, this.projects()vẫn mất khoảng 10 giây cho 400 hàng. Tôi thừa nhận rằng tôi không chắc điều này sẽ nhanh như thế nào nếu không có Knockout (chỉ thêm hàng thông qua DOM), nhưng tôi có cảm giác rằng nó sẽ nhanh hơn nhiều so với 10 giây.

CẬP NHẬT 2:

Theo lời khuyên khác bên dưới, tôi đã thử jQuery.tmpl (được hỗ trợ bởi KnockOut) và công cụ tạo khuôn mẫu này sẽ vẽ khoảng 400 hàng chỉ trong hơn 3 giây. Đây có vẻ là cách tiếp cận tốt nhất, ngắn gọn là một giải pháp có thể tải động nhiều dữ liệu hơn khi bạn cuộn.


1
Bạn đang sử dụng ràng buộc foreach loại trực tiếp hay ràng buộc mẫu với foreach. Tôi chỉ tự hỏi liệu việc sử dụng mẫu và bao gồm jquery tmpl thay vì công cụ mẫu gốc có thể tạo ra sự khác biệt hay không.
madcapnmckay

1
@MikeChristensen - Knockout có công cụ mẫu gốc của riêng nó được liên kết với các liên kết (foreach, with). Nó cũng hỗ trợ các công cụ mẫu khác, cụ thể là jquery.tmpl. Đọc ở đây để biết thêm chi tiết. Tôi chưa thực hiện bất kỳ điểm chuẩn nào với các công cụ khác nhau nên không biết liệu nó có hữu ích không. Đọc nhận xét trước đó của bạn, trong IE7, bạn có thể gặp khó khăn để có được hiệu suất như sau.
madcapnmckay

2
Xem xét chúng tôi vừa có IE7 vài tháng trước, tôi nghĩ IE9 sẽ được tung ra vào khoảng mùa hè năm 2019. Ồ, tất cả chúng ta đều đang sử dụng WinXP .. Blech.
Mike Christensen

1
ps, Lý do nó có vẻ chậm là bạn đang thêm 400 mục riêng lẻ vào mảng có thể quan sát đó . Đối với mọi thay đổi đối với phần có thể quan sát, chế độ xem phải được hiển thị cho bất kỳ thứ gì phụ thuộc vào mảng đó. Đối với các mẫu phức tạp và nhiều mục cần thêm, đó là rất nhiều chi phí khi bạn có thể chỉ cập nhật mảng cùng một lúc bằng cách đặt nó thành một phiên bản khác. Ít nhất sau đó, việc kết xuất sẽ được thực hiện một lần.
Jeff Mercado

1
Tôi đã tìm thấy một cách nhanh hơn và gọn gàng hơn (không có gì ngoài hộp). sử dụng valueHasMutatednó. kiểm tra câu trả lời nếu bạn có thời gian.
siêu thú vị

Câu trả lời:


16

Như được đề xuất trong các ý kiến.

Knockout có công cụ mẫu gốc của riêng nó được liên kết với các ràng buộc (foreach, with). Nó cũng hỗ trợ các công cụ mẫu khác, cụ thể là jquery.tmpl. Đọc ở đây để biết thêm chi tiết. Tôi chưa thực hiện bất kỳ điểm chuẩn nào với các công cụ khác nhau nên không biết liệu nó có hữu ích không. Đọc nhận xét trước đó của bạn, trong IE7, bạn có thể gặp khó khăn để có được hiệu suất như sau.

Ngoài ra, KO hỗ trợ bất kỳ công cụ tạo khuôn mẫu js nào, nếu ai đó đã viết bộ điều hợp cho nó. Bạn có thể muốn thử những người khác ở đó vì jquery tmpl sẽ được thay thế bằng JsRender .


Tôi đang trở nên hoàn hảo hơn nhiều jquery.tmplnên tôi sẽ sử dụng nó. Tôi có thể điều tra các động cơ khác cũng như viết bài của riêng tôi nếu tôi có thêm thời gian. Cảm ơn!
Mike Christensen

1
@MikeChristensen - bạn vẫn đang sử dụng data-bindcâu lệnh trong mẫu jQuery của mình hay bạn đang sử dụng cú pháp $ {code}?
ericb

@ericb - Với mã mới, tôi đang sử dụng ${code}cú pháp và nó nhanh hơn nhiều. Tôi cũng đang cố gắng làm cho Underscore.js hoạt động, nhưng vẫn chưa gặp may ( <% .. %>cú pháp can thiệp vào ASP.NET) và dường như vẫn chưa có hỗ trợ JsRender.
Mike Christensen

1
@MikeChristensen - được rồi, điều này có lý. Công cụ mẫu gốc của KO không hẳn là không hiệu quả. Khi bạn sử dụng cú pháp $ {code}, bạn sẽ không nhận được bất kỳ ràng buộc dữ liệu nào trên các phần tử đó (điều này giúp cải thiện hiệu suất). Vì vậy, nếu bạn thay đổi một thuộc tính của a ResultRow, nó sẽ không cập nhật giao diện người dùng (bạn sẽ phải cập nhật projectsObservableArray sẽ buộc hiển thị lại bảng của bạn). $ {} chắc chắn có thể có lợi nếu dữ liệu của bạn ở chế độ chỉ đọc
ericb

4
Necromancy! jquery.tmpl không còn trong phát triển
Alex Larzelere

50

Vui lòng xem: Knockout.js Performance Gotcha # 2 - Thao tác quan sát

Một mẫu tốt hơn là lấy một tham chiếu đến mảng cơ bản của chúng ta, đẩy đến mảng đó, sau đó gọi .valueHasMutated (). Bây giờ, những người đăng ký của chúng tôi sẽ chỉ nhận được một thông báo cho biết rằng mảng đã thay đổi.


13

Sử dụng phân trang với KO ngoài việc sử dụng $ .map.

Tôi đã gặp vấn đề tương tự với một bộ dữ liệu lớn gồm 1400 bản ghi cho đến khi tôi sử dụng phân trang bằng knockout. Việc sử dụng $.mapđể tải các bản ghi đã tạo ra một sự khác biệt lớn nhưng thời gian hiển thị DOM vẫn còn tệ hại. Sau đó, tôi đã thử sử dụng phân trang và điều đó làm cho tập dữ liệu của tôi chiếu sáng nhanh cũng như thân thiện hơn với người dùng. Kích thước trang 50 làm cho tập dữ liệu ít áp đảo hơn nhiều và giảm đáng kể số lượng phần tử DOM.

Rất dễ thực hiện với KO:

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


11

KnockoutJS có một số hướng dẫn tuyệt vời, đặc biệt là hướng dẫn về tải và lưu dữ liệu

Trong trường hợp của họ, họ lấy dữ liệu bằng cách sử dụng getJSON()cực kỳ nhanh. Từ ví dụ của họ:

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
Chắc chắn là một cải tiến lớn, nhưng self.tasks(mappedTasks)mất khoảng 10 giây để chạy (với 400 hàng). Tôi cảm thấy điều này vẫn chưa thể chấp nhận được.
Mike Christensen

Tôi đồng ý rằng 10 giây là không thể chấp nhận được. Sử dụng knockoutjs, tôi không chắc điều gì tốt hơn bản đồ, vì vậy tôi sẽ thích câu hỏi này và theo dõi để có câu trả lời tốt hơn.
deltree

1
Đồng ý. Câu trả lời chắc chắn xứng đáng +1cho cả việc đơn giản hóa mã của tôi và tăng tốc độ đáng kể. Có lẽ ai đó có một lời giải thích chi tiết hơn về nút cổ chai là gì.
Mike Christensen

9

Cho KoGrid xem. Nó quản lý hiển thị hàng của bạn một cách thông minh để nó hoạt động hiệu quả hơn.

Nếu bạn đang cố gắng liên kết 400 hàng vào một bảng bằng cách sử dụng foreachliên kết, bạn sẽ gặp khó khăn khi đẩy nhiều hàng đó qua KO vào DOM.

KO thực hiện một số điều rất thú vị bằng cách sử dụng foreachliên kết, hầu hết trong số đó là các hoạt động rất tốt, nhưng chúng bắt đầu bị phá vỡ về hiệu suất khi kích thước mảng của bạn tăng lên.

Tôi đã trải qua chặng đường dài đen tối khi cố gắng liên kết các tập dữ liệu lớn với các bảng / lưới và cuối cùng bạn cần phải tách / trang dữ liệu một cách cục bộ.

KoGrid làm tất cả điều này. Nó được xây dựng để chỉ hiển thị các hàng mà người xem có thể nhìn thấy trên trang, sau đó ảo hóa các hàng khác cho đến khi chúng cần thiết. Tôi nghĩ rằng bạn sẽ thấy hiệu suất của nó trên 400 mặt hàng tốt hơn nhiều so với những gì bạn đang trải nghiệm.


1
Điều này dường như đã bị hỏng hoàn toàn trên IE7 (không có mẫu nào hoạt động), nếu không thì điều này sẽ rất tuyệt!
Mike Christensen

Rất vui khi xem xét nó - KoGrid vẫn đang trong quá trình phát triển tích cực. Tuy nhiên, điều này ít nhất có trả lời câu hỏi của bạn liên quan đến perf không?
ericb

1
Đúng! Nó xác nhận nghi ngờ ban đầu của tôi rằng công cụ mẫu KO mặc định khá chậm. Nếu bạn cần bất cứ ai nuôi chuột lang KoGrid cho bạn, tôi rất sẵn lòng. Có vẻ như chính xác những gì chúng ta cần!
Mike Christensen

Em yêu. Điều này trông thực sự tốt! Thật không may, hơn 50% người dùng ứng dụng của tôi sử dụng IE7!
Jim G. Ngày

Thật thú vị, ngày nay chúng ta phải miễn cưỡng hỗ trợ IE11. Mọi thứ đã được cải thiện trong 7 năm qua.
MrBoJangle

5

Một giải pháp để tránh khóa trình duyệt khi hiển thị một mảng rất lớn là 'điều chỉnh' mảng sao cho chỉ một số phần tử được thêm vào cùng một lúc, ở giữa. Đây là một chức năng sẽ làm điều đó:

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;
}

Tùy thuộc vào trường hợp sử dụng của bạn, điều này có thể dẫn đến cải thiện UX lớn, vì người dùng có thể chỉ nhìn thấy loạt hàng đầu tiên trước khi phải cuộn.


Tôi thích giải pháp này, nhưng thay vì setTimeout mỗi lần lặp lại, tôi khuyên bạn chỉ nên chạy setTimout sau mỗi 20 lần lặp hoặc hơn vì mỗi lần cũng mất quá nhiều thời gian để tải. Tôi thấy rằng bạn đang làm điều đó với +20, nhưng nó không rõ ràng đối với tôi thoạt nhìn.
charlierlee

5

Việc tận dụng push () chấp nhận các đối số biến đã mang lại hiệu suất tốt nhất trong trường hợp của tôi. 1300 hàng được tải trong 5973ms (~ 6 giây). Với sự tối ưu hóa này, thời gian tải giảm xuống còn 914ms (<1 giây)
, cải thiện 84,7%!

Thông tin thêm tại Đẩy các mục vào một Mảng có thể quan sát

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

Tôi đã đối phó với khối lượng dữ liệu khổng lồ đến với tôi valueHasMutatednhư một cái duyên.

Xem mô hình:

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) 

Sau khi gọi, (4)dữ liệu mảng sẽ được tải this.projectstự động vào ObservableArray bắt buộc .

Nếu bạn có thời gian, hãy xem cái này và đề phòng bất kỳ rắc rối nào, hãy cho tôi biết

Thủ thuật ở đây: Bằng cách làm như vậy, nếu trong trường hợp có bất kỳ phụ thuộc nào (tính toán, đăng ký, v.v.) có thể tránh được ở mức đẩy và chúng ta có thể làm cho chúng thực thi ngay sau khi gọi (4).


1
Vấn đề không phải là quá nhiều cuộc gọi đến push, vấn đề là ngay cả một cuộc gọi đẩy sẽ gây ra thời gian hiển thị lâu. Nếu một mảng có 1000 mục được liên kết với a foreach, việc đẩy một mục duy nhất sẽ hiển thị toàn bộ foreach và bạn phải trả chi phí thời gian hiển thị lớn.
Chút

1

Một giải pháp khả thi, kết hợp với việc sử dụng jQuery.tmpl, là đẩy các mục vào cùng một lúc vào mảng có thể quan sát theo cách không đồng bộ, sử dụng 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
  }
}

Bằng cách này, khi bạn chỉ thêm một mục duy nhất tại một thời điểm, trình duyệt / knockout.js có thể dành thời gian để thao tác DOM cho phù hợp, mà trình duyệt không bị chặn hoàn toàn trong vài giây, do đó người dùng có thể cuộn danh sách đồng thời.


2
Điều này sẽ buộc N số bản cập nhật DOM sẽ dẫn đến tổng thời gian hiển thị lâu hơn nhiều so với thực hiện mọi thứ cùng một lúc.
Fredrik C

Điều đó tất nhiên là chính xác. Tuy nhiên, vấn đề là sự kết hợp của N là một con số lớn và việc đẩy một mục vào mảng dự án kích hoạt một lượng đáng kể các bản cập nhật hoặc tính toán DOM khác, có thể khiến trình duyệt đóng băng và đề nghị bạn giết tab. Bằng cách hết thời gian chờ, mỗi mục hoặc mỗi 10, 100 hoặc một số mục khác, trình duyệt sẽ vẫn phản hồi.
gnab

2
Tôi sẽ nói rằng đây là cách tiếp cận sai lầm trong trường hợp chung khi cập nhật tổng thể sẽ không đóng băng trình duyệt nhưng nó là thứ để sử dụng khi tất cả các lỗi khác. Đối với tôi, nó giống như một ứng dụng được viết không tốt, nơi các vấn đề về hiệu suất sẽ được giải quyết thay vì chỉ làm cho nó không bị đóng băng.
Fredrik C

1
Tất nhiên đó là cách tiếp cận sai trong trường hợp chung, không ai có thể không đồng ý với bạn về điều đó. Đây là một cuộc tấn công và là một bằng chứng về khái niệm để ngăn trình duyệt bị đóng băng nếu bạn cần thực hiện nhiều thao tác DOM. Tôi đã cần nó vài năm trước khi liệt kê một số bảng HTML lớn với một số liên kết trên mỗi ô, dẫn đến hàng nghìn liên kết được đánh giá, mỗi liên kết ảnh hưởng đến trạng thái của DOM. Chức năng này tạm thời cần thiết để xác minh tính đúng đắn của việc triển khai lại ứng dụng máy tính để bàn dựa trên Excel dưới dạng ứng dụng web. Sau đó, giải pháp này hoạt động hoàn hảo.
gnab

Nhận xét chủ yếu là để những người khác đọc để không cho rằng đây là cách ưa thích. Tôi cho rằng bạn biết bạn đang làm gì.
Fredrik C

1

Tôi đang thử nghiệm hiệu suất và có hai đóng góp mà tôi hy vọng có thể hữu ích.

Các thử nghiệm của tôi tập trung vào thời gian thao tác DOM. Vì vậy, trước khi đi sâu vào vấn đề này, bạn nên làm theo các điểm ở trên về việc đẩy vào một mảng JS trước khi tạo một mảng có thể quan sát được, v.v.

Nhưng nếu thời gian thao tác DOM vẫn đang cản trở bạn, thì điều này có thể giúp ích:


1: Một mẫu để bọc một con quay đang tải xung quanh kết xuất chậm, sau đó ẩn nó bằng cách sử dụng afterRender

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

Đây thực sự không phải là cách khắc phục sự cố hiệu suất, nhưng cho thấy rằng sự chậm trễ có lẽ là không thể tránh khỏi nếu bạn lặp lại hàng nghìn mục và nó sử dụng một mẫu mà bạn có thể đảm bảo rằng bạn có một con quay tải xuất hiện trước hoạt động KO dài, sau đó ẩn nó sau đó. Vì vậy, nó cải thiện UX, ít nhất.

Đảm bảo bạn có thể tải một spinner:

// 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)

Ẩn con quay:

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

mà kích hoạt:

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

2: Sử dụng liên kết html làm hack

Tôi nhớ lại một kỹ thuật cũ từ khi tôi làm việc trên set top box với Opera, xây dựng giao diện người dùng bằng thao tác DOM. Nó chậm kinh khủng, vì vậy giải pháp là lưu trữ các khối lớn HTML dưới dạng chuỗi và tải các chuỗi bằng cách đặt thuộc tính innerHTML.

Điều gì đó tương tự có thể đạt được bằng cách sử dụng liên kết html và một máy tính dẫn xuất HTML cho bảng dưới dạng một đoạn văn bản lớn, sau đó áp dụng nó trong một lần. Điều này khắc phục được sự cố hiệu suất, nhưng nhược điểm lớn là nó hạn chế nghiêm trọng những gì bạn có thể làm với ràng buộc bên trong mỗi hàng bảng.

Dưới đây là một thủ thuật cho thấy cách tiếp cận này, cùng với một hàm có thể được gọi từ bên trong các hàng của bảng để xóa một mục theo cách mơ hồ giống KO. Rõ ràng điều này không tốt bằng KO thích hợp, nhưng nếu bạn thực sự cần hiệu suất (ish) rực rỡ, thì đây là một giải pháp khả thi.

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


1

Nếu sử dụng IE, hãy thử đóng các công cụ dành cho nhà phát triển.

Việc mở các công cụ dành cho nhà phát triển trong IE sẽ làm chậm đáng kể hoạt động này. Tôi đang thêm ~ 1000 phần tử vào một mảng. Khi mở các công cụ dành cho nhà phát triển, quá trình này mất khoảng 10 giây và IE bị đóng băng trong khi nó đang diễn ra. Khi tôi đóng các công cụ dành cho nhà phát triển, hoạt động diễn ra ngay lập tức và tôi không thấy bị chậm trong IE.


0

Tôi cũng nhận thấy rằng công cụ mẫu Knockout js hoạt động chậm hơn trong IE, tôi đã thay thế nó bằng underscore.js, hoạt động nhanh hơn.


Làm thế nào bạn làm điều này xin vui lòng?
Stu Harper

@StuHarper Tôi đã nhập thư viện dấu gạch dưới và sau đó trong main.js, tôi đã làm theo các bước được mô tả trong phần tích hợp dấu gạch dưới của knockoutjs.com/documentation/template-binding.html
Marcello

Cải tiến này đã xảy ra với phiên bản IE nào?
bkwdesign

@bkwdesign Tôi đang sử dụng IE 10, 11.
Marcello
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.