Angular页面优化——删除未使用的前端代码 | 踩坑日志

981 阅读9分钟

大家好

我是创意行业中的前端prupru

是一个虽然面试官说已经没什么人用Angular,

却依然把Angular技能点满的人。

前言

我想推动团队重视网页项目中的优化由来已久,也曾在团队讨论中委婉地提过我们项目的Lighthouse分数不尽人意。虽团队大大们有所认知,但碍于业务的时间紧张和成本限制,往往是我自己抠时间根着教程做一些小修小改。

直到接下来需要我要上手一个一年一度的项目。

TL:你看看去年这个活动网站。
我:嗯……
TL:今年我们还有这个业务。你不是一直想搞优化嘛?看你年轻力壮爱学习,给你几天时间想点办法把Lighthouse分数整上去
我:!!!
(以上对话为虚构)

开心的是,网页优化和最佳实践终于被提上了新项目的日程。

紧张的是,感觉自己的经验不够用。

我的目标是整理出一个可被分享的工作流。

在瞎琢磨了几个小时后,汇报一下初步成果

-Performance指标3项达标,分数从66到70分。

-未使用的文件加载速度 1.04秒 降到 0.57秒。

-覆盖率有所上升(main-es2015.js, style.css)。

webpack-bundle-analyzer 对比图

步骤

分为诊断、优化、再测试,以此循环。

诊断

1.打包

ng build --prod --source-map --stats-json --outputPath=dist

在生产环境中打包。和真正部署不同的是,其中--source-map是为了给source-map-explorer提供信息,--stats-json是为了给webpack-bundle-analyzer提供信息。

在本地serve打包好的文件。

然后在本地查看webpack-bundle-analyzer或者source-map-explorer的打包情况。

(工具的安装和使用见各repo)

2.通过Lighthouse报告,定位prod环境下的问题,从最严重的问题着手开始。

“做正确且困难的事”

案例中:通过上图报告得知,“Remove unused javascript” 没有移除未使用的javascript导致的一系列问题,main.js和style.css的覆盖率较低,导致浪费用户的加载时间和流量。

诊断工具见附录【工具一览】。除了Lighthouse的指导外,各教程文章也有很多。

3. 分析打包情况

这也是我觉得困难的地方,因为好多JS优化的文章说到以上的工具就结束了,结果是我看着分析出来的数据和可视化图仍旧满头疑惑。并没有很多文章说如何给出规律如何找到不对劲的包。

实际上自己上手了之后的确发现每个项目特点不一样,无法以一概全。实施细节也因框架而异。然而还是有共通的思路是可以借鉴的。

整体思路是通过数据找到“不对劲”的包。我们的目标是每一份加载的文件覆盖率高,单个文件的大小不要过大,并且一次不要加载过多,可以在后期的用户互动懒加载。也就是大化小,并且在大小和请求次数中取平衡之道。

粗浅的实践经验之后,在我看来“不对劲”有以下几种

  • 1.最明显的是单个文件尺寸过大,或者某一组件/库占比过大。

    方法:- 是否该文件书写不当?
           从代码上优化。
         - 是否引用过多的组件或者库?
           拆分。
    

案例中:如果是单个angular组件的大小十分扎眼,很有可能是它承担了太多职责,非常冗长。则确认一下该组件是否可以进一步拆成小组件。

  • 2.如果单个文件中,各个组件大小差不多。

    方法:- 是否每个被引用的元素都有使用过(常常忘记)?
           删除没有使用的引用变量。
         - 是否每个被引用的元素都在被加载的第一时间使用?
           拆分非初始渲染需要的部分,再懒加载。
            
    

案例中:app提前加载了其他页面,下文讲描述Angular的懒加载。

  • 3.不同的文件中重复打包了一个同一个库

    方法: 在全局引用这个库
    
  • 4.【Angular】es2015项目却加载了某个CommonJS/AMD模块。

事实上Angular打包时ng build --prod 也会发出警告。

Warning: ... depends on .... 
CommonJS or AMD dependencies can cause optimization bailouts.
For more info see: https://angular.io/guide/build#configuring-commonjs-dependencies

  方法:- 该库是否有ES模块的替代版本?
         使用该版本。
       - 或根据以上Angular官方文档,mute关于该库的打包警告。
       

angular.io/guide/build…

优化

根据项目和问题各取所需。

1)懒加载模块

针对分析篇中的第2种“不对劲”的打包现象:首页文件大,但单个文件中各个组件大小差不多。

先放上官方的懒加载特征模块。总结就是对于首页不需要加载的页面,进行懒加载。

一个Angular初学者的常见疑惑是,为什么要把多个组件打包成一个模块?本质上还是被app根模块引用,有什么区别?我曾经也不以为然,但是遇到app规模变大、路由很多、难以优化时,就有必要注重按模块分类这些组件。

因为在Angular模块中,模块Module具有一个Component不具有的特性,即懒加载。

NgModule默认是加载所有其中组件的,Angular懒加载的具体定义是不在initial bundle中加载,在浏览该模块的路径时才加载。所以也可以把feature module特征模块浅显地理解为页面模块,或者某些条件下出现的功能。

案例:

Before app.routing.module.ts

 { path: ‘pathA’, component: AComponent }

把该组件改造成懒加载的模块,步骤

1.在terminal运行

ng module AModule —module app —route pathA

第一个参数是新建的模块名,app是指新模块要加入的根模块。

2.把原本A.component.ts中Acomponent的内容移动到AModule中的A.component.ts。(注意生成模块时的命名冲突)

