Angular project too big? Split it up reasonably!

foreword

This article describes how to split projects reasonably, and will be discussed in subsequent articles on performance optimization.

One of the criticisms of Angular is that it is very bulky after packaging. If you are not careful main.js it will be outrageously large. In fact, if you encounter similar problems, whether it is large size, large data, or large traffic, just one idea : Split. Combined with the browser’s caching mechanism, the project access speed can be well optimized.

The relevant code of this article is at: https://github.com/Vibing/angular-webpack

split ideas

  1. The whole project includes: strong dependency library (Angular framework itself), UI component library and third-party library, business code part;
  2. User behavior dimension: All user visits are based on routes, one route per page;

It can be split from the above two points. Based on the first point, the strongly dependent library and the almost unchanged library can be packaged into one vendor_library , which can contain @angular/common , @angular/core 、 @angular/forms 、 @angular/router的包,UI lodash库不建议一起打包,因为我们要运用TreeShaking , There is no need to package unused code, otherwise it will only increase the volume.

The strong dependency package is done, and the business code is packaged based on the second idea. We use route-based code spliting for packaging. The idea is very simple, whichever page the user visits, download the js corresponding to the page, and there is no need to package the pages that have not been visited together, which will not only increase the size, but also increase the download time, and the user experience will also deteriorate. .

Custom webpack configuration

If we want to use DLL to pack strong dependencies into a vendor, we need to use the function of webpack. Angular CLI has embedded webpack, but these configurations are black boxes for us.

Angular allows us to customize the webpack configuration, the steps are as follows

  1. Install @angular-builders/custom-webpack and @angular-devkit/build-angular
  2. Create a new webpack.extra.config.ts for webpack configuration
  3. Make the following modifications in angular.json
 ...
