Ionic 3's lazy loading is bad for large apps *it's being worked on!*


#1

Sorry all, this one is a complainy post instead of the usual tutorial. Please know, as I put as the first line in the blog post, I know Ionic 3 is just in beta, it can still change and I hope that it does. It really feels like an architecture flaw more than a simple beta bug though, so I have some real concerns.

EDIT: After further discussion with Ionic team, it looks like this is being addressed! It may not be ready until v3.1, but that is okay, I just want to know the option will be there so that using Angular modules as intended will be supported. Now that I know that it is, I’m quite happy. When the feature is released I’ll do a new writeup on how to take advantage of it.

http://www.roblouie.com/article/456/ionic-3s-lazy-loading-is-bad/


Modular architecture of Ionic 2 application
Considering deleting all page module files
#2

I can’t really speak to the issue of project layout, but a couple of specific points:

why can’t those modules provide services that are scoped to that module

I think they can. If you declare a provider in a page’s module, it should be page-scoped.

Why can’t those lazily loaded modules have multiple components that can be pushed onto a nav stack?

I also suspect this is possible (although I haven’t tried it). The way I’m thinking of things at this point is that there are two ways to interact with the navigation system: feed it a string and it will lazily load a page module, or feed it a constructor/class and take responsibility for having that class be loaded. So I would think this would do what you’re looking for:

@IonicPage()
@Component()
export class DaihyoPage {
  // this is the main entry point for our module

  pushSubPage(): void {
    this.nav.push(SubPage); // not "SubPage"
  }
}

@Component()
export class SubPage {
  // no @IonicPage on this one
}

@NgModule({
  declarations: [DaihyoPage, SubPage],
  // not sure if entryComponents needed
})
export class AwesomeModule {
}

class OtherPage {
  doAwesomeStuff(): void {
    this.nav.push("DaihyoPage"); // trigger lazy loading
  }
}

#3

Regarding providing a service that is page scoped, I believe you are correct. What I’m specifically asking for though is for a single module with multiple pages and multiple services. In that case the service(s) wouldn’t be page scoped, but module scoped, and be accessible by all pages in that module. But only by pages in that module. If you check out this simple vanilla Angular app you can see what I mean: https://github.com/roblouie/lazy-loaded-module

Regarding your second example, I believe that is exactly what I tried (as my second attempt), but I’m met with Runtime Error Uncaught (in promise): Error: No component factory found for SecondPageComponent. Did you add it to @NgModule.entryComponents?

Even after adding it to the entry component of the lazily loaded module.

As I mentioned in my edit the Ionic team reached out to me to talk about it, which I greatly appreciate. It really shows they care about their community and I can’t express enough how much I appreciate that. It sounds like it’s possible my use case can be supported. If so I will be very, very happy. :slight_smile:

It doesn’t need to be the default or anything for those who just need simpler apps with a few pages and services, but I only want it to be possible (as it is in regular Angular) so that those of use building larger scale apps have that flexibility.


#4

Maybe rewrite the title further to “Ionic 3 Beta #1’s proposed lazy loading could be bad for large apps” to reflect that this really is just a beta, that was posted to gather feedback and have user’s test it out? Right now, your post’s title (and content) sounds a bit ungrateful, bash- and clickbait-y.


#5

I think that is somewhat a fair criticism. The only reason it’s a little firmer is because everywhere I looked it seemed like an architecture decision and not just a beta bug.

I had a super tough time getting lazy loading working in the beta, it at least was on day one broken on Windows. On OSX the upgrade guide was wrong. All of that is totally fine by me. It’s a beta. It’s going to have issues and I’m happy to jump in early and help work them out. None of that is mentioned anywhere in my post, because I don’t care about bugs.

I do care when everywhere I look it seems a design decision was made that I think will hurt me and the framework in general. It’s not a bug. I opened a feature request on github and it was initially closed something they wouldn’t support.

That might be changing now, and if it does I will 100% update the post and write a new one (and many more) on writing reusable modules and lazy loading in Ionic. But as it stands the title represents what I believe to be true and I stand by it. I’m hoping that soon that will no longer be the case.

