# Lazy Loading
Lazy loading is not only about Angular modules that you load via loadChildren; it's about everything, including third-party JavaScript, dynamic components, CSS, images, and more.
# Third-Party JavaScript
Any third-party JavaScript should be loaded after the page has completely rendered. This includes items such as third-party analytics. There is no need to place such content within the <head> element:
<script src="https://maps.googleapis.com/maps/api/js?key=API_KEY&libraries=places"></script>
Regardless of whether the script is marked as async or not, HTML parsing usually doesn't take a significant amount of time. Angular's index.html is typically small, consisting of an empty body with only the <app-root> element.
Taking the above example into consideration, we can choose to load Google Maps only when it is needed. For this purpose, you can utilize the @googlemaps/js-api-loader package or load it manually:
declare global {
interface Window {
__googleMapsCallback(): void;
}
}
const googleMaps$ = defer(
() =>
new Observable<void>(observer => {
const script = document.createElement('script');
script.async = true;
script.src =
'https://maps.googleapis.com/maps/api/js?key=API_KEY&libraries=places&callback=__googleMapsCallback';
document.head.appendChild(script);
window.__googleMapsCallback = () => {
observer.next();
observer.complete();
};
function onError(error: ErrorEvent) {
observer.error(error);
}
script.addEventListener('error', onError);
return () => {
script.removeEventListener('error', onError);
};
}),
).pipe(shareReplay({ bufferSize: 1, refCount: true }));
@Component({
selector: 'app-google-maps-autocomplete',
templateUrl: './google-maps-autocomplete.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class GoogleMapsAutocompleteComponent implements OnInit, OnDestroy {
@ViewChild('input', { static: true }) input!: ElementRef<HTMLInputElement>;
private subscription = Subscription.EMPTY;
ngOnInit(): void {
this.subscription = googleMaps$.subscribe(() => {
const autocomplete = new google.maps.places.Autocomplete(this.input.nativeElement);
});
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
}
Well, we can create a separate class that will provide the googleMaps$ observable for the sake of testing:
declare global {
interface Window {
__googleMapsCallback(): void;
}
}
@Injectable({ providedIn: 'root' })
export class GoogleMaps {
private readonly googleMaps$ = defer(
() =>
new Observable<void>(observer => {
const script = document.createElement('script');
script.async = true;
script.src =
'https://maps.googleapis.com/maps/api/js?key=API_KEY&libraries=places&callback=__googleMapsCallback';
document.head.appendChild(script);
window.__googleMapsCallback = () => {
observer.next();
observer.complete();
};
function onError(error: ErrorEvent) {
observer.error(error);
}
script.addEventListener('error', onError);
return () => {
script.removeEventListener('error', onError);
};
}),
).pipe(shareReplay({ bufferSize: 1, refCount: true }));
getGoogleMaps() {
return this.googleMaps$;
}
}
Now you can mock the provider:
TestBed.configureTestingModule({
providers: [
{
provide: GoogleMaps,
useValue: {
getGoogleMaps() {
return of({ ... });
},
},
},
],
});
# Third-Party Libraries
Dynamic import is the greatest weapon when dealing with third-party libraries that shouldn't be bundled into the vendor file. For instance, you can load the amplitude-js library only when the first event is logged, which can happen at an unspecified point:
import type { AmplitudeClient } from 'amplitude-js';
@Injectable({ providedIn: 'root' })
export class Amplitude {
private readonly client$ = defer(
() => import(/* webpackChunkName: 'amplitude-js' */ 'amplitude-js'),
).pipe(
map(amplitude => {
const client = amplitude.getInstance(environment.amplitudeProjectName);
client.init(environment.amplitudeApiKey);
return client;
}),
shareReplay({ bufferSize: 1, refCount: true }),
);
private readonly ngZone = inject(NgZone);
logEvent(event: string, data?: object): void {
this.ngZone.runOutsideAngular(() => {
this.client$.subscribe(client => {
client.logEvent(event, data);
});
});
}
}
Another example is the custom error handler that logs events to Bugsnag and loads the bugsnag package only when the first error is thrown:
@Injectable()
export class BugsnagErrorHandler implements ErrorHandler {
private readonly bugsnag$ = defer(
() => import(/* webpackChunkName: 'bugsnag' */ '@bugsnag/js'),
).pipe(
pluck('default'),
tap(Bugsnag => {
Bugsnag.start(environment.bugsnagApiKey);
}),
shareReplay({ bufferSize: 1, refCount: true }),
);
private readonly ngZone = inject(NgZone);
handleError(error: any): void {
this.ngZone.runOutsideAngular(() => {
this.bugsnag$.subscribe(Bugsnag => {
Bugsnag.notify(error);
});
});
}
}
@NgModule({
providers: [
{
provide: ErrorHandler,
useClass: BugsnagErrorHandler,
},
],
})
export class AppModule {}
Or with bootstrapApplication:
bootstrapApplication(AppComponent, {
providers: [
{
provide: ErrorHandler,
useClass: BugsnagErrorHandler,
},
],
});
You can move the bugsnag$ observable to a separate class (e.g. BugsnagLoader) for the sake of testing.
# Dynamic Components
The rule of thumb is to avoid bundling modules and components that are not immediately needed but rather in response to a user action.
Let's consider the example of using the ng2-pdf-viewer library. We want to display the PDF file when the user clicks the "view pdf" button. However, the minified size of this library is enormous (500+kb) because it includes the bundled pdfjs-dist library. Since there is a possibility that the user may not click the "view pdf" button, including such a large package in the bundle would result in a significant amount of unused code.
Well, let's turn to our best friend import():
@Component({
selector: 'app-root',
template: `
<button (click)="viewPdf()">View PDF</button>
<ng-template #pdfViewerContainer></ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
@ViewChild('pdfViewerContainer', { read: ViewContainerRef, static: true })
pdfViewerContainer!: ViewContainerRef;
viewPdf(): void {
import(/* webpackChunkName: 'ng2-pdf-viewer' */ 'ng2-pdf-viewer').then(
({ PdfViewerComponent }) => {
const { instance, changeDetectorRef } =
this.pdfViewerContainer.createComponent(PdfViewerComponent);
instance.src = '...';
instance.renderText = true;
// Run the change detection since we're inside an OnPush component which won't be dirty when the promise resolves.
changeDetectorRef.detectChanges();
},
);
}
}
The above example is relatively easy since the PdfViewerComponent doesn't have any injectees (it doesn't depend on any PdfViewerModule providers).
Let's consider a more complex example where the component requires a module provider:
@Injectable()
export class EmojiPickerService {}
@Component({
selector: 'app-emoji-picker',
template: '',
})
export class EmojiPickerComponent {
constructor(private readonly emojiPickerService: EmojiPickerService) {}
}
@NgModule({
declarations: [EmojiPickerComponent],
providers: [EmojiPickerService],
})
export class EmojiPickerModule {}
If we try to lazy-load the component and create the embedded view, we'll get No provider for EmojiPickerService! error:
@Component({
selector: 'app-root',
template: `
<button (click)="showEmojiPicker()">Show emoji picker</button>
<ng-template #emojiPickerContainer></ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
@ViewChild('emojiPickerContainer', { read: ViewContainerRef, static: true })
emojiPickerContainer!: ViewContainerRef;
showEmojiPicker(): void {
import(/* webpackChunkName: 'emoji-picker' */ './emoji-picker').then(
({ EmojiPickerComponent }) => {
this.emojiPickerContainer.createComponent(EmojiPickerComponent);
},
);
}
}
This is because it first attempts to resolve the EmojiPickerService from the AppComponent injector and then from the AppModule injector (as it traverses through the element and module injectors).
In this case, we need to provide the parent injector for the embedded view, which should be the EmojiPickerModule injector, not the AppComponent injector. The createComponent function supports passing the ngModuleRef parameter, which will act as the parent injector for the embedded view. Internally, it creates a chained injector:
/**
* Injector that looks up a value using a specific injector, before falling back to the module
* injector. Used primarily when creating components or embedded views dynamically.
*/
class ChainedInjector implements Injector {
constructor(private injector: Injector, private parentInjector: Injector) {}
get<T>(token: ProviderToken<T>, notFoundValue?: T, flags?: InjectFlags | InjectOptions): T {
flags = convertToBitFlags(flags);
const value = this.injector.get<T | typeof NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR>(
token,
NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR,
flags,
);
if (
value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR ||
notFoundValue === (NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR as unknown as T)
) {
// Return the value from the root element injector when
// - it provides it
// (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR)
// - the module injector should not be checked
// (notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR)
return value as T;
}
return this.parentInjector.get(token, notFoundValue, flags);
}
}
It'll go to the AppComponent injector first, then to the module injector.
Previously, it was possible to retrieve the NgModuleRef through NgModuleFactory.create, and the NgModuleFactory could be obtained through Compiler.compileModuleSync, which internally called new NgModuleFactory$1(moduleType). But it's been deprecated (https://angular.io/guide/deprecations#jit-api-changes-due-to-viewengine-deprecation).
There's an exported function called createNgModule (previously named createNgModuleRef before @angular/core@14.1.0-rc.0), which takes the module type and parent injector as parameters:
import { createNgModule } from '@angular/core';
@Component({
selector: 'app-root',
template: `
<button (click)="showEmojiPicker()">Show emoji picker</button>
<ng-template #emojiPickerContainer></ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
@ViewChild('emojiPickerContainer', { read: ViewContainerRef, static: true })
emojiPickerContainer!: ViewContainerRef;
private readonly injector = inject(Injector);
showEmojiPicker(): void {
type EmojiPickerModule = import('./emoji-picker').EmojiPickerModule;
import(/* webpackChunkName: 'emoji-picker' */ './emoji-picker').then(
({ EmojiPickerModule, EmojiPickerComponent }) => {
const ngModuleRef: NgModuleRef<EmojiPickerModule> = createNgModule(
EmojiPickerModule,
/* parentInjector */ this.injector,
);
this.emojiPickerContainer.createComponent(EmojiPickerComponent, { ngModuleRef });
},
);
}
}
Suppose the EmojiPickerModule imports some feature state (let's say we're using NGXS for this example), but we don't want to create a new NgModuleRef each time the emoji picker is displayed. This requirement applies to the component, not the module itself. It is crucial for the module to be a singleton:
import { State, NgxsModule } from '@ngxs/store';
export interface EmojiPickerStateModel {}
@State<EmojiPickerStateModel>({
name: 'emojiPicker',
})
@Injectable()
export class EmojiPickerState {}
@Injectable()
export class EmojiPickerService {}
@Component({
selector: 'app-emoji-picker',
template: '',
})
export class EmojiPickerComponent {
constructor(private readonly emojiPickerService: EmojiPickerService) {}
}
@NgModule({
imports: [NgxsModule.forFeature([EmojiPickerState])],
declarations: [EmojiPickerComponent],
providers: [EmojiPickerService],
})
export class EmojiPickerModule {}
The NGXS feature module contains internal providers that will be recreated each time the emoji picker is displayed. To address this, we require a caching service that will create NgModuleRefs only once for caching purposes:
interface ComponentWithNgModuleRef {
component: Type<unknown>;
ngModuleRef: NgModuleRef<unknown>;
}
@Injectable({ providedIn: 'any' })
export class ComponentLoader {
private readonly cache = new Map<Type<unknown>, ComponentWithNgModuleRef>();
private readonly injector = inject(Injector);
resolveComponentAndNgModuleRef<M, C>(
factory: () => Promise<{ module: Type<M>; component: Type<C> }>,
): Observable<ComponentWithNgModuleRef> {
return defer(() => factory()).pipe(
map(({ module, component }) => {
if (this.cache.has(module)) {
return this.cache.get(module) as ComponentWithNgModuleRef;
}
const ngModuleRef: NgModuleRef<M> = createNgModule(
module,
/* parentInjector */ this.injector,
);
const componentWithNgModuleRef: ComponentWithNgModuleRef = { component, ngModuleRef };
this.cache.set(module, componentWithNgModuleRef);
return componentWithNgModuleRef;
}),
);
}
}
@Component({
selector: 'app-root',
template: `
<button (click)="showEmojiPicker()">Show emoji picker</button>
<ng-template #emojiPickerContainer></ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent {
@ViewChild('emojiPickerContainer', { read: ViewContainerRef, static: true })
emojiPickerContainer!: ViewContainerRef;
constructor(private readonly componentLoader: ComponentLoader) {}
showEmojiPicker(): void {
this.componentLoader
.resolveComponentAndNgModuleRef(() =>
import(/* webpackChunkName: 'emoji-picker' */ './emoji-picker').then(m => ({
module: m.EmojiPickerModule,
component: m.EmojiPickerComponent,
})),
)
.subscribe(({ component, ngModuleRef }) => {
this.emojiPickerContainer.createComponent(component, { ngModuleRef });
});
}
}
I'd recommend always using the ComponentLoader and always providing an ngModuleRef to serve as a parent injector.
You might also use some packages that require importing modules with providers, for instance:
@NgModule({
imports: [EmojiModule.forRoot()],
})
export class AppModule {}
To support resolving ModuleWithProviders<M>, we need to extend the ComponentLoader. We need to retrieve the providers property from the module and create a new injector using these providers:
interface ComponentWithNgModuleRef {
component: Type<unknown>;
ngModuleRef: NgModuleRef<unknown>;
}
@Injectable({ providedIn: 'any' })
export class ComponentLoader {
private readonly cache = new Map<Type<unknown>, ComponentWithNgModuleRef>();
private readonly injector = inject(Injector);
resolveComponentAndNgModuleRef<M, C>(
factory: () => Promise<{ module: Type<M>; component: Type<C> }>,
): Observable<ComponentWithNgModuleRef>;
resolveComponentAndNgModuleRef<M, C>(
factory: () => Promise<{ module: ModuleWithProviders<M>; component: Type<C> }>,
): Observable<ComponentWithNgModuleRef>;
resolveComponentAndNgModuleRef<M, C>(
factory:
| (() => Promise<{ module: Type<M>; component: Type<C> }>)
| (() => Promise<{ module: ModuleWithProviders<M>; component: Type<C> }>),
): Observable<ComponentWithNgModuleRef> {
return defer(() => factory()).pipe(
map(({ module, component }) => {
let providers: StaticProvider[] | undefined;
if (isModuleWithProviders(module)) {
providers = <StaticProvider[] | undefined>module.providers;
module = module.ngModule;
}
if (this.cache.has(module)) {
return this.cache.get(module) as ComponentWithNgModuleRef;
}
const injector = providers
? Injector.create({ providers, parent: this.injector })
: this.injector;
const ngModuleRef: NgModuleRef<M> = createNgModule(module, /* parentInjector */ injector);
const componentWithNgModuleRef: ComponentWithNgModuleRef = { component, ngModuleRef };
this.cache.set(module, componentWithNgModuleRef);
return componentWithNgModuleRef;
}),
);
}
}
function isModuleWithProviders<M>(
module: Type<M> | ModuleWithProviders<M>,
): module is ModuleWithProviders<M> {
return module.hasOwnProperty('ngModule');
}
Let's get back to our example with an emoji picker and create an injection token. We will provide its value through the forRoot() argument:
export const DEFAULT_EMOJI = new InjectionToken<string>('Default emoji');
@Component({
selector: 'app-emoji-picker',
template: '{{ defaultEmoji }}',
})
export class EmojiPickerComponent {
constructor(@Inject(DEFAULT_EMOJI) readonly defaultEmoji: string) {}
}
@NgModule({
declarations: [EmojiPickerComponent],
})
export class EmojiPickerModule {
static forRoot(defaultEmoji: string): ModuleWithProviders<EmojiPickerModule> {
return {
ngModule: EmojiPickerModule,
providers: [
{
provide: DEFAULT_EMOJI,
useValue: defaultEmoji,
},
],
};
}
}
Let's try to load it again:
@Component({
selector: 'app-root',
template: `
<button (click)="showEmojiPicker()">Show emoji picker</button>
<ng-template #emojiPickerContainer></ng-template>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnDestroy {
@ViewChild('emojiPickerContainer', { read: ViewContainerRef, static: true })
emojiPickerContainer!: ViewContainerRef;
private readonly componentLoader = inject(ComponentLoader);
showEmojiPicker(): void {
const factory = () =>
import(/* webpackChunkName: 'emoji-picker' */ './emoji-picker').then(m => ({
module: m.EmojiPickerModule.forRoot('😊'),
component: m.EmojiPickerComponent,
}));
this.componentLoader
.resolveComponentAndNgModuleRef(factory)
.subscribe(({ component, ngModuleRef }) => {
const { changeDetectorRef } = this.emojiPickerContainer.createComponent(component, {
ngModuleRef,
});
changeDetectorRef.detectChanges();
});
}
What if we want to load standalone components? Well, we can extend the existing ComponentLoader functionality, but that might appear complicated. Alternatively, you can load standalone components directly without any helper class and render them on the ViewContainerRef.
# CSS
Some packages require importing CSS into the main styles file. For instance, we'd want to lazy load and render the ngx-quill:
@Component({
selector: 'app-root',
template: `<ng-template #quillContainer></ng-template>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnDestroy {
@ViewChild('quillContainer', { static: true, read: ViewContainerRef })
quillContainer!: ViewContainerRef;
private readonly componentLoader = inject(ComponentLoader);
ngOnInit(): void {
const factory = () =>
import(/* webpackChunkName: 'ngx-quill' */ 'ngx-quill').then(m => ({
module: m.QuillModule.forRoot(),
component: m.QuillEditorComponent,
}));
this.componentLoader
.resolveComponentAndNgModuleRef(factory)
.subscribe(({ component, ngModuleRef }) => {
const { changeDetectorRef } = this.quillContainer.createComponent(component, {
ngModuleRef,
});
changeDetectorRef.detectChanges();
});
}
}
Well, it requires manually importing the following CSS:
@import '~quill/dist/quill.core.css';
@import '~quill/dist/quill.bubble.css';
@import '~quill/dist/quill.snow.css';
We don't want these styles to be bundled until the ngx-quill is rendered. We can also lazily load them through dynamic import. Let's create the NgxQuillLoader service:
@Injectable({ providedIn: 'root' })
export class NgxQuillLoader {
private readonly factory = () =>
import(/* webpackChunkName: 'ngx-quill' */ 'ngx-quill').then(m => ({
module: m.QuillModule.forRoot(),
component: m.QuillEditorComponent,
}));
private readonly componentAndNgModuleRef$ = defer(() =>
forkJoin([
this.componentLoader.resolveComponentAndNgModuleRef(this.factory),
import('!!raw-loader!quill/dist/quill.core.css'),
import('!!raw-loader!quill/dist/quill.bubble.css'),
import('!!raw-loader!quill/dist/quill.snow.css'),
]),
).pipe(
map(([componentAndNgModuleRef, quillCore, quillBubble, quillSnow]) => {
for (const css of [quillCore.default, quillBubble.default, quillSnow.default]) {
const style = document.createElement('style');
style.innerHTML = css;
document.head.appendChild(style);
}
return componentAndNgModuleRef;
}),
shareReplay({ bufferSize: 1, refCount: true }),
);
private readonly componentLoader = inject(ComponentLoader);
resolveComponentAndNgModuleRef() {
return this.componentAndNgModuleRef$;
}
}
This will be possible to override this service inside tests.
We should also extend custom typings:
// typings.d.ts
declare module '*.css' {
const css: string;
export default css;
}
Now let's use the service:
@Component({
selector: 'app-root',
template: `<ng-template #quillContainer></ng-template>`,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class AppComponent implements OnDestroy {
@ViewChild('quillContainer', { static: true, read: ViewContainerRef })
quillContainer!: ViewContainerRef;
private readonly ngxQuillLoader = inject(NgxQuillLoader);
ngOnInit(): void {
this.ngxQuillLoader.resolveComponentAndNgModuleRef().subscribe(({ component, ngModuleRef }) => {
const { changeDetectorRef } = this.quillContainer.createComponent(component, { ngModuleRef });
changeDetectorRef.detectChanges();
});
}
}
You can also prefetch those styles. The browser will fetch them while idling and retrieve them from the cache later on:
import(/* webpackPrefetch: true */ '!!raw-loader!quill/dist/quill.core.css');
# Lazy-Loading Components When They Enter the Viewport
We can leverage the power of IntersectionObserver and create a directive that will lazily load and render components when they enter the viewport.
@Directive({
selector: 'app-intersection-renderer[component]',
standalone: true,
})
export class IntersectionRendererDirective<T> implements OnInit, OnDestroy {
@Input() component!: Observable<Type<T>>;
@Output() rendered = new EventEmitter<T>();
private readonly destroy$ = new Subject<void>();
private readonly host = inject<ElementRef<HTMLElement>>(ElementRef);
private readonly ngZone = inject(NgZone);
private readonly container = inject(ViewContainerRef);
ngOnInit(): void {
const isIntersecting$ = new Observable<void>(subscriber =>
this.ngZone.runOutsideAngular(() => {
const observer = new IntersectionObserver(entries => {
const entry = entries[entries.length - 1];
const isIntersecting = entry.isIntersecting || entry.intersectionRatio > 0;
if (isIntersecting) {
subscriber.next();
subscriber.complete();
}
});
observer.observe(this.host.nativeElement);
return () => observer.disconnect();
}),
);
isIntersecting$
.pipe(
mergeMap(() => this.component),
takeUntil(this.destroy$),
)
.subscribe(componentType => {
this.ngZone.run(() => {
const { instance, changeDetectorRef } = this.container.createComponent(componentType);
changeDetectorRef.detectChanges();
this.rendered.emit(instance);
});
});
}
ngOnDestroy(): void {
this.destroy$.next();
}
}
Now, let's use the directive:
@Component({
selector: 'app-root',
template: '<app-intersection-renderer [component]="someComponent"></app-intersection-renderer>',
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [IntersectionRendererDirective],
})
export class AppComponent {
someComponent = defer(() => import('./some.component').then(m => m.SomeComponent));
}
Since someComponent is an observable, we can place any logic there before loading the component itself. For example, we can determine if it needs to be loaded and rendered only for admin users:
export class AppComponent {
someComponent = this.store.select(isAdmin).pipe(
take(1),
mergeMap(() => import('./some.component').then(m => m.SomeComponent)),
);
constructor(private readonly store: Store) {}
}
If you want to load the component synchronously in unit tests w/o using fixture.whenStable(), then you can override the class property before running the first change detection:
describe('AppComponent', () => {
const createComponent = createComponentFactory({
component: AppComponent,
});
let spectator: Spectator<AppComponent>;
beforeEach(() => {
spectator = createComponent({ detectChanges: false });
spectator.component.someComponent = of(SomeComponent);
spectator.detectChanges();
});
});
# Lazy-Loading Form Controls
This becomes a bit tricky when it comes to lazy loading components that might contain form controls, as we need to bind the control from the parent component to the lazy-loaded component.
Let's hypothetically assume that we want to lazy load a component that bundles ngx-color-picker and includes a form control. Consider the following component:
import { ColorPickerModule } from 'ngx-color-picker';
interface ColorPickerOptions {
control: FormControl<string | null>;
defaultColor: string;
}
@Component({
selector: 'app-color-picker',
template: `
<mat-form-field *ngIf="options">
<input
matInput
[formControl]="options.control"
[colorPicker]="options.control.value || options.defaultColor"
(colorPickerChange)="options.control.markAsDirty(); options.control.setValue($event)"
/>
</mat-form-field>
`,
changeDetection: ChangeDetectionStrategy.OnPush,
standalone: true,
imports: [
CommonModule,
ReactiveFormsModule,
MatFormFieldModule,
MatInputModule,
ColorPickerModule,
],
})
export class ColorPickerComponent {
options: ColorPickerOptions | null = null;
constructor(private readonly ref: ChangeDetectorRef) {}
setOptions(options: ColorPickerOptions): void {
this.options = options;
this.ref.detectChanges();
}
}
Now let's lazy load it:
@Component({
selector: 'app-root',
template: `
<form [formGroup]="formGroup">
<ng-template #colorPickerContainer></ng-template>
</form>
`,
})
export class AppComponent implements OnInit {
@ViewChild('colorPickerContainer', { static: true, read: ViewContainerRef })
colorPickerContainer!: ViewContainerRef;
formGroup = new FormGroup({
color: new FormControl<string | null>(null),
});
ngOnInit(): void {
import(/* webpackChunkName: 'color-picker' */ './color-picker.component').then(m => {
const { instance } = this.colorPickerContainer.createComponent(m.ColorPickerComponent);
instance.setOptions({
control: this.formGroup.controls.color,
defaultColor: 'red',
});
});
}
}
# Images
Rendering images can be delayed using the loading attribute. Internally, Blink creates an IntersectionObserver for the image with the loading attribute set to a valid value. However, please note that the loading attribute will not work if the images have the hidden attribute.