"architect": {
  "build": {
    "builder": "@angular-builders/custom-webpack:browser",
    "options": {
      ...
      "customWebpackConfig": {
        // 引用要拓展的 webpack 配置
        "path": "./webpack.extra.config.ts",
        // 是否替换重复插件
        "replaceDuplicatePlugins": true
      }
    }
  },
  "serve": {
    "builder": "@angular-builders/custom-webpack:dev-server",
    "options": {
      "browserTarget": "angular-webpack:build"
    }
  }
  ...

use DLL

After you can customize the webpack configuration, create a new webpack.dll.js file to write the DLL configuration:

 const path = require("path");
const webpack = require("webpack");

module.exports = {
  mode: "production",
  entry: {
    vendor: [
      "@angular/platform-browser",
      "@angular/platform-browser-dynamic",
      "@angular/common",
      "@angular/core",
      "@angular/forms",
      "@angular/router"
    ],
  },
  output: {
    path: path.resolve(__dirname, "./dll"),
    filename: "[name].dll.js",
    library: "[name]_library",
  },

  plugins: [
    new webpack.DllPlugin({
      context: path.resolve(__dirname, "."),
      path: path.join(__dirname, "./dll", "[name]-manifest.json"),
      name: "[name]_library",
    }),
  ],
};

Then import the dll in webpack.extra.config.ts

 import * as path from 'path';
import * as webpack from 'webpack';

export default {
  plugins: [
    new webpack.DllReferencePlugin({
      manifest: require('./dll/vendor-manifest.json'),
      context: path.resolve(__dirname, '.'),
    })
  ],
} as webpack.Configuration;

Finally, add a command to package the dll in package.json: "dll": "rm -rf dll && webpack --config webpack.dll.js" , after executing npm run dll , there will be a dll folder at the root of the project, which is the packaged content:

After packaging, we need to use vendor.dll.js in the project, and configure it in angular.json :

 "architect": {
  ...
  "build": {
    ...
    "options": {
      ...
       "scripts": [
         {
            "input": "./dll/vendor.dll.js",
            "inject": true,
            "bundleName": "vendor_library"
         }
       ]
    }
  }
}

After packaging, you can see that talk vendor_library.js has been introduced:

The purpose of DLL is to package and merge strongly dependent packages that are not frequently updated into a js file, which is generally used to package the Angular framework itself. When the user visits for the first time, the browser will download vendor_library.js and cache it. After each visit directly from the cache, the browser will only download the js of the business code and will not download the framework-related code, which greatly improves the loading speed of the application and improves the user experience.

ps: The hash behind vendor_library will re-change the hash only if the code in it changes during packaging, otherwise it will not change.

Route-level CodeSpliting

The DLL manages the code of the framework part. Let’s see how to implement the on-demand loading of the page at the route level in Angular.

Here’s a fork, in React or Vue, how to do route-level code splitting? Probably something like this:

 {
  path:'/home',
  component: () => import('./home')
}

The home here points to the corresponding component, but this method cannot be used in Angular, and the code can only be split in modules:

 {
  path:'/home',
  loadChild: ()=> import('./home.module').then(m => m.HomeModule)
}

Then use routes in specific modules to access specific components:

 import { HomeComponent } from './home.component'

{
  path:'',
  component: HomeComponent
}

Although it cannot be directly in the router import() component, but Angular provides the function of dynamic import of components :

 @Component({
  selector: 'app-home',
  template: ``,
})
export class HomeContainerComponent implements OnInit {
  constructor(
      private vcref: ViewContainerRef,
      private cfr: ComponentFactoryResolver
  ){}
  
  ngOnInit(){
    this.loadGreetComponent()
  }

  async loadGreetComponent(){
      this.vcref.clear();
      // 使用 import() 懒加载组件
      const { HomeComponent } = await import('./home.component');
      let createdComponent = this.vcref.createComponent(
        this.cfr.resolveComponentFactory(HomeComponent)
      );  
  }
}

In this way, when a page is accessed by routing, as long as the content of the accessed page is dynamically imported using import() with the component , can’t the effect of page lazyLoad be achieved?

The answer is yes. But this will have a big problem: in the lazyLoaded component, the content is only the code of the current component, and does not contain the code of the components in other modules that are referenced.

The reason is that the Angular application consists of multiple modules, and the functions required in each module may come from other modules. For example, the table component is used in module A, and the table needs to be taken from ng-zorro-antd/table module. Unlike React or Vue, Angular can package the current component with other packages that are used when packaging. Take React as an example: a table component is introduced in component A, and the table code will be packaged into component A during packaging. In Angular, when the table component is used in the A component, and imprt() is used to dynamically load the A component, the packaged A component does not contain the table code, but will package the table code to the current If a module contains multiple pages, and so many pages use a lot of UI components, the packaged module will definitely be large.

So is there no other way? The answer is yes, that is to split each page into a module, and other modules or components used by each page are undertaken by the module corresponding to the current page.

In the above picture dashboard as a module, there are two pages under it, namely monitor and welcome

dashboard.module.ts :

 import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  {
    path: 'welcome',
    loadChildren: () => import('./welcome/welcome.module').then((m) => m.WelcomeModule),
  },
  {
    path: 'monitor',
    loadChildren: () => import('./monitor/monitor.module').then((m) => m.MonitorModule),
  },
];

@NgModule({
  imports: [CommonModule, RouterModule.forChild(routes)],
  exports: [RouterModule],
  declarations: [],
})
export class DashboardModule {}

Use the route loadChildren in the module to lazyLoad the two page modules, now look at the WelcomeModule:

 import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { WelcomeComponent } from './welcome.component';
import { RouterModule, Routes } from '@angular/router';

const routes: Routes = [
  { path: '', component: WelcomeComponent }
];
@NgModule({
  declarations: [WelcomeComponent],
  imports: [RouterModule.forChild(routes), CommonModule]
})
export class WelcomeModule {}

It’s that simple, just complete the page-level lazyLoad. When you need to use external components, such as table components, just import them in imports:

 import { NzTableModule } from 'ng-zorro-antd/table';

@NgModule({
  ...
  imports: [..., NzTableModule]
})
export class WelcomeModule {}

Off topic: I prefer the split method of React. For example: React uses the table component. The table component itself has a large amount of code. If many pages use tables, then each page will have table code, causing unnecessary waste. So you can cooperate with import() to pull out the table component, and package it table as a separate js to be downloaded by the browser and provided to — c461c9317afedbab4 All pages share this copy js . But Angular can’t, it can’t use a module of imports in a module’s —dd160df4da7099e3e2fb06328050ca0d import() .

follow-up

The above is a reasonable split of the project code. In the future, the performance of Angular will be optimized reasonably, mainly from the perspectives of compilation mode, change detection, ngFor, Worker and so on. I will also write a separate article on Angular state management