Skip to content

Ergonomic component-level code splitting and lazy loading #18093

Closed

Description

@aalbericio

I'm submitting a...


[ ] Regression (a behavior that used to work and stopped working in a new release)
[ ] Bug report  
[X] Feature request
[ ] Documentation issue or request
[ ] Support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

Router Module is optional and, depending on the nature of the app, it does not naturally fit.

If we don't use the RouterModule, I can't see an easy way to take advantage of the "lazy loading" feature for modules.

I'd like to, somehow, identify an NgModule to be "lazy loaded" at definition time (just like you define its providers, etc).

Once the module has been marked as "lazy loaded", the build should generate an independent chunk for it and that module becomes eligible for being lazy-loaded at some execution time.

Of course, If I have the choice of marking a module "lazy", I also need a mechanism to manually load those "lazy loaded" modules and start using them.

In summary, the feature request is: make the "lazy loading" feature available to developers outside the RouterModule since this module is optional but the feature should still be available.

Of course this should output AOT compatible chunks, etc, just like the RouterModule internally does.

Expected behavior

In summary, the feature request is: make the "lazy loading" feature available to developers outside the RouterModule since this module is optional but the feature should still be available.

Of course this should output AOT compatible chunks, etc, just like the RouterModule internally does.

Minimal reproduction of the problem with instructions

What is the motivation / use case for changing the behavior?

Our application and functionally can not be Router driven thus we need some way to make use of the "lazy loading" strategy that is internally used by the Router.

Environment


Angular version: X.Y.Z


Browser:
- [ ] Chrome (desktop) version XX
- [ ] Chrome (Android) version XX
- [ ] Chrome (iOS) version XX
- [ ] Firefox version XX
- [ ] Safari (desktop) version XX
- [ ] Safari (iOS) version XX
- [ ] IE version XX
- [ ] Edge version XX
 
For Tooling issues:
- Node version: XX  
- Platform:  

Others:

