Lack of DOM caching killing performance on slower devices


#1

We have been spending a considerable amount of time trying to improve the performance of view transitions (going from list to detail views and back) on older devices such as iPhone 4 to reduce the delay in transitions now we have got beyond a trivial application.

There have been some minor improvements but I am starting to come to the conclusion that the way angular/ionic default to recreating the DOM when transitioning views is a really inefficient mechanism for mobile devices screens where you can transition frequently between screens and don’t expect ~1-2 second delays.

There are many things we have tried such as:

  • Optimising the use of ng-repeats (and therefore $watch statements)
  • Wrapping below the fold content in an ng-if’s which are triggered on $viewContentLoaded to defer rendering of those components.
  • Reducing the amount of content on the master screens. (not easy to convince the client about).

It feels like I am missing something obvious but looking at the code of ionic you can see the DOM of the previous view being discarded and recreated each time.

Searching around for a solution to this from an angular perspective generally keeps telling you to optimise ng-repeats/watch statements or write your own ng-view/ion-view directive implementation to cache the previous view DOM and replace it back on transition. This approach I’m sure is possible but likely to lead to all sorts of future compatibility issues with ionic and seems like something that would be best supported in the framework (angular or ionic).

Does anyone have a other advice or how to deal with this or is there anyone from ionic who can clarify whether there are plans to address this in the framework?

Thanks Dan.


How to optimize the performance of iphone 4 and 4S?
#2

We identified this problem, too.

Even with small lists it takes round about 1.5sec to go back from detail view to the list view.

Greets, Bengt


#3

Good to have someone confirm this after extensive experimenting. We’ve had exactly the same experience. It would be nice to have this acknowledged by the Ionic team, but it seems their approach on this one may be to just wait it out until the slower phones go away? Fair enough for a product in beta perhaps, the other aspects of ionic rock.

We ended up implementing our own transitions.


#4

Did your own implementation take the approach of caching the dom?

It will be quite a while before the slower phones go away, Apple is still selling the 4S. Also with the massive range of Android device capabilities lower spec’ed new devices will still suffer the same issue.

If there is no other alternative then I will look at the feasibility of custom view directives but it’s not something I am keen on having to add outside the framework.

it’s a shame because other than that ionic is great and works fine on iPhone 5+ type devices.


#5

No we didn’t do anything tricky, just standard angular animate and removed all ionic scrolling from the views.


#6

I’m also experiencing this on the iPhone 4. We’re not the only ones, there is a newly created, open issue about this on github. I wonder if the ionic team addresses this.

https://github.com/driftyco/ionic/issues/1434


#7

thanks for the link to the issue in github… +1’d


#8

In terms of performance, why does a simple angular animate solution beat ionic by so much? That’s the question I’d be trying to answer.


#9

This sounds intriguing…What would that simple angular animate solution look like?


#10

So we pulled out ionic, put the tab bar in a div, put our content in a separate div, put the whole thing in another div and used angular animate to do the slide transitions. No delay.


#11

We are working on a solution.

It requires a big refactor of most of our navigation code, so it is taking awhile.

But it will be better in every way (in addition to developer control over our navigation).


#12

Awesome, that’s good to know. Thanks for the update and look forward to it’s release!


#13

I’d like to bring my little experience on the subject.
I’m developing an app with 3 tabs, and 2 of these are quite heavy (a list with few hundred items and a leaflet map).
In my test I tried to override the ionTab directive to not delete scope and dom element of each tab when unselected, but instead hide it (well, actually setting a lower z-index).
Here is the code, you can try it by putting it in a file, including in a <script> tag in your index.html and set ionTabReplace module as a dependency of your main app module (like ionic). The .config(…) take care of removing the default ionTab directive

var extend = angular.extend,
  forEach = angular.forEach,
  isDefined = angular.isDefined,
  isString = angular.isString,
  jqLite = angular.element;
