Ionic 4 slow performance on user interaction

Can someone of the Ionic team (@mhartington, @brandyshea etc) give his / her opinion if Ionic components are slow as mentioned by @aabbcc1241 (using raw HTML)

It’s hard to tell from the gif what is even going on. We’d need a sample project to do more testing, but we have not seen any performance issues that @aabbcc1241 is mentioning.

The idea that “it is slow because it has too many level of shadow DOM” is really not even a valid point. Seems like it’s just pointing to something as the cause without providing any actual facts.

Anyways, provide a demo and we can take a look.

Thanks for your anwer, hopefull @aabbcc1241 can provide some code.

Using optimization build does make a difference, in my case ‘stencil build’ enable optimization by default, and ‘npm start’ runs ‘stencil build --dev --watch --serve’ which is not optimized.
However, it is still notifiable slow no the mobile device even using the optimization build’.

My bad for not including the sample code. It is quite long and I thought others are also facing this problem, so I didn’t include the code in the last post.

Here is the render method that makes the rightmost list of the gif demo:

renderPost(post: PostType) {
    const icon = fileService.toFileUrl(post.author?.avatar || assets.user_icon);
    return (
      <ion-card href={'#' + routes.post_detail(post)}>
        <ion-card-header class="ion-no-padding">
          <ion-row>
            <ion-col size="auto">
              {post.tags.map(tag => (
                <ion-chip
                  onClick={e => {
                    e.preventDefault();
                    this.search(tag);
                  }}
                >
                  {tag}
                </ion-chip>
              ))}
            </ion-col>
            <ion-col />
            <ion-col size="auto">
              <ion-text class="timestamp">
                Po: {formatDateTime(post.create_time)}
              </ion-text>
            </ion-col>
          </ion-row>
        </ion-card-header>
        <ion-card-content class="ion-no-padding">
          <ion-row>
            <ion-col size="auto">
              <h2>{post.title}</h2>
            </ion-col>
            <ion-col />
            <ion-col size="auto">
              <ion-buttons>
                <ReportIcon
                  report={() => this.reportPost(post)}
                  hide={() => this.hidePost(post)}
                  own={post.own}
                  recall-text={i18n.action['Recall Post']()}
                  recall={() => this.recallPost(post)}
                />
                <ShareIcon onClick={() => sharePost(post)} />
              </ion-buttons>
            </ion-col>
          </ion-row>
          {post.author ? (
            <ion-item>
              <ion-avatar slot="start">
                <img src={icon} alt={i18n.profile['User Avatar']} />
              </ion-avatar>
              <ion-text>
                {post.author?.nickname || i18n.default_user_name}
              </ion-text>
            </ion-item>
          ) : (
            undefined
          )}
        </ion-card-content>
        <ion-card-footer>
          <ion-row>
            <ion-col size="auto">
              {post.last_time ? (
                <ion-text class="timestamp">
                  CM: {formatDateTime(post.last_time)}
                </ion-text>
              ) : (
                []
              )}
            </ion-col>
            <ion-col />
            <ion-col size="auto">
              <ion-buttons>
                <ion-button color={post.has_my_comment ? 'primary' : 'medium'}>
                  {post.comments}
                  <ion-icon name="chatbubbles" />
                </ion-button>
                <VoteButtons
                  votes={post}
                  vote={vote => this.votePost({ vote, post_id: post.post_id })}
                  unVote={() => this.unVotePost(post)}
                  reversed={true}
                />
              </ion-buttons>
            </ion-col>
          </ion-row>
        </ion-card-footer>
      </ion-card>
    );
  }

And here is the render method that makes the list in the middle of the gif demo:

