Angular 13+開發(fā)模式慢的原因及構(gòu)建性能優(yōu)化解析
1 Angular 13+ 開發(fā)模式太慢的原因與解決
近期在某個(gè)高頻迭代七年的 Angular 項(xiàng)目升級(jí)至 Angular 13 后,其開發(fā)模式的構(gòu)建速度慢、資源占用高,開發(fā)體驗(yàn)相當(dāng)差。在一臺(tái)僅在開會(huì)時(shí)偶爾使用的 Macbook air(近期居家辦公期間轉(zhuǎn)換為了主要生產(chǎn)力工具) 中啟動(dòng)構(gòu)建時(shí),它的風(fēng)扇會(huì)呼呼作響,CPU 負(fù)荷被打滿,而在構(gòu)建完成后,熱更新一次的時(shí)間在一分鐘以上。
在經(jīng)過各種原因分析與排查后,最終在 angular.json 的 schema(./node_modules/@angular/cli/lib/config/schema.json) 中發(fā)現(xiàn)了問題,再結(jié)合 Angular 12 release 文檔定位到了具體原因: Angular 12 一個(gè)主要的改動(dòng)是將 aot、buildOptimizer、optimization 等參數(shù)由默認(rèn)值 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 后的默認(rèn)生產(chǎn)模式,對(duì)于跨版本升級(jí)來說是比較坑爹的。我們可以從這個(gè)提交中了解變動(dòng)細(xì)節(jié):656f8d7
1.1 解決 Angular 12+ 開發(fā)模式慢的問題
解決辦法則是在 development 配置中禁用生產(chǎn)模式相關(guān)的配置項(xiàng)。示例:
{
"$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 開啟與關(guān)閉時(shí),在構(gòu)建結(jié)果表現(xiàn)上可能會(huì)有一些差異,需視具體問題而分析。
1.2 問題:開啟 aot 后 pug 編譯報(bào)錯(cuò)
該項(xiàng)目中使用 pug 開發(fā) html 內(nèi)容。關(guān)閉 aot 時(shí)構(gòu)建正常,開啟后則會(huì)報(bào)錯(cuò)。
根據(jù)報(bào)錯(cuò)內(nèi)容及位置進(jìn)行 debugger 調(diào)試,可以看到其編譯結(jié)果為一個(gè) esModule 的對(duì)象。這是由于使用了 raw-loader,其編譯結(jié)果默認(rèn)為 esModule 模式,禁用 esModule 配置項(xiàng)即可。示例(自定義 webpack 配置可參考下文的 dll 配置相關(guān)示例):
{
test: /\.pug$/,
use: [
{
loader: 'raw-loader',
options: {
esModule: false,
},
},
{
loader: 'pug-html-loader',
options: {
doctype: 'html',
},
},
],
},
2 進(jìn)一步優(yōu)化:Angular 自定義 webpack 配置 dll 支持
該項(xiàng)目項(xiàng)目構(gòu)建上有自定義 webpack 配置的需求,使用了 @angular-builders/custom-webpack 庫實(shí)現(xiàn),但是沒有配置 dll。
Angular 提供了 vendorChunk 參數(shù),開啟它會(huì)提取在 package.json 中的依賴等公共資源至獨(dú)立 chunk 中,其可以很好的解決熱更新 bundles 過大導(dǎo)致熱更新太慢等的問題,但仍然存在較高的內(nèi)存占用,而且實(shí)際的對(duì)比測(cè)試中,在存在 webpack5 緩存的情況下,其相比 dll 模式的構(gòu)建編譯速度以及熱更新速度都稍微慢一些。故對(duì)于開發(fā)機(jī)器性能一般的情況下,給開發(fā)模式配置 dll 是會(huì)帶來一定的收益的。
2.1 Angular 支持自定義 webpack 配置
首先需要配置自定義 webpack 配置的構(gòu)建支持。執(zhí)行如下命令添加依賴:
npm i -D @angular-builders/custom-webpack
修改 angluar.json 配置。內(nèi)容格式參考:
{
"$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
}
}
}
該示例中涉及多處自定義配置內(nèi)容,主要需注意 webpack 相關(guān)的部分, 其他內(nèi)容可視自身項(xiàng)目具體情況對(duì)比參考。一些細(xì)節(jié)也可參考以前的這篇文章中的實(shí)踐介紹:lzw.me/a/update-to…
2.2 為 Angular 配置 webpack dll 支持
新建 webpack.config.js 文件。內(nèi)容參考:
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 構(gòu)建。內(nèi)容示例:
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 中的配置內(nèi)容格式。
在 package.json 中增加啟動(dòng)腳本配置。示例:
{
"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",
}
}
最后,可執(zhí)行 npm run dev 測(cè)試效果是否符合預(yù)期。
3 小結(jié)
angular-cli 在升級(jí)至 webpack 5 以后,基于 webpack 5 的緩存能力做了許多編譯優(yōu)化,一般情況下開發(fā)模式二次構(gòu)建速度相比之前會(huì)有大幅的提升。但是相比 snowpack 和 vite 一類的 esm no bundles 方案仍有較大的差距。其從 Angular 13 開始已經(jīng)在嘗試引入 esbuild,但由于其高度定制化的構(gòu)建邏輯適配等問題,對(duì)一些配置參數(shù)的兼容支持相對(duì)較為復(fù)雜。在 Angular 15 中已經(jīng)可以進(jìn)行生產(chǎn)級(jí)配置嘗試了,有興趣也可作升級(jí)配置與嘗試。
以上就是Angular 13+開發(fā)模式慢的原因及構(gòu)建性能優(yōu)化解析的詳細(xì)內(nèi)容,更多關(guān)于Angular 13+性能優(yōu)化的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
angularjs中判斷ng-repeat是否迭代完的實(shí)例
今天小編就為大家分享一篇angularjs中判斷ng-repeat是否迭代完的實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧2018-09-09
Angularjs material 實(shí)現(xiàn)搜索框功能
這篇文章主要介紹了Angularjs material 實(shí)現(xiàn)搜索框功能的相關(guān)資料,需要的朋友可以參考下2016-03-03
利用JavaScript的AngularJS庫制作電子名片的方法
這篇文章主要介紹了利用JavaScript的AngularJS庫制作電子名片的方法,其中需要使用到HTML5的canvas畫布,需要的朋友可以參考下2015-06-06
如何解決手機(jī)瀏覽器頁面點(diǎn)擊不跳轉(zhuǎn)瀏覽器雙擊放大網(wǎng)頁
這篇文章主要介紹了如何解決手機(jī)瀏覽器頁面點(diǎn)擊不跳轉(zhuǎn)瀏覽器雙擊放大網(wǎng)頁的相關(guān)資料,需要的朋友可以參考下2016-07-07
AngularJS基礎(chǔ) ng-class-odd 指令示例
本文主要介紹AngularJS ng-class-odd 指令,這里對(duì)ng-class-odd基礎(chǔ)知識(shí)做了詳細(xì)整理,并有示例代碼和效果圖,學(xué)習(xí)AngularJS的同學(xué)可以參考下2016-08-08
詳解關(guān)于Angular4 ng-zorro使用過程中遇到的問題
這篇文章主要介紹了詳解關(guān)于Angular4 ng-zorro使用過程中遇到的問題,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-12-12
AngularJS基礎(chǔ)學(xué)習(xí)筆記之簡單介紹
AngularJS 不僅僅是一個(gè)類庫,而是提供了一個(gè)完整的框架。它避免了您和多個(gè)類庫交互,需要熟悉多套接口的繁瑣工作。它由Google Chrome的開發(fā)人員設(shè)計(jì),引領(lǐng)著下一代Web應(yīng)用開發(fā)。也許我們5年或10年后不會(huì)使用AngularJS,但是它的設(shè)計(jì)精髓將會(huì)一直被沿用。2015-05-05