angular.module('ionTabReplace',[])
.config(function($provide){
    $provide.decorator('ionTabDirective', ['$delegate', function($delegate) {
        console.log($delegate);
        $delegate.shift();
        return $delegate;
    }]);
})
.directive('ionTab', [
  '$rootScope',
  '$animate',
  '$ionicBind',
  '$compile',
function($rootScope, $animate, $ionicBind, $compile) {

  //Returns ' key="value"' if value exists
  function attrStr(k,v) {
    return angular.isDefined(v) ? ' ' + k + '="' + v + '"' : '';
  }
  return {
    restrict: 'E',
    require: ['^ionTabs', 'ionTab'],
    replace: true,
    controller: '$ionicTab',
    scope: true,
    compile: function(element, attr) {

      //We create the tabNavTemplate in the compile phase so that the
      //attributes we pass down won't be interpolated yet - we want
      //to pass down the 'raw' versions of the attributes
      var tabNavTemplate = '<ion-tab-nav' +
        attrStr('ng-click', attr.ngClick) +
        attrStr('title', '*'+attr.title) +
        attrStr('icon', attr.icon) +
        attrStr('icon-on', attr.iconOn) +
        attrStr('icon-off', attr.iconOff) +
        attrStr('badge', attr.badge) +
        attrStr('badge-style', attr.badgeStyle) +
        '></ion-tab-nav>';

      //Remove the contents of the element so we can compile them later, if tab is selected
      //We don't use regular transclusion because it breaks element inheritance
      var tabContent = jqLite('<div class="pane">')
        .append( element.contents().remove() );

      return function link($scope, $element, $attr, ctrls) {
        var childScope;
        var childElement;
        var tabsCtrl = ctrls[0];
        var tabCtrl = ctrls[1];

        var navView = tabContent[0].querySelector('ion-nav-view') ||
          tabContent[0].querySelector('data-ion-nav-view');
        var navViewName = navView && navView.getAttribute('name');

        $ionicBind($scope, $attr, {
          animate: '=',
          onSelect: '&',
          onDeselect: '&',
          title: '@',
          uiSref: '@',
          href: '@',
        });

        tabsCtrl.add($scope);
        $scope.$on('$destroy', function() {
          tabsCtrl.remove($scope);
          tabNavElement.isolateScope().$destroy();
          tabNavElement.remove();
        });

        //Remove title attribute so browser-tooltip does not apear
        $element[0].removeAttribute('title');

        if (navViewName) {
          tabCtrl.navViewName = navViewName;
        }
        $scope.$on('$stateChangeSuccess', selectIfMatchesState);
        selectIfMatchesState();
        function selectIfMatchesState() {
          if (tabCtrl.tabMatchesState()) {
            tabsCtrl.select($scope);
          }
        }

        var tabNavElement = jqLite(tabNavTemplate);
        tabNavElement.data('$ionTabsController', tabsCtrl);
        tabNavElement.data('$ionTabController', tabCtrl);
        tabsCtrl.$tabsElement.append($compile(tabNavElement)($scope));

        //Create child scope and element
        childScope = $scope.$new();
        childElement = tabContent.clone();
        $animate.enter(childElement, tabsCtrl.$element);
        $compile(childElement)(childScope);
        if (!$scope.$tabSelected) //Hide it if not selected
          jqLite(childElement).css('z-index',-1);
        
        $scope.$watch('$tabSelected', function(value) {
          if (value) { //Show
            jqLite(childElement).css('z-index',1);
          } else { //Hide
            jqLite(childElement).css('z-index',-1);
          }
        });

      };
    }
  };
}]);

I didn’t do lot of test, but if the app consist only of some tab I think it’ll be quite faster when you switch between tabs, but it will take some more time in loading dom at the beginning.


#14

Hi Andy, can you share progress on this one? We’re starting development of a new App and would really appreciate some input, which will undoubtedly effect our design. Cheers.


#15

+1 – any update on this from several months ago?

It would be great if the tabs just left the content in each in place. For example if the user has typed half a message into a textarea on tab1, then clicks on tab2, when they click on tab1 again things should be exactly the same – the text and cursor position in the box should be there. Re-rendering destroys all of that and takes too long. Essentially just showing the current tab and hiding the rest is what we want.

That way there would be zero lag switching between tabs, because there there is no need to re-ajax or re-manipulate the dom.