renderPostNew3(post: PostType, now: number) {
    return (
      <div class="card post ion-margin-half ion-padding-half-2">
        <div class="tags">
          {post.tags.map(tag => (
            <span>{tag}</span>
          ))}
        </div>
        <h2 class="title">{post.title}</h2>
        <div class="time ion-text-end">
          Po: {formatDateTime(post.timestamp)}
        </div>
        {post.author ? (
          <div>
            <ion-avatar>
              <img
                src={fileService.toFileUrl(
                  post.author?.avatar || assets.user_icon,
                )}
              />
            </ion-avatar>
            <span>{post.author?.nickname || i18n.default_user_name}</span>
          </div>
        ) : (
          []
        )}
        <div class="controls">
          <ion-buttons>
            <VoteButtons
              votes={post}
              vote={vote => this.votePost({ vote, post_id: post.post_id })}
              unVote={() => this.unVotePost(post)}
              reversed={false}
            />
            <ion-button color={post.has_my_comment ? 'primary' : 'medium'}>
              {post.comments}
              <ion-icon name="chatbubbles" />
            </ion-button>
          </ion-buttons>
          <ion-buttons class="right">
            <ReportIcon
              report={() => this.reportPost(post)}
              hide={() => this.hidePost(post)}
              own={post.own}
              recall-text={i18n.action['Recall Post']()}
              recall={() => this.recallPost(post)}
            />
            <ShareIcon onClick={() => sharePost(post)} />
          </ion-buttons>
        </div>
        <div
          class={'footer ' + (isLongDateTimeFormat(post, now) ? 'long' : '')}
        >
          {post.last_time ? (
            <span class="time">CM: {formatDateTime(post.last_time)}</span>
          ) : (
            <span />
          )}
          {/*<span class='time right'>Po: {formatDateTime(post.timestamp)}</span>*/}
        </div>
      </div>
    );
  }

I will make an executable repo if it helps the investigation.

Both methods used some functional components, which are implemented as following:

export const ShareIcon = (props: { onClick: () => void }) => {
  const onClick = (e: Event) => {
    e.preventDefault();
    props.onClick();
  };
  return (
    <ion-button onClick={onClick}>
      <ion-icon
        name="share"
        color="dark"
        size="small"
        class="ios"
        onClick={onClick}
      />
      <ion-icon
        name="share-social"
        color="dark"
        size="small"
        class="md"
        onClick={onClick}
      />
    </ion-button>
  );
};

export const ReportIcon = (props: Actions & { color?: string }) => {
  return (
    <ion-icon
      name="bug"
      color={props.color || 'dark'}
      size="small"
      onClick={e => {
        e.preventDefault();
        showReportMenu(props);
      }}
    />
  );
};

export const VoteButtons = (props: {
  reversed?: boolean;
  votes: VoteViewType;
  vote: (vote: VoteType) => void;
  unVote: () => void;
}) => {
  const res = [
    <ion-button
      color={props.votes.my_vote === 'up' ? 'primary' : 'medium'}
      onClick={(evt: Event) => {
        evt.preventDefault();
        props.votes.my_vote === 'up' ? props.unVote() : props.vote('up');
      }}
    >
      {props.votes.up_vote}
      <ion-icon name="thumbs-up" />
    </ion-button>,
    <ion-button
      color={props.votes.my_vote === 'down' ? 'primary' : 'medium'}
      onClick={(evt: Event) => {
        evt.preventDefault();
        props.votes.my_vote === 'down' ? props.unVote() : props.vote('down');
      }}
    >
      {props.votes.down_vote}
      <ion-icon name="thumbs-down" />
    </ion-button>,
  ];
  if (props.reversed) {
    res.reverse();
  }
  return res;
};

I’ve extracted the benchmark into a standalone test repo. It can run without relaying on server. Wish it can provide some insight to peers. https://github.com/beenotung/ionic-benchmark

I added one more benchmark repo to rely less on the stencil vdom. This version is operating on the dom directly. The loading of the ‘ionic heavy component’ list is speed up a bit, but still noticeably slower than the plain html/css version.

repo: https://github.com/beenotung/surplus-ionic-demo/tree/benchmark
demo: https://ionic-surplus-demo.surge.sh/

More details in https://github.com/ionic-team/stencil/issues/2058

Did you tag @mhartington or a other crew member so that they can have a look ?

I have also found Ionic 5 seems to lag on Android, I have spent a few months upgrading from Ionic 3 to Ionic 4 and then decided to go to straight to Ionic 5 whilst doing the upgrade.

One thing that I have found that seems to be noticeably slower is retrieving fairly large amounts of data from Ionic storage on Android, I have tested on the same device (Samsung Galaxy S8) my old Ionic 3 App against the new Ionic 5 version, all of the code is the same (apart from what had to be changed to perform the upgrade) but there is a noticeable couple of seconds delay on the new version when running the following code if there is quite a lot of data to return, both versions are using SQL lite:

this.storage.get('xxxxx')

1 Like

I upgraded to ionic 5 from ionic 3. Ionic 3 was much faster from navigation to slides swipes. I don’t want to disable animation.

1 Like

Could someone of the team give his / her opinion about above given github / replies ? @mhartington @brandyshea ? Thanks in advance :slight_smile:

Im looking over the demo…and I honestly don’t even know where to start :smile:

