MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

优化Angular生产版本的打包体积

2022-09-013.2k 阅读

一、了解 Angular 打包原理

在深入探讨如何优化 Angular 生产版本的打包体积之前,我们首先需要理解 Angular 的打包原理。Angular CLI 使用 Webpack 作为其底层的打包工具。Webpack 是一个现代 JavaScript 应用程序的静态模块打包器。它将应用程序中的各种模块(如 JavaScript、CSS、HTML 等),通过 loader 和 plugin 进行转换和处理,最终打包成在浏览器中可运行的文件。

1.1 模块解析

Angular 应用是基于模块化开发的,Webpack 会从应用的入口文件(通常是 main.ts)开始,根据 ES6 的 import 语句和 Angular 的模块系统来解析整个应用的依赖关系树。例如,当我们在一个组件中导入另一个组件:

import { Component } from '@angular/core';
import { AnotherComponent } from './another.component';

@Component({
  selector: 'app-my-component',
  templateUrl: './my-component.html',
  styleUrls: ['./my-component.css']
})
export class MyComponent {
  // 组件逻辑
}

Webpack 会追踪 AnotherComponent 的导入,并将其相关的代码包含进打包文件中。如果 AnotherComponent 又有自己的依赖,Webpack 会继续递归解析这些依赖。

1.2 代码转换

在解析模块的过程中,Webpack 会使用各种 loader 对不同类型的文件进行转换。对于 TypeScript 文件,Angular CLI 会使用 @angular - cli 提供的 @angular - cli - webpack 插件以及 ts - loaderts - loader 会将 TypeScript 代码转换为 JavaScript 代码,同时进行类型检查。例如,下面是一个简单的 TypeScript 代码片段:

function addNumbers(a: number, b: number): number {
  return a + b;
}

经过 ts - loader 转换后,会生成等价的 JavaScript 代码,这部分代码可以在浏览器中直接运行。对于 CSS 和 HTML 文件,也有相应的 loader 进行处理。css - loader 会将 CSS 文件解析为 JavaScript 模块,html - loader 则处理 HTML 文件,通常将其内容作为字符串导入到 JavaScript 模块中。

1.3 打包输出

Webpack 在完成所有模块的解析和转换后,会根据配置将所有相关的代码打包成一个或多个输出文件。在 Angular 应用中,通常会生成一个或多个 JavaScript 文件(如 main.jsvendor.jspolyfills.js 等),以及相关的 CSS 和 HTML 文件。这些文件会被放置在指定的输出目录中(默认为 dist 目录),用于部署到生产环境。例如,在 angular.json 文件中,可以配置输出目录和其他打包相关的选项:

{
  "architect": {
    "build": {
      "outputPath": "./dist/my - app",
      // 其他打包配置选项
    }
  }
}

二、优化策略

2.1 懒加载模块

懒加载是一种强大的优化技术,它允许我们将应用的某些模块在需要时才加载,而不是在应用启动时一次性加载所有模块。这可以显著减少初始打包体积,提高应用的加载速度。

2.1.1 配置懒加载模块

在 Angular 中,配置懒加载模块非常简单。假设我们有一个 FeatureModule 模块,我们希望将其设置为懒加载。首先,我们需要在路由配置中使用 loadChildren 语法。例如,在 app - routing.module.ts 文件中:

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

