Dynamically Importing Highcharts
In a previous post, I wrote about Lazy Loading Angular Libraries and how I used Webpack Bundle Analyzer to help visualise my project's bundle sizes and Chrome DevTools Network panel to inspect the properties of individual resources.
My goal being to ensure that my App's main bundle was as small as possible.
While working towards this goal I noticed that Highcharts was being included in the main bundle.
Highcharts Angular
Highcharts Angular is the official Angular wrapper for Highcharts. I use it in my project to provide Dashboard widgets:
Highcharts Chart Component
Highcharts Angular is a lightweight wrapper that provides one component, the HighchartsChartComponent.
At the start of the file you will notice the following import statement:
import * as Highcharts from 'highcharts';
However, as the HighchartsChartComponent accepts Highcharts as an @Input()
I commented out this line and I updated all references to Highcharts types to <any>:
...
// import * as Highcharts from 'highcharts';
@Component({
selector: 'highcharts-chart',
template: ''
})
export class HighchartsChartComponent implements OnDestroy {
// @Input() Highcharts: typeof Highcharts;
@Input() highcharts: any;
@Input() constructorType: string;
// @Input() callbackFunction: Highcharts.ChartCallbackFunction;
@Input() callbackFunction: any;
@Input() oneToOne: boolean;
@Input() runOutsideAngular: boolean;
// @Input() set options(val: Highcharts.Options) {
@Input() set options(val: any) {
this.optionsValue = val;
this.wrappedUpdateOrCreateChart();
}
...
}
Pie Chart Component
At the start of the file for my PieChartComponent (just like the HighchartsChartComponent) you will notice the following import statement:
import * as Highcharts from 'highcharts';
Now, what we want to do is take advantage of ECMAScript's support for dynamic import() in order to load a script dynamically at runtime.
As we're using Angular and the Angular CLI uses webpack, we can rely on webpack to replace calls to import() with its own dynamic loading function.
Dynamic import()
To start using dynamic import() I updated my PieChartComponent as follows:
import { Component, OnInit } from '@angular/core';
import { normalizeCommonJSImport } from '../../normalizeCommonJSImport';
const loadHighcharts = normalizeCommonJSImport(
import('highcharts'),
);
@Component({
selector: 'widget-pie-chart',
template: `
<ng-container *ngIf="highcharts">
<highcharts-chart
[highcharts]="highcharts"
[options]="chartOptions">
</highcharts-chart>
</ng-container>
`
})
export class PieChartComponent implements OnInit {
highcharts: any;
chartOptions: any = {
...
};
...
public async ngOnInit() {
this.highcharts = await loadHighcharts;
}
}
...
Now if we rebuild the App ("target": "es2015") and launch the Webpack Bundle Analyzer:
ng build --prod --named-chunks --stats-json && ./node_modules/webpack-bundle-analyzer/lib/bin/analyzer.js ./dist/serendipity/stats-es2015.json
We can see that Highcharts is no longer included in the App's main bundle!
Note: normalizeCommonJSImport
is a utility function that deals with the inconsistencies between CommonJS and ES modules:
export function normalizeCommonJSImport<T>(
importPromise: Promise<T>,
): Promise<T> {
// CommonJS's `module.exports` is wrapped as `default` in ESModule.
return importPromise.then((m: any) => (m.default || m) as T);
}
See: Angular - Dynamic Importing Large Libraries
Source Code:
- GitHub: Serendipity
References:
- v8.dev: Dynamic import()
- MDN docs: import
- Suguru Inatomi's (Lacolaco) blog: Angular - Dynamic Importing Large Libraries