4、构建优化
资源的合并与压缩所涉及的优化点包括两方面:一方面是减少HTTP的请求数量,另一方面是减少HTTP请求资源的大小。
1、HTML 压缩
1、什么是 HTML 压缩
虽然这些格式化的字符能带来很好的代码可读性,但对浏览器解析过程来说其实并不需要,反而还增加了资源的开销。
HTML压缩就是要删除在文本文件中有意义的,但在HTML中并不参与解析的字符。这些字符包括空格、制表符、换行符及一些其他意义的字符,如HTML注释等。
如果单纯只看一个文件的压缩,可能效果并不明显,比如就以前面百度首页为例,原始字符长度为158519,经过压缩之后的长度为157334,长度减少了约0.75%。
难道这就能说明HTML压缩的效果不明显吗?其实我们不能这么考虑,对大型的互联网公司来说,每一个请求的减少都是一个非常大的优化。以谷歌为例,它的网络流量占到了全网流量的40%左右,假如网络流量能达到5ZB(1ZB=109TB),以谷歌的流量占比,它当年的实际网络流量就是5ZB×40%=2ZB,那么当谷歌经过优化让每1MB的请求减少一个字节,则整年便可节省2000TB的流量。如果以每GB流量一毛钱计算,那么一年省下来的开支也不是个小数目,这就是要进行HTML压缩的原因。
2、如何压缩
1、使用 html-minifier
使用nodejs所提供的html-minifier工具进行压缩,它涉及很多参数的配置,包括是否去掉注释removeComments,是否去掉空格collapseWhitespace,是否压缩HTML中的JavaScript的minifyJS及是否压缩HTML中的CSS的minifyCSS,具体方式如下:
2、CSS 压缩
CSS代码也能进行压缩,而且很有必要去压缩,如下是谷歌首页经过压缩的部分CSS源代码:
从中非常直观的感受是它去掉了回车和换行,此外这里的CSS压缩还做了两件事:首先是无效代码的删除,因为对有些CSS来说,无效的代码可能是注释和无效字符,需要将这些无效的代码删除,这一步很重要;其次是CSS语义合并,通常我们写的CSS可能由于文件层级的嵌套,很难避免一定的语义重复,所以就需要进行语义合并。
可以使用html-minifier针对HTML中的CSS进行压缩。除此之外,还可以使用clean-css进行CSS的压缩,其基本使用方法如下:
clean-css也有许多配置项,比如是否基于语义进行合并的merging,是否优化颜色取值的colors,以及诸多对不同类型浏览器兼容性的规则导出。
3、JavaScript 压缩与混淆
JavaScript部分的处理主要包括三个方面:无效字符和注释的删除、代码语义缩减和优化及代码混淆保护。无效字符和注释的删除原理与HTML和CSS的压缩类似。
1、代码语义缩减和优化
通过对JavaScript的压缩可以将一些变量的长度进行缩短,比如说原本一个很长的变量名经过压缩后,可以用很短的像a、b来代替,这样能进一步有效地缩减JavaScript的代码量。同样还可以针对一些重复代码进行优化,比如去除重复的变量赋值,将一些无效的代码进行缩减与合并的优化。
2、代码混淆保护
由于任何能够访问到网站页面的用户,都可以通过浏览器的开发者工具查看到前端的JavaScript代码,如果前端代码的语义非常明显,没进行压缩也没进行混淆,其格式还完整保留,那么理论上任何访问网站的人都可以轻易地窥探到我们代码中的逻辑,从而去做一些威胁系统安全的事情。所以进行JavaScript代码压缩和混淆的处理,也是对我们前端代码的一种保护。
如何来防止别有用心之人窥探到我们业务的核心代码呢?实际上就是进行JavaScript压缩,将前端源代码的可读性变得尽可能低,混淆变量与方法的命名。
对大部分公司来说,HTML压缩可有可无,但是CSS与JavaScript压缩却是必须要进行的。
JavaScript压缩处理也有类似的第三方库可供使用:uglifyJS2。可以通过npm引入uglify-js库来使用,也可以结合构建工具一起使用,其单独使用时的方法如下:
其中压缩混淆的配置对象结构如下:
4、webpack 构建与优化
1、尽量与时俱进:尽可能使用最新稳定版本的webpack、nodejs、npm或yarn能有效地提升打包构建的效率。
2、较少 Loader 的执行
根据具体情况使用include或exclude,在尽可能少的模块上执行Loader。webpack的配置文件如下:
这里关注module字段中对JavaScript文件的处理规则,如果不加exclude字段,则webpack会对该配置文件所在路径下的所有JavaScript文件使用babel-loader,虽然babel-loader的功能强大,但它执行起来很慢。
这样所处理的JavaScript不仅会包含我们的项目源代码,还会涉及node_modules路径下项目引用的所有第三方文件。由于第三方库的文件在发布前本身已经执行过一次babel-loader,没必要再重复执行一次,增加不必要的打包构建耗时,所以exclude字段不可省略。
同时与之对应的还有一个include字段,其使用含义与exclude相反,即仅对其指定范围内的JavaScript文件进行处理,以降低loader被执行的频率。
对于图片文件则没有必要通过include或exclude来降低loader的执行频率,因为无论哪里引入的图片,最后打包都需要通过url-loader对其进行处理,所以include或exclude的语法并不适用于所有loader类型,要根据具体的情况而定。
使用exclude或include可以帮助我们规避对庞大的第三方库文件的处理,但仅通过限定文件处理范围所带来的性能提升其实是有限的,除此之外,如果开启缓存将构建结果缓存到文件系统中,则可让babel-loader的工作效率得到成倍增加,处理方式也很简单,只需为loader增加相应的参数即可:
3、确保插件的精简和可靠
建议使用webpack官方网站上推荐的插件
4、合理配置 resolve 参数
配置resolve参数可以为我们在编写代码引入模块时提供不少便利,比如使用extensions省略引入JavaScript文件的后缀,使用alias减少书写所引入模块的多目录层级,使用mainFiles声明目录下的默认使用文件等,但当我们使用这些参数带来便利的同时,如果滥加使用也会降低打包速度。
1、可对 js、ts 省略路径,但不要对 css、图片等设置
可以使用resolve中的extensions属性来申明这些后缀,让项目在构建打包时,由webpack帮我们查找并补全文件后缀。同时对组件路径的引用也可通过resolve的alias配置来进行简化,配置如下:
如此配置的含义是,当所引入的模块默认了文件后缀时,webpack会在其指定路径下依次查找是否有.js、.jsx、.ts这三种后缀的文件,如果有便使用,并且在模块引入的同时用cpn代替src/component。当有了上述配置后,之前提到的Hello模块的引入便可写成如下形式:
2、不建议使用 mainFiles 属性
另外,resolve还有一个mainFiles属性,通过对它的配置可以指定让webpack查找引入模块路径下的默认文件名,虽然它能在很大程度上简化模块引用的编码量,但付出的代价是增加了打包构建过程中对目标文件的查找时间,所以不建议使用。
5、使用 DllPlugin
前端项目中经常会用到庞大的第三方库,来协助我们完成特定功能的开发,而每当发生修改需要重新进行打包时,webpack会默认去分析所有引用的第三方组件库,最后将其打包进我们的项目代码中。在通常情况下,第三方组件包的代码是稳定的,不更换所引用的版本其代码是不会发生修改的,所以这就给出了一条优化的思路:我们仅需要在第一次打包时去分析这些第三方库,而在之后的打包过程中只需使用之前的结果即可。
这便会用到DllPlugin,它是基于Windows动态链接库(DLL)的思想创建出来的,该插件会把第三方库单独打包到一个文件中,作为一个单纯的依赖库,它不会和我们的项目代码一起参与重新打包,只有当依赖自身发生版本变化时才会重新进行打包。
使用DllPlugin处理文件的过程可分为两步:首先基于动态链接库专属的配置文件打包dll库文件,然后再基于webpack的构建文件打包项目代码。下面以一个简单的React组件为例:
这里引入了三个在React项目中经常会遇到的第三方包react、react-dom和lodash,如果我们不进行任何处理,每当修改该文件后进行重新打包,则都会引起webpack去分析它们,若打包次数频繁,显然会浪费许多时间。
接下来我们进行优化,具体分为两步,首先将所依赖的第三方库打包成dll文件,然后检查第三方库的版本是否在其后的迭代中发生了变化,若无变化就都使用之前的打包结果。这里可以为第三方库创建单独的配置文件,内容如下:
该配置文件中将react、react-dom和lodash三个包的包名存储在数组中,并赋值给vendors,其含义是经过打包后,这三个包归于一个名为vendors的包。output中声明了该包的文件名及输出路径,library字段表示该第三方包对外暴露的引用名,即在其他地方可以使用该字段值引用包中的内容。最后使用webpack中的DllPlugin插件对该包中的映射关系进行分析,并将结果输出到指定路径下的json文件。
然后我们需要在webpack主配置文件中,声明对上述打包好的第三方包的使用规则:
这样配置的意思是首先引入第三方包的打包结果路径,其次引入第三方包的映射关系,当发生重新打包构建时,webpack会首先查看引用的第三方包是否包含在已建立的映射关系文件vendor.manifest.json中,若存在便通过所声明的全局变量vendors去使用,若不存在便去node_modules中获取所需的模块,动态地进行打包操作。
上述方式虽然能够降低重复打包构建的时间,但将项目中所有第三方包都打包进一个文件,势必会使其体积过大从而导致请求过慢,所以在实际项目中,我们也应根据各个第三方包的大小进行拆分,就上述代码而言,可将react和react-dom从vendors中组成为一个新包,代码如下:
如此在执行webpack构建后会生成四个文件,分别是:vendors包的代码文件vendors.dll.js和其映射关系文件vendors.manifest.json,react包的代码文件react.dll.js和其映射关系文件react.manifest.json,同时对应的主配置文件也需要进行相应的修改,以引入打包的代码文件和映射关系文件。
这样处理后,既降低了重复构建时的打包时间,又规避了打包成单一文件时,可能由于代码文件体积过大而存在加载过慢的风险。
6、将单进程转化为多进程
webpack是单进程的,就算有多个任务同时存在,它们也只能一个一个排队依次执行,这是nodejs的限制。
但好在大多数CPU已经都是多核的了,我们可以使用happypack充分释放CPU在多核并发方面的优势,帮助我们把打包构建任务分解成多个子任务去并发执行,这将大大提高打包的效率。其使用方法也很简单,就是将原有的loader配置转移到happypack中去处理:
7、压缩打包结果的体积
1、删除冗余代码
webpack从2.0版本开始,便基于ES6推出了Tree-shaking,它能根据import、export的模块导入导出语法,在构建编译过程中分析每个模块是否被真实使用,对于没用到的代码,会在最后的打包结果中删除。
容易看出Tree-shaking对处理模块级的代码冗余比较擅长,但对更细粒度的代码冗余,比如console语句、注释等,可能就需要在CSS和JavaScript压缩过程中进行处理了,常用的方式是通过uglifyjs-webpack-plugin来实现的,具体配置方式如下:
2、代码拆分按需加载
项目源代码也需要拆分,可以根据路由来划分打包文件,当访问到不同路由时再触发相应回调请求打包文件,对于webpack的打包输出的配置如下:
以React项目为例,在配置路由时还需添加如下内容:
此处的关键方法就是require.ensure这个异步方法,webpack会将我们这里定义的组件单独打包成一个文件,仅当路由跳转到mine时,才会触发回调去获取MyComponent组件的内容。
3、可视化分析
分析打包结果的可视化工具:webpack-bundle-analyzer。它的分析结果会生成一个文件大小图:
该插件的工作原理也比较简单,就是分析在compiler.plugin(‘done’,function(stats));时传入的参数stats,它是webpack的一个统计类Stats的实例,然后通过对实例调用toJson()方法转成json文件,再从中提取出chunks各个包的大小信息,最后在Canvas中进行画图。通过该图能让开发者快速意识到哪些模块异常的大,然后找出过大的原因去优化它。
该工具使用起来也很方便,就如使用普通插件一样: