Tăng tốc độ tối đa cho ứng dụng viết bằng AngularJS
Gắn bó với AngularJS cũng khoảng 2 năm rồi. Thật sự nhìn lại nhưng app mình làm với nó vẫn chưa gọi là “good” về chất lượng (performance). Có thể do: code sh*t, hoặc do cách tổ chức (structure) chưa tốt, hoặc cũng có thể do dữ liệu quá lớn,… Đến lúc phải nhìn lại xem ...
Gắn bó với AngularJS cũng khoảng 2 năm rồi. Thật sự nhìn lại nhưng app mình làm với nó vẫn chưa gọi là “good” về chất lượng (performance). Có thể do: code sh*t, hoặc do cách tổ chức (structure) chưa tốt, hoặc cũng có thể do dữ liệu quá lớn,… Đến lúc phải nhìn lại xem nó đang bị ảnh hưởng do yếu tố nào? Làm sao để cải thiện và làm thế nào để cải thiện.
Ở phạm vi bài viết này, mình không bàn về vấn đề code sh*t hay không và cũng không bàn về cách tổ chức code sao cho tốt. Vì đó là việc tùy vào đẳng cấp của mỗi người, tùy vào dự án, tùy vào tâm lý xã hội tình cảm,… Và việc thực hiện nó cần cả team và cần có một TechLead giỏi để giúp thực hiện tốt. Ở đây mình chỉ tập trung nói việc hiểu và sử dụng một công nghệ như thế nào cho tốt thôi. Và chủ đề của bài này là tập trung vào AngularJS.
Theo như ng-conf team định nghĩa thế nào là “chậm” trong ứng dụng dùng AngularJS? thì họ có nêu ra một số vấn đề như sau:
- $apply > 25ms
- Click handler > 100ms
- Show a new page > 1s
- If > 10s => User will give up
- 200ms or less is ideal
Mình sẽ chia sẽ một số cách để có thể tăng thêm tốc độ cho ứng dụng web dùng AngularJS và Javascript mà mình đã tìm hiểu và thử nghiệm.
- Các file js, css nên được nén lại (minified): việc minify js (javascript) các file css (stylesheet) sẽ giúp bạn có được các file js và css nhẹ hơn, tải lên nhanh hơn và bảo mật hơn.
- Tắt chế độ debug của AngularJs
Trước khi release package bạn nên tắt chế độ này để có thể tăng hiệu suất ứng dụng.Tại sao vậy? Vì, mặc định AngularJS tự động thêm các siêu dữ liệu vào trong DOM để hỗ trợ các công cụ phát triển và debug.Vậy siêu dữ liệu đó là những gì? Khi bạn debug ứng dụng bạn sẽ thấy các class như ‘ng-bind’, ‘ng-scope’, ‘ng-isolate-scope’ được gắn vào DOM.Làm sào tắt chế độ này bây giờ? Đây là cách dùng:app.config(['$compileProvider', function($compileProvider){ $compileProvider.debugInfoEnabled(false); }]);12345app.config(['$compileProvider', function($compileProvider){$compileProvider.debugInfoEnabled(false);}]);
Ngoài ra, khi đã tắt chế độ debug rồi bạn vẫn có thể load lại chế độ này tạm thời để debug bằng cách gọi hàm angular.reloadWithDebugInfo() bên trong console tool - Sử dụng phương thức useApplyAsync() cho các đối tượng $http
Nếu như ứng dụng của bạn có rất nhiều $http request cùng lúc để lấy dữ liệu về. Thì mỗi $http response nó sẽ gọi digest() để cập nhật lại DOM.
Trong các ứng dụng lớn, khi khởi động ứng dụng bạn cần lấy rất nhiều dữ liệu về cùng một lúc rồi mới hiện lên giao diện. thì nên sử dụng useApplyAsync() để đồng bộ lại tất cả các response này và gọi digest() một lần thôi. Trong các ứng dụng nhỏ (có ít request cùng lúc) bạn sẽ không thấy được sự khác biệt khi dùng phương thức này.
Vậy làm sao dùng nó trong ứng dụng đây? Xem ví dụ sau:app.config(function($httpProvider){ $httpProvider.useApplyAsync(1000); //true });12345app.config(function($httpProvider){$httpProvider.useApplyAsync(1000); //true}); - Sử dụng $templateCache
Nếu ứng dụng của bạn có nhiều templates vậy thì server cần phải tải tất cả các templates này về trước khi hiện lên cho người dùng thấy. Sử dụng $templateCache sẽ giúp hạn chế việc tải lại các templates này. Nghĩa là template chỉ được tải 1 lần sau đó AngularJs lưu vào cache và nhưng request sau đó sẽ được gọi từ cache để load template.
Oh, sử dụng thế nào? Có nhiều cách để chúng ta dùng template. Cách để dễ bảo trì và trực quan nhất là viết template vào 1 file html riêng rồi dùng $TemplateCache service put nó vào. Xem ví dụ sau:app.run(function($templateCache){ $templateCache.put('templateId.html', 'This is my content'); //Or get a template cache $templateCache.get('path/templateId.html'); });1234567app.run(function($templateCache){$templateCache.put('templateId.html', 'This is my content');//Or get a template cache$templateCache.get('path/templateId.html');}); - Tránh dùng quá nhiều $watch
Phần này khá quan trọng, nó ảnh hưởng đến performance rất nhiều. Đầu tiên chúng ta cần hiểu được $watch được tạo ra khi nào? watcher được tạo ra khi:
– Dùng binding {{ item }}
– Dùng biến scope: scope: { bar: ‘=’}
– Dùng Filter {{ value | myFilter }}
– Dùng các directive: ng-show, ng-hide, ng-repeat, ng-change, ng-model, ng-click,….
– Dùng $http event
– Dùng $q để resolve promise
– Dùng $timeout, $interval
– Dùng method $scope.$watch()Vì sao vậy? vì tất cả những thứ này sẽ gọi xuống $digest() để cập nhật lại DOM. Chính vì thể khi bạn cũng cần hạn chế việc dùng $apply() vì nó cũng sẽ gọi $digest(), hoặc khi gọi trực tiếp $digest().Bạn cần hiểu cách làm việc của AngularJs như thế này: $watch() sẽ làm thêm cái đó rồi gọi $apply(). Trong $apply có thể làm thêm việc gì đó rồi gọi $digest(). $digest sẽ kiểm tra xem đối tượng được watch có thay đổi gì không, nếu có sẽ cập nhật DOM
Tóm lại, có nhiều watchers sẽ tạo ra nhiều digest cycleẢnh từ angular.orgVậy làm bây giờ? ứng dụng của tôi cần phải dùng tất cả những thứ ở trên, không lẽ không dùng.
Ở đây chúng ta hiểu được khi nào tạo ra watcher rồi, chúng ta chỉ hạn chế tạo ra quá nhiều watcher không cần thiết. Cho nên, tùy trường hợp, bạn cần phải xem xét để có thể áp dụng đúng.Vậy giờ làm sao để hạn chế việc tạo ra watcher?
OK, tôi sẽ hướng dẫn cho bạn làm thế nào. Nhưng bạn cần xem xét trường hợp nào nên dùng và không nên dùng để tránh ảnh hương đến logic của ứng dụng.a) Chỉ sử dụng two-way binding (binding 2 chiều) khi nào dữ liệu của bạn cần thay đổi model. Trong AngularJs để dùng two-way binding bạn dùng ng-model. Bạn có thể sử dụng {{ item }} hoặc ng-binding nếu dữ liệu không cần thay đổi model của bạn.- Sử dụng ngModelOptions để hạn chế gọi digest() liên tục mà cần chờ một khoảng thời gian nào đó rồi mới gọi digest (trường hợp user input). Cách dùng:<span ng-model="user.username" ng-model-options="{ update: 'blur'}"></span> <span ng-model="search.keyword" ng-model-options="{ debounce: 500}"></span> <span ng-model="search.keyword" ng-model-options="{ debounce: {default: 500, blur: 0}}"></span>12345<span ng-model="user.username" ng-model-options="{ update: 'blur'}"></span><span ng-model="search.keyword" ng-model-options="{ debounce: 500}"></span><span ng-model="search.keyword" ng-model-options="{ debounce: {default: 500, blur: 0}}"></span>b) Sử dùng bind-one (binding 1 lần) nếu giá trị không cần thay đổi. Từ AngularJs 1.3 trơ đi đã hỗ trợ cho chúng ta cú pháp bind-one như sau:
{{ :: item }}, chỉ cần thêm dấu :: trước tên biến. Khi dùng bind-one sẽ không tạo ra watcher.
c) Dùng ng-if, ng-switch thay cho ng-show/ng-hide. Vì sao vậy? vì ng-if, ng-switch rẽ remove element html nếu false nên các watcher cũng sẽ bị remove theo. Còn ng-show/ng-hide element html luôn luôn tồn tại và nò dùng CSS để ẩn/ hiện trên giao diện nên watcher cho những element này vẫn tồn tại. Đây là ví dụ dùng ng-switch:
d) Nên giảm thiểu sử dụng ng-repeat. Giảm thiểu là sao? nghĩa là nếu bạn có 1000 items cần show lên GUI bạn nên dùng paging hoặc lazy-load để show từng phần dữ liệu. Có một vấn đề là watcher được tao ra rất nhiếu khi dùng ng-repeat
Bạn cũng có thể kết hợp bind-one với ng-repeat hạn chế việc tao ra watcher. Như sau:AngualrJs hỗ trợ thêm cú pháp track by khi dùng với ng-repeat như sau:
Với việc dùng track by ng-repeat sẽ không cần $destroy và tạo lại DOM một cách không cần thiết.
- Sử dụng $destroy
Nên $destroy: $timeout, $interval, $watch, $on, các events của element DOM: click, change,…var timeout = $timeout(function(){ //do something }, 500); var interval = $interval(function(){ //do something }, 500); var watchfunc = $scope.$watch("item", function(newVal, oldVal){ if(newVal !== oldVal){ //do something watchfunc(); //remove watch after done } }); $scope.$on('$destroy', function () { $timeout.cancel(timeout); $interval.cancel(interval); watchfunc(); // apply the same for $scope.on }); scope.$on('$destroy', function () { angular.element(window).off('click', windowClick); })1234567891011121314151617181920212223242526var timeout = $timeout(function(){//do something}, 500);var interval = $interval(function(){//do something}, 500);var watchfunc = $scope.$watch("item", function(newVal, oldVal){if(newVal !== oldVal){//do somethingwatchfunc(); //remove watch after done}});$scope.$on('$destroy', function () {$timeout.cancel(timeout);$interval.cancel(interval);watchfunc(); // apply the same for $scope.on});scope.$on('$destroy', function () {angular.element(window).off('click', windowClick);})
Hy vọng với những gì mình đã trình bày sẽ giúp các bạn có inscrease performance cho ứng dụng của mình tốt hơn khi dùng với AngularJs. Có thế bạn sẽ không thể áp dụng tất cả các cách mình đã nói. Vậy tùy trường hợp mà bạn nên xem xét dùng thế nào cho hợp lý nhé.