It seems overly complicated and written in a way that will always be slow.
The overall slowness of your demo is due to a mixture of things.

  • Overly complicated DOM structure
  • Unnecessary setTimeouts on connected callback
  • Unnecessarily splitting out rendering to different chunks
  • setting refresh as a class prop vs method.

The list can go on. In general though, it seems like everything in this demo was done to make things slow on purpose.

In contrast to some changes I made when I clone the project, I went for a simpler test case, and things were fast again.

Are you responding to my demo?
I appreciate your help. Let me try to answer your comments:

  • I admit the DOM structure is complex but I want to put information in a compact view instead of in the (space consuming) parahraph-style
  • The setTimeout for ion-refresher is to allow the user some time to read the content, it shouldn’t be the cause of the list rendering slow?
  • I split out the rendering into multiple functions because it is reused in several pages in the complete application
  • I use class prop instead of class method in some cases, to avoid creating new arrow functions in each render

In addition, it seems your demo render 20 items almost instantly. Would the loading time be significant if it render 50 items?
I know lazy loading can improve the situation, but when we compare ionic 5 to ionic 3 with same number of items to be rendered. It seems there is some room of improvement.

No the loading time would not be impacted if the items were bumped up to 50. The network requests and the rendering of the components is not the bottle neck. It’s the methods in your class that are slow.

To be frank. You wrote slow code. Nothing more to add.

1 Like

I would like to add that almost all Ionic components are slower than their “plain HTML” equivalent. Even on a small example like that What difference does ion-list make? .

I also had to change my “critical parts” to plain HTML because of that. My biggest problems are using multiple ion-sliders on the same page and ion-segments (when the content changes after pressing an ion-segment-button).

I suggest that you use Google Lighthouse to profile your layout and discover what is causing the slowness

I suppose it’s not related to the page itself but the underneath components. In my demo, it shows three lists of views side by side for comparasion, using “plain-text”, “plain-html-and-css”, and “quite-a-lot-of-ionic-compoennts” respectively. The method/class properties used are the same for the lists but they took significantly different latency to be rendered. i.e. my demo can be fast even using class properties for event listeners as long as it’s not using much ionic components.

In your demo, your implementation used ion-card, ion-card-header, and ion-card-content but didn’t use ion-list, ion-item, ion-row, ion-col. I can understand why you version is faster because it’s just simpler.

Is the class for us that, we should use less ionic components, and DIY with (simple) HTML and CSS unless totally necessary?

1 Like

The list, item, row, and col are not going to impact performance. Literally the code you wrote to fetch data, render nested items, etc etc is the slow part. Not the DOM structure.

I compared the ‘ionic-heavy’ version with ‘html’ version, having same amount of elements (same level of nested rendering) by replacing the list, item, row and col into div. It rendered mush faster, almost instantly. So it seems ionic components does impact the performance.

I’m not going to drag on this for long. I think I have express my observation. And you have suggested better way (to write cleaner / simpler code). I wish this conversation is helpful to other facing the same problem (having the UI render slowly).

TLDR; keep the layout (DOM structure) simple and flat if possible helps making it render faster.

And don’t treat ionic component just like a div. Although they looks similar on the tsx source code, the expanded runtime of ionic component has some cost, and the cost does add up if being mapped over a non trivial array of items.

1 Like

@aabbcc1241 Thanks for your comment its is very helpful. We use Ionic in our PWA app and currently doing some performance tuning as some areas of our app is sluggish. The reason we went with Ionic is at some point we do want to deploy IOS and Android apps.

One area where we are working on is a spreadsheet style table (11 columns x 25 rows) which currently uses ion-input, Material design version. We can’t use virtual scroll in this instance. We found by switching between the ion-input to boot-strap styling there was about a 10% performance improvement in load time. In fairness to Ionic this is understandable as there is more styling occurring so it’s only logical it would require more processing.

So far our conclusion is for 90% of what we use Ionic such as general input forms and displaying data Ionic works fine without any issues. But if you are doing very rich/complex forms that need to display alot of data best keeping it simple such as bootstrap or basic css styling (we just loaded the only a select few of the CSS class in our global.css rather downloading the whole library to our ionic app).

Also another interesting feedback we received is for more beginner users they found the bootstrap input more intuitive than the ionic material design input. Although in my opinion only that ionic designs looks more modern than bootstrap when it came to the crunch of it having the outline around the input box sub-consciously guided the more beginner users better around the form. This mattered more in desktop mode where there is more real-estate on the screen.