Ionic Lazy Loading: Cut Your App's Startup Cost Without Rewriting It
Ionic's lazy loading splits each page into its own bundle so users only download code when they navigate to it. Here's how to convert routes, verify the split, and avoid the singleton-service trap.
13 Apr 2026, 16:48 UTC

Your Ionic app's first screen is a login page, yet the browser downloads the code for every settings screen, modal, and admin panel before it can paint anything. That is the default cost of an eagerly loaded Angular bundle, and it grows linearly with every page you add. Lazy loading fixes this by splitting each page into its own chunk that ships only when the user actually navigates to it.
What lazy loading actually changes
In a standard Ionic Angular app, all page modules are imported into the root module, so the bundler merges them into one main JavaScript file. On a mid-size app this file can easily exceed several megabytes, and every byte must be downloaded, parsed, and executed before the first interaction.
With lazy loading, the router is told where a page's code lives instead of importing it directly. The bundler (Webpack under the Angular CLI, or esbuild in newer versions) then emits a separate chunk file per page. At runtime, the Ionic Router fetches that chunk the first time the route activates. The initial bundle shrinks to the shell: the framework, shared components, and whatever your first screen needs.
Converting a route to loadChildren
The change is small and lives in your routing module. Assume an Ionic Angular project generated with a recent CLI (Ionic 6/7, Angular 15+). An eager route looks like this:
// app-routing.module.ts — eager (everything in the main bundle)
import { SettingsPageModule } from './settings/settings.module';
const routes: Routes = [
{ path: 'settings', loadChildren: () => import('./settings/settings.module').then(m => m.SettingsPageModule) }
];Note that even the "eager" comment is slightly misleading here: the modern loadChildren with a dynamic import() is the lazy form. The genuinely eager pattern imports the module at the top of the file and references it via component: or adds it to the root module's imports. If you generate pages with ionic generate page settings, the CLI already wires up the lazy loadChildren route for you — the common mistake is later adding the page module to AppModule imports, which silently pulls it back into the main bundle.
Run this in your project root with the Ionic CLI installed. No elevated permissions needed; it only modifies source files.
Verifying the split actually happened
Do not trust the config — check the build output. From the project root:
ionic build --prodThen inspect the output directory (www/ for a browser build). You should see files named like settings-settings-module.[hash].js alongside main.[hash].js. If every page module name appears as its own chunk, the split worked. If your settings code is inside main.[hash].js (search the file for a string unique to that page), something is importing it eagerly — usually a stray import in AppModule or a shared module.
For runtime confirmation, serve the build, open Chrome DevTools' Network tab filtered to JS, and navigate to the lazy route. The chunk should appear in the list only at the moment of navigation, not on initial load. To quantify the win, use the Performance panel: record a cold load before and after the change and compare the scripting and total blocking time.
Preloading: the middle ground
Pure lazy loading trades startup speed for a navigation delay: the first tap on a secondary page waits for a network fetch. Angular's router lets you soften this with a preloading strategy:
RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })PreloadAllModules keeps the initial bundle small but fetches remaining chunks in the background after first paint, so later navigations feel instant. The trade-off is bandwidth: on metered connections you are downloading code the user may never open. For large apps, a custom strategy that preloads only high-traffic routes is worth the extra code.
Two limitations worth knowing before you commit
Singleton services can silently duplicate. If a service is provided in a lazy-loaded page module rather than with providedIn: 'root', each lazy chunk that provides it gets its own instance. This breaks caches, auth state, and event buses in confusing ways. Rule of thumb: app-wide services use @Injectable({ providedIn: 'root' }); only services truly scoped to one page go in that page's module. You can verify by logging in the service constructor and navigating around — the log should fire exactly once.
Chunk count becomes a network concern. An app with forty lazy pages issues forty potential requests. On HTTP/1.1 or high-latency mobile networks this can hurt more than a big bundle. HTTP/2 server support mitigates it, and grouping related pages into shared feature modules (one chunk per feature area rather than per page) is a pragmatic compromise.
Where to start
Pick your largest non-critical page — settings and account screens are classic candidates — convert its route to loadChildren, run a production build, and confirm the chunk appears in www/ and in the Network tab only on navigation. Measure cold-start scripting time before and after. If the numbers move, roll the pattern out feature by feature, keeping singleton services in the root injector as you go.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.