After spending the better part of three years building and maintaining production Ionic applications at Tawk.to, I have picked up a handful of performance patterns that have made a real, measurable difference. Ionic has come a long way, but hybrid apps still carry the burden of running inside a WebView, which means every kilobyte and every unnecessary re-render matters more than it does on the open web. In this post I want to share the techniques that have given me the biggest wins.

Lazy Loading Done Right

Lazy loading is table stakes for any modern Angular app, but I have seen many Ionic projects that only lazy-load at the page level. The real gains come from lazy-loading individual components and, more importantly, heavy third-party libraries. If you only need a charting library on one screen, there is no reason for it to live in your main bundle.

// app-routing.module.ts
const routes: Routes = [
  {
    path: 'dashboard',
    loadChildren: () =>
      import('./pages/dashboard/dashboard.module')
        .then(m => m.DashboardPageModule)
  },
  {
    path: 'analytics',
    loadChildren: () =>
      import('./pages/analytics/analytics.module')
        .then(m => m.AnalyticsPageModule)
  }
];

The pattern above is standard, but the next step is what most people miss. Inside the analytics module, do not import a heavy charting library at the top of the file. Instead, dynamically import it only when the component initializes:

// analytics.page.ts
async ngOnInit() {
  const { Chart } = await import('chart.js/auto');
  this.chart = new Chart(this.canvasRef.nativeElement, {
    type: 'line',
    data: this.chartData
  });
}

This alone shaved about 120 KB off our initial bundle in one project. Multiply that across a few heavy dependencies and you start to see serious improvements in time-to-interactive on mid-range Android devices.

Virtual Scrolling for Long Lists

If your app displays any kind of list with more than a few dozen items, virtual scrolling is non-negotiable. Ionic ships with ion-virtual-scroll (now deprecated in favor of integrating with CDK virtual scroll), but the principle is the same: only render the DOM nodes that are currently visible in the viewport.

<cdk-virtual-scroll-viewport itemSize="72" class="ion-content-scroll-host">
  <ion-item *cdkVirtualFor="let message of messages">
    <ion-avatar slot="start">
      <img [src]="message.avatar" loading="lazy" />
    </ion-avatar>
    <ion-label>
      <h3>{{ message.sender }}</h3>
      <p>{{ message.preview }}</p>
    </ion-label>
  </ion-item>
</cdk-virtual-scroll-viewport>

One thing that caught me off guard early on is that virtual scrolling and Ionic's pull-to-refresh do not always play nicely together out of the box. You may need to set up a custom scroll strategy or manually wire the scroll container to ion-content. It is worth the effort though, because rendering 500 chat messages without virtual scrolling will absolutely destroy frame rates on lower-end hardware.

Change Detection Strategy

Angular's default change detection checks every component in the tree on every event. In an Ionic app running inside a WebView, this is one of the easiest sources of jank. Switching components to OnPush change detection is one of the highest-leverage changes you can make:

@Component({
  selector: 'app-message-item',
  templateUrl: './message-item.component.html',
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class MessageItemComponent {
  @Input() message: Message;
}

With OnPush, Angular only checks the component when its inputs change by reference or when an event originates from the component itself. This pairs beautifully with immutable data patterns. In our chat application, moving the message list components to OnPush cut unnecessary change detection cycles by roughly 60 percent during rapid message delivery.

Bundle Optimization and Tree Shaking

The Angular CLI does a decent job of tree-shaking, but there are a few things you can do to help it along. First, always check your bundle with source-map-explorer:

npm install -g source-map-explorer
ng build --source-map
source-map-explorer www/main.*.js

You will almost certainly find surprises. Common culprits include moment.js (replace it with date-fns or dayjs), lodash (use lodash-es and import individual functions), and icon libraries where you import the entire set instead of individual icons. In one audit, switching from the full Ionicons import to individual SVG imports saved us over 200 KB of gzipped JavaScript.

Image and Asset Optimization

This one seems obvious, but I still see it overlooked. Every image in your Ionic app should use the loading="lazy" attribute at minimum. Beyond that, consider using WebP format with a fallback, and serve responsive image sizes using srcset. For avatars and thumbnails, aggressively resize on the server side rather than shipping full-resolution images to the client.

Combining all of these techniques, we managed to bring our largest Ionic app's time-to-interactive from over 4 seconds down to about 1.8 seconds on a Moto G Power. Performance is not a one-time fix; it is something you have to measure and maintain with every release. But these patterns have been the foundation of fast Ionic apps for me, and I hope they help you as well.