Angular13+ 開發模式太慢怎麼辦?原因與解決方法介紹

2022-12-22 22:00:29
Angular13+ 開發模式太慢怎麼辦?下面本篇文章給大家介紹一下Angular 13+ 開發模式太慢的原因與構建效能優化的方法,希望對大家有所幫助!

1 Angular 13+ 開發模式太慢的原因與解決

近期在某個高頻迭代七年的 Angular 專案升級至 Angular 13 後,其開發模式的構建速度慢、資源佔用高,開發體驗相當差。在一臺僅在開會時偶爾使用的 Macbook air(近期居家辦公期間轉換為了主要生產力工具) 中啟動構建時,它的風扇會呼呼作響,CPU 負荷被打滿,而在構建完成後,熱更新一次的時間在一分鐘以上。【相關教學推薦:《》】

在經過各種原因分析與排查後,最終在 angular.json 的 schema(./node_modules/@angular/cli/lib/config/schema.json) 中發現了問題,再結合 定位到了具體原因: Angular 12 一個主要的改動是將 aotbuildOptimizeroptimization 等引數由預設值 false 改為了 true

A number of browser and server builder options have had their default values changed. The aim of these changes is to reduce the configuration complexity and support the new "production builds by default" initiative.

可以看到 Angular 12 後的預設生產模式,對於跨版本升級來說是比較坑爹的。我們可以從這個提交中瞭解變動細節:

1.1 解決 Angular 12+ 開發模式慢的問題

解決辦法則是在 development 設定中禁用生產模式相關的設定項。範例:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "projects": {
    "front": {
      "architect": {
        "build": {
          "configurations": {
            "development": {
              "tsConfig": "./tsconfig.dev.json",
              "aot": false,
              "buildOptimizer": false,
              "optimization": false,
              "extractLicenses": false,
              "sourceMap": true,
              "vendorChunk": true,
              "namedChunks": true
            }
          }
        },
    }
  },
  "defaultProject": "front"
}
登入後複製

需注意 aot 開啟與關閉時,在構建結果表現上可能會有一些差異,需視具體問題而分析。

1.2 問題:開啟 aotpug 編譯報錯

該專案中使用 pug 開發 html 內容。關閉 aot 時構建正常,開啟後則會報錯。

根據報錯內容及位置進行 debugger 偵錯,可以看到其編譯結果為一個 esModule 的物件。這是由於使用了 raw-loader,其編譯結果預設為 esModule 模式,禁用 esModule 設定項即可。範例(自定義 webpack 設定可參考下文的 dll 設定相關範例):

{
  test: /\.pug$/,
  use: [
    {
      loader: 'raw-loader',
      options: {
        esModule: false,
      },
    },
    {
      loader: 'pug-html-loader',
      options: {
        doctype: 'html',
      },
    },
  ],
},
登入後複製

2 進一步優化:Angular 自定義 webpack 設定 dll 支援

該專案專案構建上有自定義 webpack 設定的需求,使用了 @angular-builders/custom-webpack 庫實現,但是沒有設定 dll。

Angular 提供了 vendorChunk 引數,開啟它會提取在 package.json 中的依賴等公共資源至獨立 chunk 中,其可以很好的解決熱更新 bundles 過大導致熱更新太慢等的問題,但仍然存在較高的記憶體佔用,而且實際的對比測試中,在存在 webpack5 快取的情況下,其相比 dll 模式的構建編譯速度以及熱更新速度都稍微慢一些。故對於開發機器效能一般的情況下,給開發模式設定 dll 是會帶來一定的收益的。

2.1 Angular 支援自定義 webpack 設定

首先需要設定自定義 webpack 設定的構建支援。執行如下命令新增依賴:

npm i -D @angular-builders/custom-webpack
登入後複製

修改 angluar.json 設定。內容格式參考:

