Ionic 4 slow performance on user interaction

Understood

I tried to look at performance analysis in DevTools, but that is a bit beyond my cup of tea to really understand what causes the scripting and style recalculations, that delay all the stuff…

Tom

Hi

out of curiosity I ran your test in vanilla JS only, and this is a bit faster, but requires 3.1 seconds to open, so approx 2 second less?

If you replace the ion-item/ion-label with a standard divs (no list), the modal shows in less then a second.

So that kind-of says proves your point assuming a bit of CSS to the DIV won’t make much difference (?)…

Here the Ionic4-angular test (AfterView hook):

Here the code on vanillaJS:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />

    <title>Using Ionic without any framework</title>

    <!-- Import the Ionic CSS -->
    <link href="https://unpkg.com/@ionic/core@latest/css/ionic.bundle.css" rel="stylesheet" />
    <!-- Import Ionic -->
    <script src="https://unpkg.com/@ionic/core@latest/dist/ionic.js"></script>
  </head>
  <body>
    <!-- We create a vanilla Javascript function to call the alert -->
    <script>
      customElements.define(
        "modal-page",
        class extends HTMLElement {
          connectedCallback() {
            var items = [];

            for (var i = 0; i < 2000; i++) {
              items[i] = "item " + i;
            }

            var moreHTML = "";
            for (var i = 0; i < 2000; i++) {
              moreHTML =
                moreHTML +
                `
                <ion-item>
                <ion-label>` +
                items[i] +
                `</ion-label>
                 </ion-item>`;
            }

            this.innerHTML =
              `
      <ion-header>
        <ion-toolbar>
          <ion-title>Modal Header</ion-title>
          <ion-buttons slot="primary">
            <ion-button onClick="dismissModal()">
              <ion-icon slot="icon-only" name="close"></ion-icon>
            </ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding">
        Modal Content
        <ion-list>` +
              moreHTML +
              `
        </ion-list>
      </ion-content>`;

            console.timeEnd("test");
          }
        }
      );

      hello = async function() {
        // initialize controller
        const modalController = document.querySelector("ion-modal-controller");

        // create the modal with the `modal-page` component
        const modalElement = await modalController.create({
          component: "modal-page"
        });

        console.time("test");

        // present the modal
        await modalElement.present();
      };
    </script>
    <ion-modal-controller></ion-modal-controller>

    <!-- We declare an Ionic app using the <ion-app/> element -->
    <ion-app>
      <!-- Cool thing, the Ionic CSS utilities could be used too -->
      <ion-content text-center>
        <h1>Basic usage</h1>
        Taken from
        https://medium.com/@david.dalbusco/using-ionic-without-any-frameworks-775dc757e5e8
        <!-- We add an ion-button with an onclick event -->
        <ion-button onclick="hello()">Click me</ion-button>
      </ion-content>
    </ion-app>
  </body>
</html>

And the diff with DIVs:

var moreHTML = "";
            for (var i = 0; i < 2000; i++) {
              moreHTML =
                moreHTML +
                `
                <div>
                <div>` +
                items[i] +
                `</div>
                 </div>`;
            }

            this.innerHTML =
              `
      <ion-header>
        <ion-toolbar>
          <ion-title>Modal Header</ion-title>
          <ion-buttons slot="primary">
            <ion-button onClick="dismissModal()">
              <ion-icon slot="icon-only" name="close"></ion-icon>
            </ion-button>
          </ion-buttons>
        </ion-toolbar>
      </ion-header>
      <ion-content class="ion-padding">
        Modal Content
` +
              moreHTML +
              `

      </ion-content>`;

Running in Angular7 project with DIV yields same results as javascript DIV. Adding Ionic through CUSTOM_SCHEMAS and CDN is actually a bit faster as “normal” Ionic embedding:

Tom

I have no doubt that with the css on can reproduce the result a lot and a lot more fluid. the proof ionic version 3 that does not use the shadow dom. It is very fast and makes it identical. it’s clearly a performance problem on all ionic component 4 no solution is brought by the ionic team. if it does not correct this problem. ionic will no longer be used by developers. I like ionic but if the performances are not good. I would not have the choice to go to another framwork.

For now I will continue with Ionic and where higher performance is needed I will go for DIV+CSS and use Ionic’s CSS instead of webcomponents. If the need arises, of course.

The 2000 items thing, I had the same problem and I just used the virtual-scroll and it did render with no lag my list (it was in the hundreds though). But that would mean that you will have to rewrite how it shows on the screen .

I think you can’t rely on the results of a comparison between <div/> and <ion-item/> or any web components as these are more than just an html elements. They contain styling (so their own css which has to be evaluated and rendered), dynamic code which is performed at different lifecycles (componentDidLoad etc.) and have dynamic attributes and properties which need to be evaluated too (disabled or activated etc.) respectively they are more than just “nacked” <div/>. Same you can’t really compare <div/> with a complex Angular Components neither.

1 Like

I do not only compare to an html element but also to an angular component but with ionic 3. and the loss of performance between the old ionic component 3 and the new ionic component 4 is quite important. of course the solution exists with virtual scroll and surely other way to optimize. but why need to optimize something that can work very well and be very fluid without going through virtual scrolls or anything else. there is a real problem of performance with ionic 4 components. and I repeat the problem undoubtedly comes from the shadow dom. my tests prove it clearly.

1 Like

I have Same problem did you solved it? please share with us

I agree and I have rebuilt the application using Ionic 3 even though I finished it with ionic 4

1 Like

I was having really poor performance on my Android 9 device, what really improved the UX in my case was:

  • use of *ngIf to reduce the dom size as much as possible
  • improve image size/compression (with tinypng or similar)
  • disable ionic animations with IonicModule.forRoot({animated: false})

So the bottom line IMO is keep the dom as small as possible and avoid animations and/or big images.

1 Like

or we can go back to ionic 3/2/1

Shadow DOM may not be the root cause, I test with modified @ionic/core, turned all shadow flag into scoped flag, still slow rendering. But if I change all ion-* into app-* in my page (effectively using raw html, without calling ionic web components), it load very fast (like others test with div).

So the slow down seems comes from the complexity in the ionic components, regardless of shadow dom or not.

My workaround is to implement the layout using plain html/css for critical parts, instead of using ionic components.

Below stencil (not angular) demo has 49 items on each column. The data is inserted 33ms after connectedCallback() is called. It shows the significant different in rendering latency.

Left used: plain text with table

Middle used: flex, ion-buttons, ion-button, ion-icon, ion-avatar

Right used: ion-card, ion-card-header, ion-card-content, ion-card-footer, ion-row, ion-col, ion-item, ion-avatar, ion-buttons, ion-button, ion-icon, ion-text, ion-chip

The “ionic version” is slow because it has too many level of shadow DOM. I tried to modify the @ionic/core code base, turned most shadow component into scoped component but it doesn’t help much on the performance.

Please use optimize options when ionic build.
This makes deploy js and css as minimized.
And then UI components will be so fast both on browser and native device.

ionic build --prod --aot --minifyjs --minifycss --optimizejs

This simple optimized options make me and my clients happy for fast loading and moving of ionic app.

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