I hope that makes sense and you can understand why I did it. I do appreciate the feedback.


#6

I totally get where you are coming from. But if I were in the framework team, this “style” wouldn’t help to convince me (or even help that I really think about it). A “Ionic 3 Beta seems to go that way, and here is why I think that would be not a good idea: […] Maybe do it this way […]” wording would move the discussion in a totally different direction.


#7

Yeah, you’re right. It doesn’t work. But I think it can, and maybe even is intended to, because if you look at the end of here:

{ provide: ANALYZE_FOR_ENTRY_COMPONENTS, useValue: page, multi: true },

The component itself does instantiate properly, so DI works for this case. It may just be an issue of how to placate the DeepLinker, which I’ve never worked with before.

The current situation seems to be geared for 1 module = 1 page. You want a coarser granularity, where 1 module = N pages. I’m stuck in the other direction, where I want 1 page = N modules, because I have (rather heavy) components that are shared by several different pages. I think this is likely to be even harder than what you are looking for.

I’m a big fan of the design motto of “make easy things easy and hard things possible”. Looks like more progress has been made on the first part than on the second so far.


#8

Hmm, I find the 1 page = N modules confusing. If you have components that are shared by several pages can’t you just use those components anywhere? I may not be fully following, just curious.


#9

Only if I do one of two rather suboptimal things:

  • throw them in the AppModule, which negatively impacts startup time
  • make a module for them and import it in every page that uses them, which when lazy loading is enabled ends up with all that heavy component code in each [n].main.js per page, which really bloats total application size

#10

OK, this goes beyond just the deep linker, but I remain cautiously optimistic that @rlouie’s goal might still be attainable. I am now looking at module-loader, and it seems to have a map of components to modules/component factory resolvers that can handle them. If one module really can provide multiple components, I’m hopeful that the same resolver can properly resolve requests for any of them.

So, IonicPageModule.forChild() under the hood associates an arbitrary injection token with its argument. The module loader unpacks that and uses it to populate the cfr map. What if it were allowed to be an array of classes, so one could write:

import IonicPageModule.forChild([ComponentA, ComponentB])

The module loader could detect this and loop through the array, doing what it is currently doing on line 46:

this._cfrMap.set(component, ref.componentFactoryResolver);

…for each component in the array. That might work. I’m not sure how to run against a local dev Ionic build, though. I remember that not going real great the last time I tried it.

UPDATE: that sort of worked, but not really, because now that component array is coming in as the argument to the resolver, which is choking it.

FURTHER UPDATE: OK, I have a hacked-up PoC that does seem to work. Obviously, don’t try this at home if you’re not willing to break things. I took the conference app and folded the SpeakerDetailPage into the same module as SpeakerListPage:

  • Comment out all of SpeakerDetailModule.
  • Add SpeakerDetailPage to the declarations and entryComponents of SpeakerListModule.
  • Add a second parameter [SpeakerDetailPage] to theforChild() call in SpeakerListModule.
  • Hack up IonicPageModule in node_modules/ionic-angular/index.js to add this parameter:
IonicPageModule.forChild = function (page, addlPages) {
        return {
            ngModule: IonicPageModule,
            providers: [
                { provide: /** @type {?} */ (LAZY_LOADED_TOKEN), useValue: page },
              { provide: 'hackeroo-addpages', useValue: addlPages},
                { provide: ANALYZE_FOR_ENTRY_COMPONENTS, useValue: page, multi: true },
            ]
        };
  • and hack up ModuleLoader.prototype.load in util/module-loader.js to receive it:
var /** @type {?} */ ref = loadedModule.create(_this._injector);
            var /** @type {?} */ component = ref.injector.get(LAZY_LOADED_TOKEN);
            _this._cfrMap.set(component, ref.componentFactoryResolver);
            var addlPages = ref.injector.get('hackeroo-addpages');
            if (addlPages) {
              var ncomps = addlPages.length;
              var i;
              for (i = 0; i < ncomps; i++) {
                _this._cfrMap.set(addlPages[i], ref.componentFactoryResolver);
              }
            }
  • Finally, change the way SpeakerDetailsPage is pushed in SpeakerListPage from a string to the actual SpeakerDetailsPage class/constructor.

Have not tested what happens with providers yet, but perhaps somebody will find it interesting.


#11

I find this very interesting thank you! I thought about going along the same lines as you for part one, but did not take it as far as either of your updates. I think I started with that same idea because it’s similar to how it works in regular Angular:

const routes: Routes = [
  { path: '', component: PageOneComponent },
  { path: 'subpage', component: PageTwoComponent }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule],
})