{
  "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
  "cli": {
    "analytics": false,
    "cache": {
      "path": "node_modules/.cache/ng"
    }
  },
  "version": 1,
  "newProjectRoot": "projects",
  "projects": {
    "front": {
      "root": "",
      "sourceRoot": "src",
      "projectType": "application",
      "prefix": "app",
      "schematics": {
        "@schematics/angular:component": {
          "style": "less"
        }
      },
      "architect": {
        "build": {
          "builder": "@angular-builders/custom-webpack:browser",
          "options": {
            "customWebpackConfig": {
              "path": "./webpack.config.js"
            },
            "indexTransform": "scripts/index-html-transform.js",
            "outputHashing": "media",
            "deleteOutputPath": true,
            "watch": true,
            "sourceMap": false,
            "outputPath": "dist/dev",
            "index": "src/index.html",
            "main": "src/app-main.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "./tsconfig.app.json",
            "baseHref": "./",
            "assets": [
              "src/assets/",
              {
                "glob": "**/*",
                "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
                "output": "/assets/"
              }
            ],
            "styles": [
              "node_modules/angular-tree-component/dist/angular-tree-component.css",
              "src/css/index.less"
            ],
            "scripts": []
          },
          "configurations": {
            "development": {
              "tsConfig": "./tsconfig.dev.json",
              "buildOptimizer": false,
              "optimization": false,
              "aot": false,
              "extractLicenses": false,
              "sourceMap": true,
              "vendorChunk": true,
              "namedChunks": true,
              "scripts": [
                {
                  "inject": true,
                  "input": "./dist/dll/dll.js",
                  "bundleName": "dll_library"
                }
              ]
            },
            "production": {
              "outputPath": "dist/prod",
              "baseHref": "./",
              "watch": false,
              "fileReplacements": [
                {
                  "replace": "src/environments/environment.ts",
                  "with": "src/environments/environment.prod.ts"
                }
              ],
              "optimization": {
                "scripts": true,
                "styles": {
                  "minify": true,
                  "inlineCritical": false
                },
                "fonts": true
              },
              "outputHashing": "all",
              "sourceMap": false,
              "namedChunks": false,
              "aot": true,
              "extractLicenses": false,
              "vendorChunk": false,
              "buildOptimizer": true
            }
          },
          "defaultConfiguration": "production"
        },
        "serve": {
          "builder": "@angular-builders/custom-webpack:dev-server",
          "options": {
            "browserTarget": "front:build",
            "liveReload": false,
            "open": false,
            "host": "0.0.0.0",
            "port": 3002,
            "servePath": "/",
            "publicHost": "localhost.gf.com.cn",
            "proxyConfig": "config/ngcli-proxy-config.js",
            "disableHostCheck": true
          },
          "configurations": {
            "production": {
              "browserTarget": "front:build:production"
            },
            "development": {
              "browserTarget": "front:build:development"
            }
          },
          "defaultConfiguration": "development"
        },
        "test": {
          "builder": "@angular-builders/custom-webpack:karma",
          "options": {
            "customWebpackConfig": {
              "path": "./webpack.test.config.js"
            },
            "indexTransform": "scripts/index-html-transform.js",
            "main": "src/ngtest.ts",
            "polyfills": "src/polyfills.ts",
            "tsConfig": "./tsconfig.spec.json",
            "karmaConfig": "./karma.conf.js",
            "assets": [
              "src/assets/",
              {
                "glob": "**/*",
                "input": "./node_modules/@ant-design/icons-angular/src/inline-svg/",
                "output": "/assets/"
              }
            ],
            "styles": [
              "node_modules/angular-tree-component/dist/angular-tree-component.css",
              "src/css/index.less"
            ],
            "scripts": []
          }
        }
      }
    }
  },
  "defaultProject": "front",
  "schematics": {
    "@schematics/angular:module": {
      "routing": true,
      "spec": false
    },
    "@schematics/angular:component": {
      "flat": false,
      "inlineStyle": true,
      "inlineTemplate": false
    }
  }
}
登入後複製

該範例中涉及多處自定義設定內容,主要需注意 webpack 相關的部分, 其他內容可視自身專案具體情況對比參考。一些細節也可參考以前的這篇文章中的實踐介紹:

2.2 為 Angular 設定 webpack dll 支援

新建 webpack.config.js 檔案。內容參考:

const { existsSync } = require('node:fs');
const { resolve } = require('node:path');
const webpack = require('webpack');

// require('events').EventEmitter.defaultMaxListeners = 0;

/**
 * @param {import('webpack').Configuration} config
 * @param {import('@angular-builders/custom-webpack').CustomWebpackBrowserSchema} options
 * @param {import('@angular-builders/custom-webpack').TargetOptions} targetOptions
 */