Activity

  1. Toxicable commented on Jul 13, 2017

    @Toxicable

    You can already do this, here is a small example of how

    System.import('./lazy.module.ngfactory.ts').then((moduleFactories: any) => {
       const moduleFactory: NgModuleFactory<any> = moduleFactories['LazyModuleNgFactory'];
       const moduleRef = moduleFactory.create(injector);
       const compFactory = moduleRef.componentFactoryResolver.resolveComponentFactory(COMP_TYPE_TOKEN);
       viewRef.createComponent(compFactory);
    });
    

    This is almost the exact same method the router uses to lazy load except it simply provides a nice API to use it, however, this only works for AOT, you'd need extra logic to support JIT.

  2. josephliccini commented on Jul 16, 2017

    @josephliccini

    Hi!

    I had left this idea a while ago but was making some progress on a solution.

    (@Toxicable we chatted about this a while ago in the gitter.im angular room.)

    I think it will require a new webpack loader to get it to work cleanly, but here's the basic idea.

    1. create a <dynamic-lazy-load> component:
    @Component({
      selector: 'dynamic-lazy-load',
      template: ''
    })
    export class DynamicLazyLoadComponent implements OnChanges {
    
      /** Entry Component desired */
      @Input() 
      public componentName: string;
    
      /** This is the same module callback one would pass to Angular Router loadChildren */
      @Input() 
      public loadChildrenCallback: () => Promise<any>; 
      
      private loadingStarted: boolean = false;
    
      constructor(
        private injector: Injector,
        private viewRef: ViewContainerRef,
        @Optional() private compiler: Compiler // only available in JiT mode
      ) { }
    
       public async ngOnChanges(changes) {
            if (this.loadChildrenCallback && !this.loadingStarted) {
    	    this.loadingStarted = true;
    
                const lazyModule = await this.loadChildrenCallback();
                if (lazyModule instanceof NgModuleFactory) {
                    console.log('AoT Mode');
                    this.createAndInsertComponent(lazyModule);
                } else {
                    const compiledModule = await this.compiler.compileModuleAsync(lazyModule);
                    console.log('JiT Mode');
                    this.createAndInsertComponent(compiledModule);
                }
            }
        }
    
        public createAndInsertComponent(moduleFactory: NgModuleFactory<any>) {
            const moduleRef = moduleFactory.create(this.injector);
            if (!moduleRef.instance.dynamicLazyLoadComponents) {
                console.log('No Lazy Load Components Found...');
                return;
            }
    
            const componentType = moduleRef.instance.dynamicLazyLoadComponents[this.componentName];
            const componentFactoryResolver: ComponentFactoryResolver = moduleRef.componentFactoryResolver;
            const compFactory = componentFactoryResolver.resolveComponentFactory(componentType);
            this.viewRef.createComponent(compFactory);
        }
    }
    1. write a lazy-loaded @NgModule() in format:
    interface DynamicLazyLoadModule {
    	dynamicLazyLoadComponents: { [key: string]: any };
    }
    
    @NgModule({
    	imports: [ MyLazyComponent ],
    	exports: [ MyLazyComponent ],
    	entryComponents: [ MyLazyComponent ]
    })
    export class MyLazyNgModule implements DynamicLazyLoadModule {
    	public dynamicLazyLoadComponents = { 'MyLazyComponent': MyLazyComponent };
    }
    1. In the containing component's HTML:
    <dynamic-lazy-load *ngIf="shouldLoadLazyComponent" 
                                      componentName="MyLazyComponent" 
                                      [loadChildrenCallback]="loadLazyModuleCallback">
    </dynamic-lazy-load>

    I had a prototype working but abandoned as I had some other stuff to take care of and hit a blocker on AoT.

    And the hackiest part was I had to trick the Webpack loader into thinking this was a loadChildren for the router, which is where I gave up because I couldn't get AoT working.

    const loadLazyModuleCallback = '../my-lazy/my-lazy.module#MyLazyModule';

    I did have it working when I hardcoded to JiT like this:

    const loadLazyModuleCallback = () => new Promise((resolve) => {
    	require.ensure([], (require) => {
    		const module = require('../my-lazy/my-lazy.module');
    		resolve(module);
    	});
    }, 'my-lazy.module');

    I haven't tested this code in a while, but I can maybe update with a demo sometime when I have time.

    This feature would be great to have as something easier and standardized, as I've been doing some React coding on the side and they can lazy load components within a view pretty easily.

    Some problems I can foresee:

    1. I am not sure what would happen if there was providers, pipes, etc. in the lazy module..
    2. Dependency on an arbitrary string 'MyLazyComponent' in the example.
    3. Interactions with router

    I just thought I'd post and share my exploration as I've seen this question a lot. By no means am I posting above code as a solution (again, this was just a hacky prototype I tried)

  3. aalbericio commented on Jul 20, 2017

    @aalbericio
    Author

    Hello guys,

    I aprreciate the efforts of offering a workaround on this (I will more than probably use a custom hack on this following your instructions) but I still see the advantage of declaring a Module like:

    @NgModule({
    imports: [ MyLazyComponent ],
    exports: [ MyLazyComponent ],
    entryComponents: [ MyLazyComponent ],
    lazy: true
    })

    ... and, of course, having a programmatic API to load this/or any Module

    ModuleManager.loadModule(....).then(...)

    ... and, of course, this should work with JiT/AOT, serve/build, etc

    I know Ionic and others like you have implemented their custom solution but I can bet adding this feature makes Angular (and any other based solution) just better.

  4. Toxicable commented on Jul 20, 2017

    @Toxicable

    I made another demo here https://plnkr.co/edit/ZGC82G9u10EQFYFvvRMB?p=preview
    it uses NgModuleFactoryLoader which Angular already provides for you to handle lazy loading modules

  5. aalbericio commented on Jul 20, 2017

    @aalbericio
    Author

    Hello,

    Okay, that will load the module and instantiate its entry component but how does that solution play in conjunction with the DI/Injectors system and what about the build? Will the build generate a separate chunk for the LazyModule?

    I would like not to lose any feature provided both by the DI/Injectors system (eager/lazy) and the build process.

    I still firmly believe that this feature should be provided by the framework and open to developers.

  6. Toxicable commented on Jul 20, 2017

    @Toxicable

    @aalbericio It works the same as the Router. Seprate Chunk with child Injector.
    Is the example I provided to concise enough?

  7. aalbericio commented on Jul 20, 2017

    @aalbericio
    Author

    Ok. I'm going to test it under an isolated environment and tell you. I just wanted to confirm those points before testing it.

    Thanks very much, Fabian.

  8. mlc-mlapis commented on Jul 20, 2017

    @mlc-mlapis
    Contributor

    @Toxicable ... not sure but the building could be a problem. You will need probably to define all routes with lazy module paths even if you don't use them directly to get right chunks.

  9. Toxicable commented on Jul 20, 2017

    @Toxicable

    @mlc-mlapis You're right I was missing a few small components for it to work with the CLI and Webpack. I've updated the demo with this now working in plunkr, CLI and with AOT.
    https://plnkr.co/edit/ZGC82G9u10EQFYFvvRMB?p=preview

  10. mlc-mlapis commented on Jul 20, 2017

    @mlc-mlapis
    Contributor

    @Toxicable ... great ... and now there will be always the right answer for every one who has a request for a new feature or API ... to allow programmatical way how to load and use lazy loaded modules.

    It is so simple that it couldn't be more simpler.

  11. josephliccini commented on Jul 20, 2017

    @josephliccini

    @Toxicable this is great!! Thanks for doing this!

  12. Toxicable commented on Jul 20, 2017

    @Toxicable

    I made one more little update to it just to show how you can use the paths to the lazy modules without any duplication aswell as how you provide in a feature module aswell.
    https://plnkr.co/edit/ZGC82G9u10EQFYFvvRMB?p=preview

    @josephliccini No problem, hope it's helped

  13. josephliccini commented on Jul 20, 2017

    @josephliccini

    Would something like this be able to be placed into a ngx-lazy-load or something? If I find time I'd like to make it easier for folks and our own engineers to use

    EDIT: not asking you to do so, but I am interested on if it its possible

  14. Toxicable commented on Jul 20, 2017

    @Toxicable

    @josephliccini it would be possible to put this into a lib.
    However since most of it is config you'd have to provide it would only containDynamicComponent the rest of it you'd still have to provide yourself.

  15. 55 remaining items

  16. changed the title [-]Make lazy loading available outside the Router Module[/-] [+]Ergonomic component-level code splitting and lazy loading[/+] on Oct 9, 2020
  17. jelbourn commented on Oct 9, 2020

    @jelbourn
    Contributor

    Just closed a bunch of issues that @atscott and I believe are asking for the same general idea. Updated the title of this issue to reflect that underlying concept. #22243 and #13218 seem to be very similar, need to look into further whether they can be combined into this.

  18. KarlXOL commented on Oct 10, 2020

    @KarlXOL

    Solution 2 here, demonstrate how to do it with ivy.
    https://medium.com/front-end-tricks/dynamic-header-bars-and-or-contents-for-your-ivy-angular-app-56e1e7e3fc22
    check: headerbar-selector.component.ts in the demo2

    Checked this approach a while ago.
    This solution has its very pitfalls and limitations and is no solution for "feel-good" dynamic loading at runtime.
    For example you have to care of component instantiation, parameter passing, etc...

  19. nasreddineskandrani commented on Oct 12, 2020

    @nasreddineskandrani

    @KarlXOL it's only a solution and i do agree with you => Angular team should think about something more efficient and nice to achieve the desired result if possible.

  20. jstoppa commented on Feb 8, 2021

    @jstoppa

    I'd just like to share s solution that worked for me for lazy loading components at runtime.

    My use case is that I've got an application that loads components programmatically using the Dynamics Component Loader, it was never architected to use the router and the number of components that can be loaded programmatically grew over time, they are loaded conditionally based on configuration/feature flags. The problem is that all these components are not lazy loaded and they ended up bloating the main bundle.

    I can’t really rearchitect the entire app to user the router which would require a lot of time and effort so lazy loading programmatically is key for my use case.

    I initially tried loading these components like the below:

    ngAfterViewInit(): void {
        import("./path_to_my_component").then( c => {
          const componentFactory = this.componentFactoryResolver.resolveComponentFactory(
            c.SubComponent
          );
          this.lazyComponentRef.createComponent(componentFactory);
        });
      }

    This works but it would bring other problems at runtime:

    • Issues when using structural directives in my lazy loaded component (e.g. can't bind to 'ngIf' since it isn't a known property of ‘xyz') suggesting it needs the CommonModule
    • Issues when referencing other components in my template saying it can’t find the component.

    I searched and I found a few approaches like this or this, which requires putting a module inside the component file and loading the module using the compiler.

    My approach was to reference the component and the module from the tsconfig.json’s paths via a proxy file that references them…

    "paths": {
          "@SubComponent": [ "src/app/sub-component/public-api.ts"]
    }

    the public-api.ts would reference the component and the module

    export * from "./sub.component";
    export * from "./sub.module";

    and then load the component using the reference in the tsconfig.json file.

    ngAfterViewInit(): void {
        import("@SubComponent").then( c => {
          const componentFactory = this.componentFactoryResolver.resolveComponentFactory(
            c.SubComponent
          );
          this.lazyComponentRef.createComponent(componentFactory);
        });
      }

    The sub.module.ts file will contain the modules that I need to solve the errors I was getting.

    This approach worked really well for me, I had to do little refactoring of my application and managed to reduce the main bundle size by 25% 😊

    I hope it helps someone else

  21. added
    feature: under considerationFeature request for which voting has completed and the request is now under consideration
    on Jun 4, 2021
  22. petebacondarwin commented on Nov 26, 2021

    @petebacondarwin
    Contributor

    This should be made possible by the "standalone components" project.

  23. pkozlowski-opensource commented on Jun 30, 2022

    @pkozlowski-opensource
    Member

    Angular's standalone components implemented in Angular v14 and other changes we've been doing around ComponentFactoryResolver greatly improve the component-level, lazy-loading story.

    As an example, lazy-loading a standalone component and rendering it boils down to:

    @Component({...})
    export class LazyLoadingComponent {
         constructor(private _vcRef: ViewContainerRef) {}
         
         someAction() {
             import('path/to/a/file').then((m) => this._vcRef.createComponent(m.MyLazyComponent));
         }
    }

    With the the RFC: Standalone components, directives and pipes - making Angular's NgModules optional implemented we can consider that the stated issue is solved!

    In summary, the feature request is: make the "lazy loading" feature available to developers outside the RouterModule since this module is optional but the feature should still be available.
    Of course this should output AOT compatible chunks, etc, just like the RouterModule internally does.

  24. angular-automatic-lock-bot commented on Jul 31, 2022

    @angular-automatic-lock-bot

    This issue has been automatically locked due to inactivity.
    Please file a new issue if you are encountering a similar or related problem.

    Read more about our automatic conversation locking policy.

    This action has been performed automatically by a bot.

  25. locked and limited conversation to collaborators on Jul 31, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

area: coreIssues related to the framework runtimefeatureLabel used to distinguish feature request from other issuesfeature: under considerationFeature request for which voting has completed and the request is now under consideration

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions