xhs-test
v1.0.0
Published
this is a test project for stduy,so don't to download,thanks!!!
Downloads
2
Readme
性能优化概述
性能优化主要体现在以下三个方面:
构建性能
这里所说的构建性能是指开发阶段的构建性能,而不是生产环境的构建性能。
优化的目标是,降低从打包开始到代码效果呈现所经过的事件
构建性能会影响开发效率。构建性能越高,开发过程中的时间浪费越少
传输性能
传输性能是指打包后的js代码传输到浏览器所经过的时间。
优化传输性能时要考虑到:
- 总传输量:所有需要传输的js文件的内容加起来,就是总传输量,重复代码越少,总传输量越小
- 文件数量:当访问页面时,需要传输的js文件数量,文件越多,http请求越多,响应速度越慢
- 浏览器缓存:js文件会被浏览器缓存,被缓存的js文件不会在进行传输
运行性能
运行性能是指,js代码在浏览器端的运行速度
它主要取决于我们如何书写高质量代码
永远不要过早的关注性能
,因为你在开发的时候,无法完全预知最终的性能,过早的关注性能会极大的降低开发效率。
减少模块解析
什么是模块解析
模块解析包括:AST抽象语法树分析、依赖分析、模块语法替换
不做模块解析会怎么样?
如果对某个模块不做解析,该模块经过loader处理后的代码就是最终代码。 如果没有loader对该模块进行处理,该模块的源码就是最终的打包结果。 如果不对某个模块进行解析,可以缩短构建时间。
优化loader性能
进一步限制loader的应用范围
思路:对于某些库,不使用loader
例如:babel-loader可以转换ES6或更高级的语法,可有些库本身就是用ES语法书写的不需要转换,使用babel-loader反而会浪费构建时间。 lodash就是这样一个库。 lodash是在ES5之前出现的库,使用的是ES3的语法。 通过
module.rule.exclude
或module.rule.include
,排除或仅包含需要应用loader的场景。
module.exports = {
module:{
rules:[
{
test:/\.js$/,
use:['babel-loader'],
exclude:/loadsh/,
include:/jquery/
}
]
}
}
如果暴力一点,甚至可以直接排除掉node_modules
目录中的模块,或者仅转换src目录的模块
module.exports = {
module:{
rules:[
{
test:/\.js$/,
use:['babel-loader'],
exclude:/node_modules/,
}
]
}
}
这种做法是对loader的范围进行进一步的限制,和noParse不冲突
缓存loader的结果
我们可以基于这样一种假设:如果某个文件内容不变,经过相同的loader解析后,解析后的结果也不变,于是我们可以将loader的解析结果保存下来,让后续的解析直接使用保存结果。
cache-loader
可以实现这样的功能,
module.exports = {
module:{
rules:[
{
test:/\.js$/,
use:['cache-loader', '...other loader'],
}
]
}
}
有趣的是,cache-loader明明放在最前面,为什么却可以决定后续loader是否运行 实际上loader的运行过程中还包含一个过程,就是pitch 如图所示,pitch是一个函数,如果该loader由一个pitch方法,webpack就会在开始的时候首先执行各个loader的pitch,pitch会接收到一个文件路径,如果pitch没有返回,他就会接下去执行下一个loader的pitch,如果有返回,他就会将资源(也就是源码)传递给前一个loader,假设现在loader2的pitch有返回值,那么webpack就不会执行loader3.pitch、loader2、loader3,而是直接将资源传递给loader1,因此catch-loader放在第一位却可以控制后续的运行,一旦cache-loader发现存在缓存结果,它就直接返回,由于前面并没有其他loader,整个loader过程就直接结束,从而实现控制的效果;如果没有发现缓存值,就会执行下一个loader的pitch。 cache-loader可以实现个自定义的配置,详细间文档
为loader的运行开启多线程
thread-loader会开启一个线程池,线程池包含适量的线程 它会把后续的loader放到新的线程中运行,以提高构建效率 由于后续的loader会放到新线程中,所以后续的loader不能:
- 无法使用webpack api生成文件
- 无法使用自定义的plugin api
- 无法访问webpack options
在实际开发中,可以进行测试,来决定thread-loader放在什么位置 特别注意:开启和管理线程需要消耗时间,小型项目中使用thread-loader反而会增加构建时间
热替换 HMR(Hot Module Replacement)
热替换并不能降低构建时间(可能还会稍微增加),但可以降低改动代码到效果呈现的时间。 当使用webpack-dev-server时,考虑代码改动到效果呈现的过程。 然而使用热替换之后,流程发生了变化。
使用和原理
- 更改配置
module.exports = {
devServer:{
hot:true //开启HMR
},
plugins:[
new webpack.HotModuleReplacementPlugin()
]
}
- 更改代码
// index.js
if(module.hot){ // 是否开启热更新
module.hot.accept() // 接受热更新
}
首先这段代码是会参与运行的!!
当开启热更新后,webpack-dev-server会向打包结果中注入module.hot属性
默认情况下,webpack-dev-server不管是否开启热更新,当重新打包之后,都会调用location.reload
刷新页面
但如果运行了module.hot.accept(),将改变这一行为
module.hot.accept()的作用是让webpack-dev-server通过socket管道,把服务器更新的内容发送到浏览器
然后将结果交给插件HotModuleReplacementPlugin
注入的代码执行
插件HotModuleReplacementPlugin
会覆盖原始代码,然后让代码重新执行
所以热替换发生在代码运行期间
样式热替换
对于样式也是可以使用热替换的,但需要使用style-loader 因为发生热替换的时候,HotModuleReplacementPlugin只会简单的运行模块代码 因此style-loader的代码一运行,就会重新设置style元素中的样式 而mini-css-extract-plugin,由于它生成文件是在构建期间,运行期间无法改动文件,因此他对于热替换是无效的
手动分包
基本原理
手动分包的总体思路是:
- 先单独打包公共模块 公共模块会被打包成为动态链接库(dll Dynamic Link Library),并生成资源清单
- 根据入口模块进行正常打包 打包时,如果发现模块中使用了资源清单中描述的的模块,则不会形成下面的代码:
import $ from 'jquery'
import _ from 'lodash'
_.isArray($('.pich'))
由于资源清单中包含jquery和lodash两个模块,因此打包结果的大致格式是:
(function(module){
...
})({
"./src/index.js":function(module, exports, __webpack_require__){
var $ = __webpack_require__('./node_modules/jquery/index.js')
var _ = __webpack_require__('./node_modules/lodash/index.js')
_.isArray($('.pich'))
},
// 由于资源清单中存在,jquery的代码不会出现再这里
"./node_modules/jquery/index.js":function(module, exports, __webpack_require__){
module.exports = jquery
},
// 由于资源清单中存在,lodash的代码不会出现再这里
"./node_modules/lodash/index.js":function(module, exports, __webpack_require__){
module.exports = jquery
}
})
打包公共模块
打包公共模块是一个独立的打包过程
- 单独打包公共模块,暴露变量名
// webpack.dll.config.js
module.exports = {
mode:'production',
entry:{
jquery:['jquery'],
lodash:"lodash"
},
output:{
filename:'dll/[name].js',
library:'[name]', // 暴露的全局变量
libraryTarget:"var", // 暴露的方式
}
}
- 利用DllPlugin生成资源清单
const webpack = require('webpack')
const path = require('path')
module.exports = {
mode:'production',
entry:{
jquery:'jquery',
lodash:"lodash"
},
output:{
filename:'dll/[name].js',
library:'[name]', // 暴露的全局变量
libraryTarget:"var", // 暴露的方式
},
plugins:[
new webpack.DllPlugin({
path:path.resolve(__dirname,'dll','[name].mainfest.json'), // 资源清单保存位置
name:'[name]' // 资源清单中暴露的变量名
})
]
}
使用公共模块
- 在页面中手动引用公共模块
<script src='./dll/jquery.js'></script>
<script src='./dll/lodash.js'></script>
- 重新设置clean-webpack-plugin 如果使用clean-webpack-plugin,为了避免它把公共模块清除,需要做出以下配置
new CleanWebpackPlugin({
// 要清除的文件或目录
// 排除掉dll目录本身和它里面的文件
cleanOnceBeforeBuildPatterns:['**/*',"!dll","!dll/*"]
})
- 使用DllReferencePlugin控制打包结果
module.exports = {
plugins:[
new webpack.DllReferencePlugin({
mainfest:require('./dll/lodash.mainfest.json')
}),
new webpack.DllReferencePlugin({
mainfest:require('./dll/lodash.mainfest.json')
})
]
}
总结
手动打包过程
- 开启output.library暴露公共模块
- 用DllPlugin创建资源清单
- 用DllReferencePlugin使用资源清单 手动打包注意事项
- 资源清单不参与运行,可以不放在打包目录中
- 记得手动引入公共js, 以及避免被删除
- 不要对小型公共js库使用 优点
- 极大提升自身模块打包速度
- 极大的缩小了自身体积
- 有利于浏览器缓存第三方库的公共代码 缺点
- 使用非常繁琐
- 如果第三方库中包含重复代码,则效果不太理想
自动分包
基本原理
不同于手动分包,自动分包是从实际的角度出发,从一个更加宏观的角度来控制分包,而一般不对具体那个包要分出去进行控制。 因此使用自动分包,不仅非常方便,而且更加贴合实际的开发需要。 要控制分包,关键是要配置一个合理的分包策略 有了分包策略之后,不需要额外安装任何插件,webpack会自动按照策略进行分包
实际上,webpack内部是使用splitChunkPlugin进行分包的 过去有一个库CommonsChunkPlugin也可以实现分包,不过由于该库某些地方并不完善,到了webpack4之后就被splitChunkPlugin取代 从分包的流程至少可以看出一下几点:
- 分包策略至关重要,它决定了如何分包
- 分包时,webpack开启了一个新的chunk,对分离模块进行打包
- 打包结果中,公共的部分被提取出来形成一个单独的文件,它是新chunk的产物
分包策略的基本配置
webpack提供optimization配置项,用于配置一些优化信息 其中splitChunks是分包策略的配置
module.exports = {
optimization:{
splitChunks:{
// 分包策略
}
}
}
事实上,分包策略有其默认的配置,我们只需轻微的改动,即可应对大部分场景
- chunks 该配置项用于配置需要应用分包策略的chunk 我们知道分包是从已有的chunk中分离出新的chunk,那么哪些chunk需要分离呢? chunks有三个取值,分别是:
- all:对于所有chunk都要应用分包策略
- async:[默认]仅针对异步chunk应用分包策略
- initial:仅针对普通chunk应用分包策略 所以你只需配置chunks为all即可
- maxSize 该配置可以控制包的最大字节数,如果某个包(包括分出来的包)超过了该值,则webpack会尽可能将其分离成多个包 但是不要忽略的是,分包的基础单位是模块,如果一个完整的模块超过该体积,它是无法做到再切割的,因此,尽管使用该配置,完全有可能某个包还是会超过该体积 另外,该配置看上去很美妙,实际意义上不大 因为分包的目的是提取大量的公共代码,从而减少总体积和充分利用浏览器缓存 虽然该配置可以把一些包进行再切分,但实际的总体积和传输量并没有发生变化
如果要进一步减少公共模块的体积,只能是压缩和tree shaking
分包策略的其他配置
如果不想使用其他配置的默认值,可以手动进行配置
- automaticNameDelimiter: 新chunk名称的分隔符,默认~
- minChunks:一个模块被多少个chunk使用时才会进行分包,默认是1
- minSize:当分包达到多少字节后才被运行真正的拆分,默认值是30000
缓存组
之前配置的分包策略是全局的 而实际上,分包策略是基于缓存组的 每一套缓存组提供一套独有的策略,webpack按照缓存组的优先级依次处理每个缓存组,被缓存组处理过的分包不在需要再次分包 默认情况下,webpack提供两个缓存组:
module.exports = {
optimization:{
splitChunks:{
chunks:'all',
cacheGroups:{
// 属性名是缓存组的名称,会影响到分包的chunk名
// 属性值是缓存组的配置,缓存组继承所有的全局配置,也有自己的特殊配置
vendors:{
test:/[\\/]node_modules[\\/]/, // 匹配到相应模块时,这些模块进行单独打包
priority: -10, // 缓存组优先级,优先级越高,该模块越先进行处理,默认值为0
},
default:{
minChunks:1, // 覆盖全局配置,将最小chunk引用数改为2
priority:-20, // 优先级
reuseExistingChunk:true, // 重用已经被分离出去的chunk
}
}
}
}
很多时候,缓存组对于我们来说没什么意义,因为默认缓存组就已经足够了 但我们同样可以利用缓存组完成一些事情,比如对公共样式的抽离
module.exports = {
optimization:{
splitChunks:{
chunks:'all',
cacheGroups:{
styles:{
test:/\.css$/
minSize:0,
minChunks:1,
}
}
}
}
配合多页应用
虽然现在单页应用是主流,但免不了还是会遇到多页应用 由于多页应用中需要为每个html页面指定需要的chunk,这就造成了问题
new HTmlWebpackPlugin({
template:"./public/index.html",
chunks:['index-other','vendors~index-other','index']
})
我们必须手动指定被分离出去的chunk名称,这不是一种好办法, 幸好html-webpack-plugin的新版本解决了这个问题 做出如下配置即可:
new HTmlWebpackPlugin({
template:"./public/index.html",
chunks:['index']
})
它会自动的找到被index分离出去的chunk,并完成引用
原理
自动分包的原理其实并不复杂,主要经过以下步骤:
- 检查每个chunk编译的结果
- 根据分包策略,找到满足策略的模块
- 根据分包策略,生成新chunk打包这些模块(代码有所变化)
- 把打包出去的模块从原始包中剔除,并修正原始包代码 在代码层面有以下改动:
- 分包的代码中,加入一个全局变量,类型为数组,其中包含公共模块代码
- 原始包的代码中,使用数组中的公共模块
代码压缩
前言
- 为什么要进行代码压缩?
- 减少代码体积
- 破坏代码可读性
- 提升破解成本
- 什么时候进行代码压缩 生产环境
- 使用什么压缩工具
目前最流行的代码压缩工具主要有两个:
Uglifyjs
和Terser
Uglifyjs
是一个传统的代码压缩工具,已存在多年,曾经是前端应用的必备工具,但由于它不支持es6语法,所以目前的流行度有所下降。 Terser是一个新起的代码压缩工具,支持ES6+语法,因此被很多构建工具内置使用,webpack安装后会内置Terser,当启用生产环境后即可用其进行代码压缩。
关于副作用 side function 副作用:函数运行过程中,可能会对外部环境造成影响的功能 如果函数包含以下代码,该函数称为副作用函数:
- 异步函数
- localStorage
- 对外部数据进行修改 如果一个函数没有副作用,同时函数的返回结果仅依赖参数,则该函数称为纯函数(pure function)
webpack+Terser
webpack自动集成了Terser 如果你想更改、添加压缩工具,又或者想对Terser进行配置,使用下面的webpack配置即可
const TerserPlugin = require('terser-webpack-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
module.exports = {
optimization:{
// 是否要启用压缩,默认情况下,生产环境会自动开启
minimize:true,
minimizer:[ // 压缩时使用的插件,可以配置多个
new TerserPlugin(),
new OptimizeCSSAssetsPlugin()
]
}
}
tree shaking
代码压缩可以移除模块内部的无效代码 tree shaking 可以移除模块之间的无效代码
背景
某些模块导出的代码并不一定会被用到
// Math.js
export function sum(a,b)=>{
return a+b
}
export function sub(a,b)=>{
return a - b
}
// index.js
import { sub } from './Math.js'
console.log(sub(1,2))
上述模块之中并没有使用到sum函数,但是代码压缩并无法去除sum函数,导致sum成为无效代码 tree shaking就是用来解决上述的问题:移除模块之间的无效代码
使用
webpack2就开始支持了tree shaking 只要是生产环境,tree shaking就会自动开启
原理
webpack会从入口模块出发寻找依赖关系 当解析一个模块时,webpack会根据ES6的模块导入语句判断,该模块依赖了另一个模块的那个导出 webpack之所以选择ES6的模块导入语句,是因为ES6模块有以下特点:
- 导入导出语句只能是顶层语句
- import的模块名只能是字符串常量
- import绑定的变量是不可变的 这些特征都非常有利于分析出稳定的依赖 在具体分析依赖时,webpack坚持的原则是:保证代码正常运行,然后再尽量tree shaking 所以如果你依赖的是一个导出的对象,由于js语言的动态特性,以及webpack的不够智能,为了保证代码正常运行,它不会移除对象中的任何信息 因此我们编写代码的时候,尽量:
- 使用
export xxx
导出,而不使用export default {xxx}
导出 - 使用
import {xxx} from 'xxx'
导入,而不是使用import xxx from 'xxx'
导入 依赖分析完毕后,webpack会根据每个模块每个导出是否被使用,标记其他导出为dead code
,然后交给代码压缩工具处理 代码压缩工具最终移除掉哪些dead code
代码
使用第三方库
某些第三方库可能使用的是commonjs
的方式导出,比如lodash,又或者没有提供普通的es6方式导出
对于这些库,tree shaking
无法发挥作用
因此我们要寻找这些库的ex6版本,好在很多流行但没有使用的ex6的第三方库,都发布了ES6版本,比如lodash-es
作用域分析
tree shaking 本身并没有完善的作用域分析,可能导致一些再dead code
函数中的依赖乃然会被视为依赖.
插件webpack-deep-scope-plugin
提供作用域分析,可以解决该问题
副作用问题
webpack在tree shaking的使用,有一个原则:一定要保证代码的正确运行 在满足该原则的基础上,再来决定如何tree shaking 因此,当webpack 无法确定某个模块是否有副作用时,往往将其视为有副作用 因此,某些情况可能并不是我们想要的
// common.js
var n = Math.random()
// index.js
import './common.js'
虽然我们根本没有用到common.js
的导出,但webpack担心common.js
有副作用,如果去掉会影响某些功能
如果要解决该问题,就需要标记该文件是没有副作用的
在package.json
中加入sideEffects
{
...
"sideEffects":false
}
有两种配置:
- false:当前工程中,所有模块都没有副作用。注意,这种写法会影响到某些css文件的导入
- 数组:设置哪些文件拥有副作用,例如['!src/common.js'],表示只要不是src/common.js的文件,都有副作用
这种方式我们一般不处理,通常是一些第三方库在它们自己的package.json中标注
css tree shaking
webpack无法对css完成tree shaking,因为css跟es6没有半毛钱关系 因此对于css的tree shaking需要其他插件来完成 例如:purgecss-webpack-plugin
注意:purgecss-webpack-plugin对css module无能为力
ESLint
ESlint是一个针对js的代码风格检查工具,当不满足其要求的风格时,会给予警告或错误
使用
ESLint通常配合编辑器使用
- 在vscode中安装ESLint
该工具会自动检查工程中的js文件
检查的工作交给eslint库,如果当前工程没有就会去全局库中查找,如果都没有,则无法完成检查
另外,检查的依据是eslint配置文件
.eslintrc
,如果找不到工程的配置文件,也无法完成检查 - 安装eslint
npm i [-g] eslint
- 创建配置文件 可以通过eslint交互式命令创建配置文件
npx eslint --init
eslint会识别工程中的.eslintrc文件,也能够识别package.json中的eslintConfig字段
配置
env
- browser: 代码是否在浏览器中运行
- es6: 是否启用ES6全局API,例如Promise
parserOptions
该配置指定eslint对哪些语法的支持
- ecmaVersion:支持ES语法版本
- soucreType:
- script:传统脚本
- module:模块化脚本
parser
eslint的工作原理是先将代码进行解析,然后按照规则进行分析 eslint 默认使用Espree作为解析器,你可以在配置文件中指定一个不同的解析器
globals
配置可以使用的额外的全局变量
{
"globals":{
"var1":"readonly",
"var2":"writable"
}
}
eslint支持注释形式的配置,在代码中使用下面的代码注释也可完成配置
/* global var1,var2 */
/* global var3:readonly, var4:writabal */
extends
配置继承自哪里
ignoredFile
排除掉不需要验证的文件 .eslintignore
rules
eslint规则集 每条规则影响某个方面的代码风格 每条规则都有下面几个取值:
- off或0或false:关闭该规则检查
- warn或1或true:警告,不会导致程序退出
- error或2:错误,当被触发的时候,程序会退出 除了配置文件中的规则,还可以在注释中使用
/* eslint eqeqes:0 , curly: 0 */
bundle analyzer
对每个模块具体的体积进行分析 安装一个插件即可
npm i -D webpack-bundle-analyzer
使用
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer')
module.exports = {
plugins:{
new BundleAnalyzerPlugin()
}
}