After

 { 
   path: 'pathA', 
   loadChildren: () => import('./A/A.module.ts')
                         .then(m => m.AModule) 
 }

2) 将global library单独打包

针对分析篇中的第1种“不对劲”的打包现象:单个文件中某个库的比例过大,或者第4种:库是CommonJS/AMD模块。

根据webpack-bundle-analyzer的可视图,感觉可以从lottie.js这个库下手。lottie占比看上去很大,同时被打包在了main.js内。

大致思路是webpack中可以划分打包,Angular内置webpack,如何告诉它分开打包呢?

一顿搜索,发现了宝藏Angular官方文章Adding a library to the runtime global scope 和stackoverflow上的Angular Cli Webpack, How to add or bundle external js files?

angular.json中的scripts本来用来添加外部库,其实也可以用来指定本地库的打包。

添加lottie的本地node module路径

angular.json


"architect": {
  "build": {
    "options": {
      ...
      "styles": [
        "src/styles.scss"
      ],
      "scripts": [
        "../node_modules/lottie-web/build/player/lottie_svg.js"
      ]
    },
  ...
  }
}
...

打包后,dist文件夹中应该出现一个scripts.js

优点:(推论)如果app发布更新,scripts的部分不变而web app发布新版本,则用户不必重复下载lottie的部分。

优化x1: 发现了min.js文件

"scripts": [
  "../node_modules/lottie-web/build/player/lottie_svg.min.js"
]

优化x2: 懒加载库

根据自己对项目的了解,甚至可以将库随着组件懒加载。

观察dist/index.html

<script src="scripts.<hash>.js" defer></script>

思路:配置“scripts”后,打包好的scripts.js被插入首页。那其实只要打包scripts.js但默认不插入首页,然后需要的时候添加一个script DOM就好了。

一顿搜索后,还是上面的Angular官方文章。

1.修改angular.json配置,默认不插入打包的scripts

Before

"scripts": [
  "../node_modules/lottie-web/build/player/lottie_svg.min.js"
]

After

"scripts": [
  {
    "input": "../node_modules/lottie-web/build/player/lottie_svg.min.js",
    "inject": false,
    "bundleName": "lottie"
  }
]

2.在组件中懒加载

这一部偷懒我就没继续了。参考教程 Lazy Loading Scripts and Styles in Angular

3) 针对库自身的优化

和lottie还没完。

在一顿搜索后发现此文Reducing lottie-web bundle size。大意是lottie自带3种渲染器:canvas、html和svg,但是大部分项目在初始化时就指定一种渲染器并且不需要切换。那么只加载该渲染器的部分就够了。

案例中:只使用了lottie的svg渲染器

Before

import lottie from 'lottie-web';

After

import lottie from 'lottie-web/build/player/lottie_svg';

并且,该方法正是lottie的官方angular组件版ngx-lottie。 所以使用ngx-lottie本身也可以提升优化。

优化:CSS篇

打开Coverage,观察未使用的selector的特征。

案例中:发现都是material组件的style。

实际上整个项目只使用到material的6个组件。

我们一般会记得在js/Angular Module里按需取用,却忘了css也是可以如法炮制的。

这也是上篇针对库自身的优化的一种。

1)减少import的内容

Before

@import '~@angular/material/theming';

观察该引用样式的源代码,和node_modules的库路径的其他文件(或者官方仓库)。

在此,观察material button组件的样式代码(左边是~@angular/material/theming,右边是~@angular/material/button/button-theme)

Angual material 源代码截图

可得theming.scss包含两部分core样式定义,和各组件样式代码。由于我们不需要所组件的样式,并且这些未使用过的组件占比很大,将引用改为如下:

After

@import '~@angular/material/core/core';
@import '~@angular/material/checkbox/checkbox-theme';
...

再次确认样式显示没有不同。

2)针对Angular material,减少生成的组件样式。

Before

@include angular-material-theme($gweb-play-jp-esports-theme);

以下教程完全来自视频 Remove unused Angular Material CSS (2020)

同理观察该mixin的源代码

Angual material 源代码截图

自定义一个mixin,复制该mixin并删除不需要的组件includes。

After

/* Copied from mixin angular-material-theme */
/* @import '~@angular/material/theming'; */
@mixin custom-theme($theme-or-color-config) {
  $dedupe-key: 'angular-material-theme';
  @include mat-private-check-duplicate-theme-styles($theme-or-color-config, $dedupe-key) {
    @include mat-core-theme($theme-or-color-config);
    @include mat-checkbox-theme($theme-or-color-config);
    ...
  }
}

@include custom-theme($gweb-play-jp-esports-theme);

总结

减少文件大小效果立竿见影。 96kb -> 32kb,几乎是三分之一。

Angual compile output截图

附录

工具列表和引用列表

工具一览

source-map-explorer

webpack-bundle-analyzer

Angular虽然内置webpack,但是不包含webpack-bundle-analyzer,所以仍需要添加这个库到devDenpencies。

引用一览

Angular官方文档

Adding a library to the runtime global scope

Configuring CommonJS dependencies

Lazy-loading feature modules

Angular Cli Webpack, How to add or bundle external js files?

其他

Remove unused Angular Material CSS (2020)

Lazy Loading Scripts and Styles in Angular

Reducing lottie-web bundle size

后序

以上一些步骤,是我根据各平台的文章和自己对项目整体的理解摸索出来的。

看到数据上的改善很欣慰,但也烦恼整体覆盖率仍是不算高,未使用的css selector还是占了很多。

在此抛砖引玉,若有点赞或指点不胜感激!