const routes: Routes = [
  {
    path: 'feature',
    loadChildren: () => import('./feature/feature.module').then(m => m.FeatureModule)
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule {}

在上面的代码中,loadChildren 使用了动态导入(ES2020 的 import() 语法)。当用户访问 /feature 路由时,Angular 才会加载 FeatureModule 模块及其所有依赖。

2.1.2 懒加载的优势

懒加载不仅减少了初始打包体积,还可以提高应用的性能。因为只有当用户实际需要某个功能时,才会加载相关的代码。这对于大型应用尤其重要,因为可能有一些功能用户很少使用,如果这些功能的代码一开始就被加载,会增加应用的启动时间和带宽消耗。例如,一个电商应用可能有一个“管理订单”的功能,只有管理员用户才会使用。通过懒加载这个功能模块,普通用户在访问应用时就不需要加载这部分代码,从而加快了应用的加载速度。

2.2 移除未使用的代码(Tree - shaking)

Tree - shaking 是一种通过分析代码的导入和导出关系,移除未使用代码的技术。在 Angular 应用中,Webpack 会自动尝试进行 Tree - shaking,但我们需要确保代码的编写方式符合 Tree - shaking 的要求。

2.2.1 ES6 模块与 Tree - shaking

ES6 模块是 Tree - shaking 的基础。因为 ES6 模块的导入和导出是静态的,Webpack 可以在编译时分析哪些模块和导出是未使用的,并将其从打包文件中移除。例如,考虑以下代码:

// utils.ts
export function addNumbers(a: number, b: number): number {
  return a + b;
}

export function subtractNumbers(a: number, b: number): number {
  return a - b;
}

// main.ts
import { addNumbers } from './utils';

console.log(addNumbers(2, 3));

在上面的代码中,subtractNumbers 函数在 main.ts 中没有被使用。Webpack 在打包时,会分析 utils.ts 的导出和 main.ts 的导入,发现 subtractNumbers 未被使用,从而将其从打包文件中移除。

2.2.2 注意事项

然而,在 Angular 应用中,有些情况可能会影响 Tree - shaking 的效果。例如,使用动态导入(除了懒加载模块中的动态导入)可能会阻止 Tree - shaking。因为动态导入是在运行时解析的,Webpack 无法在编译时确定哪些代码是未使用的。另外,如果我们在模块中使用了副作用代码(例如,在模块顶层执行一些会影响外部状态的代码),也可能会影响 Tree - shaking。例如:

// side - effect.ts
let globalVar = 0;
export function incrementGlobalVar() {
  globalVar++;
  return globalVar;
}

// main.ts
import './side - effect';
// 这里虽然没有直接使用 incrementGlobalVar,但导入了 side - effect.ts,
// 由于存在副作用代码,Webpack 可能无法对 side - effect.ts 进行 Tree - shaking

为了确保 Tree - shaking 正常工作,我们应该尽量避免使用会阻止它的代码模式,并且确保我们的模块设计是基于 ES6 模块的最佳实践。

2.3 优化第三方库的使用

在 Angular 应用中,我们经常会使用各种第三方库来扩展应用的功能。然而,一些第三方库可能体积较大,如果不进行优化,会显著增加打包体积。

2.3.1 选择轻量级库

在选择第三方库时,我们应该优先选择轻量级的库。例如,如果我们需要一个图表库,有很多不同的选择,如 Chart.js、D3.js 等。Chart.js 相对来说体积较小,对于一些简单的图表需求,它可能是更好的选择。我们可以通过查看库的文档、npm 包大小以及社区评价来评估库的轻量级程度。

2.3.2 只导入需要的部分

很多第三方库都提供了模块化的导入方式,我们应该只导入我们实际需要的部分。例如,对于 Lodash 库,它是一个功能丰富的 JavaScript 工具库。如果我们只需要使用其中的 debounce 函数,我们可以这样导入:

import { debounce } from 'lodash';

// 使用 debounce 函数
const debouncedFunction = debounce(() => {
  console.log('Debounced function called');
}, 300);

而不是导入整个 Lodash 库:

import _ from 'lodash';
// 只使用了 debounce 函数,但导入了整个库,增加了打包体积
const debouncedFunction = _.debounce(() => {
  console.log('Debounced function called');
}, 300);

通过只导入需要的部分,我们可以减少引入的代码量,从而优化打包体积。

2.3.3 分析库的依赖

一些第三方库可能有自己的依赖,我们需要分析这些依赖,确保它们不会引入过多不必要的代码。有时候,一个库可能依赖于其他较大的库,而我们实际上只需要其中的一小部分功能。在这种情况下,我们可以考虑寻找替代方案,或者尝试直接使用依赖库中我们需要的部分,而不是通过中间库来间接引入。例如,某个库依赖于一个大型的 UI 框架,但我们只需要该库的一个简单的数据处理功能,此时我们可以检查是否可以直接使用数据处理功能所在的子模块,而避免引入整个 UI 框架。

2.4 优化 CSS 打包

CSS 在 Angular 应用中也占据一定的体积,优化 CSS 打包可以进一步减少整体打包体积。

2.4.1 代码压缩

Angular CLI 在生产模式下会默认对 CSS 进行压缩。压缩 CSS 可以移除不必要的空格、注释等,从而减小文件大小。例如,下面是一段原始的 CSS 代码:

/* 这是一个注释 */
body {
  background - color: #f0f0f0;
  font - size: 16px;
}

经过压缩后,会变成:

body{background - color:#f0f0f0;font - size:16px}

虽然压缩后的代码可读性降低,但在生产环境中,这并不重要,重要的是文件大小的减小。我们可以通过在 angular.json 文件中配置 optimization.minimize 选项来控制 CSS 的压缩。默认情况下,该选项在生产模式下是开启的:

{
  "architect": {
    "build": {
      "optimization": {
        "minimize": true,
        // 其他优化选项
      },
      // 其他构建配置
    }
  }
}

2.4.2 移除未使用的 CSS

类似于 JavaScript 的 Tree - shaking,我们也可以尝试移除未使用的 CSS。有一些工具可以帮助我们实现这一点,如 PurgeCSS。PurgeCSS 可以分析 HTML 和 JavaScript 文件,找出哪些 CSS 规则实际上被使用了,然后移除未使用的规则。要在 Angular 项目中使用 PurgeCSS,我们可以安装 @fullhuman/postcss - purgecss 包,并在 postcss.config.js 文件中进行配置。例如:

const purgecss = require('@fullhuman/postcss - purgecss')({
  content: ['./src/**/*.html', './src/**/*.ts'],
  defaultExtractor: content => content.match(/[\w - :/]+(?<!:)/g) || []
});

module.exports = {
  plugins: [
    require('postcss - import'),
    require('tailwindcss'),
    require('autoprefixer'),
    ...(process.env.NODE_ENV === 'production'? [purgecss] : [])
  ]
};

在上面的配置中,content 选项指定了要分析的文件路径,defaultExtractor 选项定义了如何从这些文件中提取类名等 CSS 选择器。这样,在生产构建时,PurgeCSS 会移除未使用的 CSS 规则,减小 CSS 文件的大小。

2.4.3 使用 CSS 模块

CSS 模块是一种在组件级别封装 CSS 的方式,它可以避免全局 CSS 样式冲突,并且有助于优化 CSS 体积。当我们使用 CSS 模块时,每个组件的 CSS 文件只作用于该组件。例如,我们创建一个 my - component.component.css 文件:

/* my - component.component.css */
.my - class {
  color: blue;
}

my - component.component.ts 文件中,我们可以这样使用 CSS 模块:

import { Component } from '@angular/core';
import styles from './my - component.component.css';

@Component({
  selector: 'app - my - component',
  templateUrl: './my - component.component.html',
  styleUrls: [styles]
})
export class MyComponent {
  // 组件逻辑
}

在 HTML 模板中:

<div [ngClass]="styles.my - class">这是一个使用 CSS 模块的 div</div>

这样,每个组件的 CSS 只在该组件内部生效,避免了全局样式污染,同时也使得代码结构更清晰,并且有助于在打包时只包含实际使用的 CSS 代码,优化了 CSS 打包体积。

2.5 图片优化

图片在 Angular 应用中也可能占据较大的体积,特别是在一些包含大量图片的应用中,如电商应用或图片分享应用。优化图片可以显著减少整体打包体积。

2.5.1 压缩图片

在将图片添加到项目之前,我们应该对其进行压缩。有很多工具可以用于压缩图片,如 ImageOptim(适用于 Mac)、TinyPNG(在线工具)等。压缩图片可以在不明显损失画质的情况下减小图片文件的大小。例如,一张原本 500KB 的 JPEG 图片,经过压缩后可能减小到 200KB 左右,具体减小的程度取决于图片的内容和压缩工具的设置。

2.5.2 选择合适的图片格式

不同的图片格式适用于不同的场景。例如,JPEG 格式适合用于照片等色彩丰富的图像,PNG 格式适合用于具有透明度的图像或简单的图形。对于一些图标,我们可以考虑使用 SVG 格式。SVG 是一种矢量图形格式,它的文件大小通常比位图格式(如 JPEG 和 PNG)小得多,并且无论如何缩放都不会失真。例如,我们可以将网站的 logo 等图标转换为 SVG 格式。在 Angular 应用中,我们可以直接在 HTML 中使用 SVG 文件:

<svg viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
  <path d="M10 10 L90 10 L50 90 Z" fill="blue" />
</svg>

2.5.3 图片懒加载

类似于模块的懒加载,我们也可以对图片进行懒加载。在 Angular 中,我们可以使用 ngx - lazyload - image 等库来实现图片懒加载。图片懒加载可以确保只有当图片进入浏览器的视口时才加载图片,从而减少初始页面加载时的带宽消耗。例如,在模板中使用 ngx - lazyload - image

<ngx - lazyload - image [src]="imageUrl" [loadingStrategy]="'observer'" [placeholder]="placeholderUrl"></ngx - lazyload - image>

在上面的代码中,imageUrl 是图片的实际 URL,placeholderUrl 是加载图片时显示的占位符图片的 URL,loadingStrategy 配置了懒加载的策略,这里使用了 observer 策略,它基于 Intersection Observer API 来检测图片是否进入视口。

2.6 配置 Webpack 优化

由于 Angular CLI 底层使用 Webpack,我们可以通过自定义 Webpack 配置来进一步优化打包体积。

2.6.1 安装相关工具

首先,我们需要安装 @angular - cli - webpackwebpack - merge 包。@angular - cli - webpack 允许我们使用自定义的 Webpack 配置,webpack - merge 用于合并我们的自定义配置和 Angular CLI 默认的 Webpack 配置。

npm install @angular - cli - webpack webpack - merge --save - dev

2.6.2 创建自定义 Webpack 配置文件

在项目根目录下创建一个 webpack.extra.js 文件(文件名可以自定义)。例如,我们可以在这个文件中配置 TerserPlugin 来进一步优化 JavaScript 代码的压缩。TerserPlugin 是 Webpack 内置的用于压缩 JavaScript 代码的插件。

const TerserPlugin = require('terser - webpack - plugin');

module.exports = {
  optimization: {
    minimizer: [
      new TerserPlugin({
        parallel: true,
        terserOptions: {
          compress: {
            drop_console: true // 移除 console.log 语句
          }
        }
      })
    ]
  }
};

在上面的配置中,我们启用了并行压缩(parallel: true),并且通过 terserOptions.compress.drop_console 选项移除了所有的 console.log 语句。这样可以进一步减小 JavaScript 打包文件的大小。

2.6.3 配置 Angular CLI 使用自定义 Webpack 配置

最后,我们需要在 angular.json 文件中配置 Angular CLI 使用我们的自定义 Webpack 配置。在 architect.build 部分添加以下配置:

{
  "architect": {
    "build": {
      "builder": "@angular - cli - webpack:browser",
      "options": {
        // 其他现有选项
      },
      "configurations": {
        "production": {
          "fileReplacements": [
            // 生产环境文件替换配置
          ],
          "customWebpackConfig": {
            "path": "./webpack.extra.js"
          },
          // 其他生产环境配置
        }
      }
    }
  }
}

通过以上步骤,我们就可以使用自定义的 Webpack 配置来优化 Angular 应用的打包体积。

三、性能分析与持续优化

3.1 使用分析工具

在优化 Angular 生产版本的打包体积过程中,使用分析工具是非常重要的。这些工具可以帮助我们了解打包文件的组成,找出体积较大的模块和文件,从而有针对性地进行优化。

3.1.1 Webpack Bundle Analyzer

webpack - bundle - analyzer 是一个非常有用的工具,它可以生成可视化的图表,展示打包文件中各个模块的大小和依赖关系。首先,我们需要安装这个工具:

npm install webpack - bundle - analyzer --save - dev

然后,在 angular.json 文件的 architect.build 部分添加以下配置,以便在构建时使用这个工具:

{
  "architect": {
    "build": {
      "builder": "@angular - cli - webpack:browser",
      "options": {
        // 其他现有选项
      },
      "configurations": {
        "production": {
          "fileReplacements": [
            // 生产环境文件替换配置
          ],
          "customWebpackConfig": {
            "path": "./webpack.extra.js"
          },
          "analyze": true,
          // 其他生产环境配置
        }
      }
    }
  }
}

当我们运行 ng build --prod 命令时,webpack - bundle - analyzer 会启动,并在浏览器中打开一个可视化界面,展示打包文件的详细信息。例如,我们可以看到哪些第三方库占据了较大的体积,哪些模块之间存在复杂的依赖关系等。通过分析这些信息,我们可以决定是否需要更换第三方库,或者进一步优化模块的导入和导出。

3.1.2 Lighthouse

Lighthouse 是 Google 开发的一个开源工具,用于评估网页的性能、可访问性、最佳实践等方面。它可以在 Chrome 浏览器中运行,也可以作为 Node.js 模块使用。我们可以使用 Lighthouse 来评估 Angular 应用的性能,其中包括打包体积对加载速度的影响。要在 Chrome 浏览器中使用 Lighthouse,我们可以打开应用的页面,然后按下 Ctrl + Shift + I(Windows / Linux)或 Command + Option + I(Mac)打开开发者工具,切换到“Lighthouse”标签页,点击“Generate report”按钮,Lighthouse 就会对页面进行分析,并生成一份详细的报告。报告中会指出页面加载速度慢的原因,可能包括打包体积过大等问题。我们可以根据报告中的建议进行针对性的优化。

3.2 持续优化流程

优化打包体积不是一次性的任务,而是一个持续的过程。随着项目的发展,可能会引入新的功能、第三方库等,这些都可能导致打包体积增加。因此,我们需要建立一个持续优化的流程。

3.2.1 定期性能分析

我们应该定期(例如每周或每月)使用上述分析工具对应用进行性能分析。特别是在引入新的功能模块或第三方库后,要及时进行分析,确保打包体积没有大幅增加。如果发现体积增加明显,要及时找出原因并进行优化。例如,如果新引入的一个第三方库导致打包体积增加了 500KB,我们需要分析这个库是否可以优化使用方式,或者是否有更轻量级的替代库。

3.2.2 代码审查与优化建议

在团队开发过程中,进行代码审查时,除了检查代码的质量和规范性,还应该关注代码对打包体积的影响。例如,在审查一个新的组件时,检查是否有未使用的导入,是否可以优化 CSS 的编写以减少体积等。同时,团队成员可以分享优化打包体积的经验和建议,形成一个良好的优化氛围。例如,某个成员发现了一种更有效的图片优化方法,就可以在团队中分享,让整个项目受益。

3.2.3 自动化构建与测试

我们应该设置自动化的构建和测试流程,确保每次代码提交后都能进行打包体积的检查。可以使用工具如 Jenkins、GitLab CI/CD 等。在构建过程中,除了进行常规的编译和测试,还可以运行分析工具,检查打包体积是否超过预设的阈值。如果超过阈值,自动构建流程可以失败,并通知相关开发人员进行优化。例如,我们可以设置打包体积的阈值为 1MB,如果某次构建后的打包文件超过了这个大小,构建就会失败,开发人员就需要检查代码,找出导致体积增加的原因并进行修复。通过这样的自动化流程,可以保证项目的打包体积始终处于一个可控的范围内。

通过以上全面的优化策略、性能分析工具的使用以及持续优化流程的建立,我们可以有效地优化 Angular 生产版本的打包体积,提高应用的加载速度和性能,为用户提供更好的体验。同时,随着技术的不断发展和项目的演进,我们需要持续关注新的优化技术和方法,不断改进我们的优化策略。例如,未来可能会有更高效的代码压缩算法,或者新的工具可以更好地分析和优化依赖关系,我们要及时引入这些新技术,保持应用的高性能。在实际项目中,不同的优化策略可能对不同的应用有不同的效果,我们需要根据项目的特点和需求,灵活选择和组合这些优化策略,以达到最佳的优化效果。例如,对于一个以图片展示为主的应用,图片优化可能是最重要的;而对于一个功能复杂、依赖众多的企业级应用,懒加载模块和移除未使用代码可能更为关键。总之,优化 Angular 生产版本的打包体积是一个综合性的工作,需要我们从多个方面入手,不断探索和实践。