大家好
我是创意行业中的前端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关于该库的打包警告。
优化
根据项目和问题各取所需。
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)
可得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的源代码
自定义一个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,几乎是三分之一。
附录
工具列表和引用列表
工具一览
Angular虽然内置webpack,但是不包含webpack-bundle-analyzer,所以仍需要添加这个库到devDenpencies。
引用一览
Angular官方文档
Adding a library to the runtime global scope
Configuring CommonJS dependencies
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还是占了很多。
在此抛砖引玉,若有点赞或指点不胜感激!