module.exports = (config, options, targetOptions) => {
  if (!config.devServer) config.devServer = {};

  config.plugins.push(
    new webpack.DefinePlugin({ LZWME_DEV: config.mode === 'development' }),
  );

  const dllDir = resolve(__dirname, './dist/dll');
  if (
    existsSync(dllDir) &&
    config.mode === 'development' &&
    options.scripts?.some((d) => d.bundleName === 'dll_library')
  ) {
    console.log('use dll:', dllDir);
    config.plugins.unshift(
      new webpack.DllReferencePlugin({
        manifest: require(resolve(dllDir, 'dll-manifest.json')),
        context: __dirname,
      })
    );
  }

  config.module.rules = config.module.rules.filter((d) => {
    if (d.test instanceof RegExp) {
      // 使用 less,移除 sass/stylus loader
      return !(d.test.test('x.sass') || d.test.test('x.scss') || d.test.test('x.styl'));
    }
    return true;
  });

  config.module.rules.unshift(
    {
      test: /\.pug$/,
      use: [
        {
          loader: 'raw-loader',
          options: {
            esModule: false,
          },
        },
        {
          loader: 'pug-html-loader',
          options: {
            doctype: 'html',
          },
        },
      ],
    },
    {
      test: /\.html$/,
      loader: 'raw-loader',
      exclude: [helpers.root('src/index.html')],
    },
    {
      test: /\.svg$/,
      loader: 'raw-loader',
    },
    {
      test: /\.(t|les)s/,
      loader: require.resolve('@lzwme/strip-loader'),
      exclude: /node_modules/,
      options: {
        disabled: config.mode !== 'production',
      },
    }
  );

  // AngularWebpackPlugin,用於自定義 index.html 處理外掛
  const awPlugin = config.plugins.find((p) => p.options?.hasOwnProperty('directTemplateLoading'));
  if (awPlugin) awPlugin.pluginOptions.directTemplateLoading = false;

  // 相容上古遺傳邏輯,禁用部分外掛
  config.plugins = config.plugins.filter((plugin) => {
    const pluginName = plugin.constructor.name;
    if (/CircularDependency|CommonJsUsageWarnPlugin/.test(pluginName)) {
      console.log('[webpack][plugin] disabled: ', pluginName);
      return false;
    }

    return true;
  });
  // console.log('[webpack][config]', config.mode, config, options, targetOptions);
  return config;
};
登入後複製

新建 webpack.dll.mjs 檔案,用於 dll 構建。內容範例:

import { join } from 'node:path';
import webpack from 'webpack';

const rootDir = process.cwd();
const isDev = process.argv.slice(2).includes('--dev') || process.env.NODE_ENV === 'development';

/** @type {import('webpack').Configuration} */
const config = {
  context: rootDir,
  mode: isDev ? 'development' : 'production',
  entry: {
    dll: [
      '@angular/common',
      '@angular/core',
      '@angular/forms',
      '@angular/platform-browser',
      '@angular/platform-browser-dynamic',
      '@angular/router',
      '@lzwme/asmd-calc',
      // more...
    ],
  },
  output: {
    path: join(rootDir, 'dist/dll'),
    filename: 'dll.js',
    library: '[name]_library',
  },
  plugins: [
    new webpack.DllPlugin({
      path: join(rootDir, 'dist/dll/[name]-manifest.json'),
      name: '[name]_library',
    }),
    new webpack.IgnorePlugin({
      resourceRegExp: /^\.\/locale$/,
      contextRegExp: /moment$/,
    }),
  ],
  cache: { type: 'filesystem' },
};

webpack(config).run((err, result) => {
  console.log(err ? `Failed!` : `Success!`, err || `${result.endTime - result.startTime}ms`);
});
登入後複製

angular.json 中新增 dll.js 檔案的注入設定,可參考前文範例中 development.scripts 中的設定內容格式。

package.json 中增加啟動指令碼設定。範例:

{
    "scripts": {
        "ng:serve": "node --max_old_space_size=8192 node_modules/@angular/cli/bin/ng serve",
        "dll": "node config/webpack.dll.mjs",
        "dev": "npm run dll -- --dev && npm run ng:serve -- -c development",
    }
}
登入後複製

最後,可執行 npm run dev 測試效果是否符合預期。

3 小結

angular-cli 在升級至 webpack 5 以後,基於 webpack 5 的快取能力做了許多編譯優化,一般情況下開發模式二次構建速度相比之前會有大幅的提升。但是相比 snowpackvite 一類的 esm no bundles 方案仍有較大的差距。其從 Angular 13 開始已經在嘗試引入 esbuild,但由於其高度客製化化的構建邏輯適配等問題,對一些設定引數的相容支援相對較為複雜。在 Angular 15 中已經可以進行生產級設定嘗試了,有興趣也可作升級設定與嘗試。

更多程式設計相關知識,請存取:!!

以上就是Angular13+ 開發模式太慢怎麼辦?原因與解決方法介紹的詳細內容,更多請關注TW511.COM其它相關文章!