Granted with Angular you define named routes, but otherwise similar idea, pass multiple components to the forChild method.

You’re two updates have me extremely interested though. THANK YOU!!! I will definitely be looking into that. Probably tomorrow though. My brains kind of fried today, I’ve been going hard working while also digging into Ionic.

I’ll report back if / when I find anything.


#12

If anybody from the Ionic team is monitoring this thread, LMK if this is an avenue that you would be open to entertaining PRs on, or if it’s a can of worms you would prefer left unopened.


#13

Fyi on Twitter it sounds like they can support my use case. If they’re thinking along the same lines as you they may welcome a PR, not sure though.


#14

If they’re thinking along the same lines as me, we’re probably all headed to the nuthouse.


#15

Idk, it sounds not super far off:

You on twitter? We should follow each other if so: https://twitter.com/rob_louie


#16

Hi, sorry but i have not been following this as closely as i should have.
What i understand so far is that it is not possible to lazy load a module that contains multiple components?
In a project i have a “settings” folder. that well…manages settings :slight_smile:
The folder structure looks like this:

Every file in this folder is a component, except the index, wich just exports all the components, and the sub.settings wich are classes. So this folder alone contains about 15 different components. At this moment, every single one off these components is list in the app.module…twice, because you need to add them to the declarations and the entrycomponents.
This is also not the only folder containing multiple components. I hope you can see that my app.module file is several miles long :blush:

With the announcement i saw that the Ionic 3 beta supported lazy loading, i was very excited.
However, just as @rlouie “complained” about, with the current state i’m not sure if that is possible.

The examples in the Doc’s i read yesterday do no help me at all, there they are showing how to lazy load 1 single page.
What? Who even wants to lazy load a single page, i might be missing something but i really dont see the point for that.

In the docs however, near the bottom, they mentioned you could have 1 module for multiple pages, yet they failed to provide some sample code of said module.
I have tried it like this :

but that does not work.

I’m not sure how to add multiple components to this module. But from what i read it’s not really possible to do so?

I really don’t want to make a module for every single component in that folder, it’s already too big and it would only be worse.


#17

Couple points here, I’ll start with one fundamental thing. In Ionic world a component is not necessarily a page, so are all of these pages? It doesn’t seem like it, so we might be talking slightly different things. Can you clarify if those components are actual pages? That will clear up a lot.

Then regarding modularity, what I’m getting at (and what would clean up your app.module…), is that you should make a settings.module.ts in that folder that declares all these components. Then you just import the settings module into app.module and you’re done, much cleaner. (Unless these are all actual pages and you want to lazy load them).

So you’re not really using module right now as far as I can see. You have a giant list of components that you use separately, although you have them organized by feature which is one point I was going for.

Regarding lazy loading a single page, well, if you lazy load every single page that will definitely cut down file size, and that’s what Ionic is allowing right now.


#18

no no, every single component is an actual page, some smaller than the other but they are pages.
they have input fields or checkboxes or whatever.


#19

Ah, ya, gotcha. So ya, you could still have a single module for all those pages, and that would still clean up your app.module code and organize all that to a single module, but(!), then you couldn’t lazy load them. Now, if you do lazy load them, it will still clean up your app.module because since they’d all be lazy loaded you wouldn’t import them anywhere, you’d just push them onto the nav stack via a string with the page name.

However yes that means you will have basically double the files in that folder, one module for each page. On the plus side you can then lazy load them and clean up your app module. Ionic also provides a script that will generate all those module files for you provided all pages have the IonicPage decorator. You can find how to run the script in the upgrade guide.


#20

well, i’ll give it a try, probably next week though.
i dont really mind a big app.module tbh, that’s for developing, users dont know that, i care more about the speed of the app.