开发环境性能优化
- 优化打包构建速度
- 优化代码调试
生产环境性能优化
- 优化打包构建速度
- 优化代码运行的性能
HMR 模块热替换
HMR : hot Module replacement 热模块替换 / 模块热替换。
**作用: 一个模块发生变化,只会重新打包这一个模块(而不是打包所有的模块)。 极大的提升构建速度
- 样式文件: 可以使用 HMR 功能,style-loader内部实现
-
js文件: 默认没有 HMR
- 需要修改 Js 代码, 添加支持 HMR 功能的 代码
- 注意:HMR 功能对 JS 的处理, 只能处理 非入口 JS文件的 其他文件
-
html文件: 默认没有 HMR, 同时会导致问题: html 文件不能热更新了~ (不用做HMR 功能)
- 解决:修改 Entry 入口, 将 HTML 文件引入。
配置 webpack.config.js
// webpack.config.js
const { resolve } = require ('path')
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
// 入口
// ./src/index.html 加入入口
entry: {'./src/js/index.js', './src/index.html'}
// 输出
output: {
// 输出文件名
filename: 'bundle.js',
// 输出路径 __dirname node.js变量,当前文件的目录绝对路径
path: path.resolve(__dirname, 'dist'),
// 自定义资源文件名
assetModuleFilename: 'images/[hash:10][ext][query]',
// 在生成文件之前清空 output 目录
clean: true,
},
// 开发服务器配置
devServer: {
// 从目录提供静态文件
static: {
directory: resolve(__dirname, "public"),
publicPath: "/",
},
// 启动后打开浏览器
open: true,
// 监听请求的端口号
port: 8080,
// 开启HMR功能 --> 热模块替换是默认开启的
// 当修改了 webpack 配置,新配置要想生效,必须重启 Webpac k服务
hot:true
},
};
Js 非入口文件 热模块替换
index.js 入口文件
// index.js
// 引入
import '../css/iconfont.css';
import '../css/index.css';
improt print from './print'
console.log ('index.js 文件被加载了~')
print ();
function add (x,y){
return x + y;
}
console.log (add(1,2));
if(module.hot) {
// 一旦 module.hot 为 true, 说明开启了 HMR 功能。 --> 让 HMR 功能代码生效
module.hot.accept('./print.js',function(){
// 方法会监听 print.js 文件变化, 一旦发生变化 了, 其他默认不会重 新打包构建。
// 会执行后面的回调函数
print ();
})
}
print.js 非入口文件
// print.js
function print() {
// const content = 'hello print';
// 变化
const content = 'hello print ···';
console.log ('阿啊梅')
}
export default print
source map 源码映射
source map: 一种提供源代码到构建后代码隐射 技术。 (如果构建后代码出错 了,通过 隐射可以追踪源代码错误。 )
简而言之, SourceMap 就是一个储存着代码位置信息的文件,转换后的代码的每一个位置,所对应的转换前的位置。有了它,点击浏览器的控制台报错信息时,可以直接显示出错源代码位置而不是转换后的代码。
如何使用sourcemap
步骤
- 根据源文件,生成source-map文件,webpack在打包时,可以通过配置生成 source-map。
- 在转换后的代码,最后添加一个注释,它指向sourcemap; //#sourceMappingURL=common.bundle.js.map。
- 浏览器会根据我们的注释,查找响应的source-map,并且根据source-map还原我们的代码,方便进行调试。
-
在Chrome中,我们可以按照如下的方式打开source-map:
配置 webpack.config.js
代码实例
- 如下是一个简单的 index.js 文件,我们故意将最后一行
console.log('hello world')文件 错写成console.logo('hello world')。
const a = 1;
const b = 2;
console.log(a + b);
console.logo('hello world');
无 SourceMap
- 我们将 webpack.config.js 的 devtool 选项配置为
'none',打包上述的index.js文件。
// webpack.config.js
module.exports = {
// 入口
// ./src/index.html 加入入口
entry: {'./src/js/index.js', './src/index.html'}
// 输出
output: {
// 输出文件名
filename: 'bundle.js',
// 输出路径 __dirname node.js变量,当前文件的目录绝对路径
path: path.resolve(__dirname, 'dist'),
// 自定义资源文件名
assetModuleFilename: 'images/[hash:10][ext][query]',
// 在生成文件之前清空 output 目录
clean: true,
},
// 配置 source map
devtool: "none",
// 开发服务器配置
devServer: {
// 从目录提供静态文件
static: {
directory: resolve(__dirname, "public"),
publicPath: "/",
},
// 启动后打开浏览器
open: true,
// 监听请求的端口号
port: 8080,
// 开启HMR功能 --> 热模块替换是默认开启的
// 当修改了 webpack 配置,新配置要想生效,必须重启 Webpac k服务
hot:true
},
};
有 SourceMap
- 将 webpack.config.js 的 devtool 选项配置由
'none'改成source-map后,再次打包上面的index.js文件。
// webpack.config.js
module.exports = {
// 入口
// ./src/index.html 加入入口
entry: {'./src/js/index.js', './src/index.html'}
// 输出
output: {
// 输出文件名
filename: 'bundle.js',
// 输出路径 __dirname node.js变量,当前文件的目录绝对路径
path: path.resolve(__dirname, 'dist'),
// 自定义资源文件名
assetModuleFilename: 'images/[hash:10][ext][query]',
// 在生成文件之前清空 output 目录
clean: true,
},
// 配置 source map
devtool: "source-map",
// 开发服务器配置
devServer: {
// 从目录提供静态文件
static: {
directory: resolve(__dirname, "public"),
publicPath: "/",
},
// 启动后打开浏览器
open: true,
// 监听请求的端口号
port: 8080,
// 开启HMR功能 --> 热模块替换是默认开启的
// 当修改了 webpack 配置,新配置要想生效,必须重启 Webpac k服务
hot:true
},
};
SourceMap工作原理
-
我们使用 webpack 打包并选择 devtool 为
source-map后,每个打包后的 就是 JS 模块会有一个对应的.map文件。-
打包出来的
main.js.map文件中,就是一个标准的 SourceMap 内容格式:{ "version": 3, "sources": [ "webpack://webpack5-template/./src/index.js" ], "names": [], "mappings": ";;;;;AAAA;AACA;AACA;AACA;AACA,eAAe;AACf;AACA;AACA,mB", "file": "main.bundle.js", "sourcesContent": [ "console.log('Interesting!!!')\n// Create heading node\nconst heading = document.createElement('h1')\nheading.textContent = 'Interesting!'\nconsole.log(a); // 这一行会报错\n// Append heading node to the DOM\nconst app = document.querySelector('#root')\napp.append(heading)" ], "sourceRoot": "" }
-
分析sourcemap
最初 source-map 生成的文件带下是原始文件的 10 倍,第二版减少了约 50% ,第三版又减少了 50% ,所以目前一个 133kb 的文件,最终的 source-map 的大小大概在 300kb。
| 值 | 说明 |
|---|---|
| version | Source Map 的版本,如今最新版本为3 |
| sources | 源文件列表,转换前的文件。该项是一个数组,表示可能存在多个文件合并。 |
| names | 转换前的所有变量名和属性名。 |
| mappings | 压缩混淆后的代码定位源代码的位置信息。 记录位置信息的字符串。这个的话,用到了 VLQ 编码相关。 |
| file | 该 Source Map 对应文件的名称。转换后的文件名。 |
| sourcesContent | 源代码字符串列表,用于调试时展示源文件,列表每一项对应于 sources。 |
| sourceRoot | 源文件根目录,这个值会加在每个源文件之前。转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空。 |
sourceMap格式
// json 代码
{
"version": 3, // source map 的版本。
"sources": [], // 转换前的文件,该项是一个数组,表示可能存在多个文件合并。
"names": [], // 转换前的所有变量名和属性名。
"mappings": "", // 记录位置信息的字符串。
"file": "", // 转换后的文件名。
"sourcesContent": [""] // 转换前文件的内容,当没有配置 sources 的时候会使用该项。
}
devtool格式
[ inline - | hidden - | eval - ] [ nosources -] [ [ cheap - [module - ] ] source - map
| 内联 | 值 | 说明 | 错误提示 |
| inline-source-map |
只生成一个内联 source-map , 不是创建一个单独的文件 dist 目录会发现 main.js.map 文件没有了,但在 main.js 文件的最下面,多出了一个很长的 base64 字符串, inline-source-map 会把映射内容以 base64 编码形式加入到打包生成的 JS 文件中去。 inline-source-map 和 source-map 的执行效果是完全一致的只是映射的代码文件一个在 .map文件中一个是内嵌在构建后的 `built.js`中 |
错误代码的准确信息,源代码的错位位置 | |
| eval-source-map |
内联 每个文件都生成对应的
source-map,都在
`eval`
里。并且生成一个
DataUrl 形式的
SourceMap `eval`包裹 js 代码,很容易被 XSS 攻击,存在很大的安全隐患。 `eval` 不可预测,所以将会使用 slow path。 `eval` 我们一般只用于开发环境,不会用于打包线上环境的代码 |
错误代码的准确信息,源代码的错位位置 | |
| 外部 | 值 | 说明 | 错误提示 |
| source-map | 打包后的模块在模块后面会对应引用一个 .map 文件,同时在打包好的目录下会针对每一个模块生成相应的 .map 文件,会生成一个 index.js.map 文件,这个文件是一个典型的 sourcemap 文件 | 错误代码的准确信息,源代码的错位位置 | |
| hidden-source-map |
仍然会生成 `.map ` 文件,但是 打包后的代码中没有 sourceMappingURL ,也就是说请求代码时浏览器不会加载`.map ` 文件,控制台中看不到源代码 hidden-source-map 执行到错误后隐藏了错误源代码的位置,指挥提示原因和构建后代码的报错位置,相 较于我们的 default状态,他更不会暴露我的源代码。 |
错误代码的原因,没有源代码错误位置,只有构建后代码位置 | |
| nosources-source-map | SourceMap 中不包含 sourcesContent 内容,因此 调试时只能看到文件信息和行信息,无法看到源码。 | 有错误代码准确信息,没有源代码位置 | |
| cheap-source-map | 生成一个没有列信息
`(column-mappings)` 的
SourceMaps 文件,不包含 loader 的
SourceMaps (譬如 babel 的 sourcemap)
转换代码(行内) 生成的 SourceMaps 没有列映射,从 loaders 生成的 sourcemap 没有被使用 cheap-source-map 和正常的 source-map 相比只能精确到行,而正常的可以精确到列(不包括 的模式), cheap-source-map 在使用 babel-loader 时会自动转译(转译后的源代码会独立格式化分行 |
有错误代码准确信息,有源代码信息,只能精确到行 | |
| cheap-module-source-map | 生成一个没有列信息
`(column-mappings)` 的
SourceMaps 文件,
同时
loader 的
sourcemap
也被简化为只包含对应行的。 cheap-module-source-map 也一样不会精确到列,好像是一样的,但是这个不会被 babel-loader 影响,而 cheap-source-map 在使用 babel-loader 时会自动转译(转译后的源代码会独立格式化分行), 因为module模式会把loader的sourcemap也加 进来。 |
错误代码的准确信息,源代码的错位位置 |
内联 和 外部 的区别:
- 外部生成了文件,内联是没有的。
- 内联构建速度更快。
development (开发环境) devtool 配置
开发环境选择就比较容易了,只需要考虑打包速度快、调试方便,官方推荐以下4种:
- eval
- eval-source-map
- eval-cheap-source-map
- eval-cheap-module-source-map 大多数情况下我们选择
eval-cheap-module-source-map即可。
速度快(eval>inline>cheap>… :
- eval-cheap-souce-map(首推)
- eval-source-map
调试更友好:
- souce-map(首推)
- cheap-module-souce-map
- cheap-souce-map
最终平衡速度和调试,开发环境推荐的方案:
- eval-source-map(调试最友好)
- eval-cheap-module-souce-map(性能更好)
如果我们使用vue或者react框架开发,都会有对应的脚手架,而脚手架的配置默认是:
- eval-source-map
production (线上环境) devtool 配置
线上环境官方推荐的 devtool 有4种:
- none
- source-map
- hidden-source-map
- nosources-source-map 线上环境没有绝对的最优选择一说,根据自己业务需要去选择即可,很多项目也是选择除上述4种之外的
cheap-module-source-map选项。
内联会让代码体积变大,所以在生产环境不用内联
需要隐藏源代码的方案:
- nosources-source-map(全部隐藏)
- hidden-source-map(只隐藏源代码,会提示构建后代码错误信息)
生产环境推荐方案:
- source-map(调试最友好)
- cheap-module-souce-map(性能更好)
oneOf
正常来讲,一个文件只能被一个 loader 处理。当一个文件要被多个 loader 处理,那么一定指定 loader 执行的先后顺序:比如 JS 文件先执行 eslint 再执行 babel 。
通过oneOf 规则可以优化构建速度。当每个文件不再需要全部 rules 检查,而只需一条规则解析时,使用 oneOf 确保文件匹配到第一条合适的规则后停止后续匹配。正确安排 oneOf 内 loader 的顺序至关重要,避免同一文件需要多个 loader 处理,例如,将 eslint-loader 置于 oneOf 顶部,以优先处理 JS 文件。
问题:
| 假如我设置了七八个 loader 处理相应的文件,虽然 test 正则校验文件名称后缀不通过,但是每个文件还是都要经过一下这七八个loader,设置 oneOf 就是处理这个,如果找到了某一个文件的处理 loader,就直接用,不用再过后面的 loader,提高构建速度。 |
|---|
注意:不能有两个配置处理同一种类型文件。比如: eslint-loader 和 babel-loader 都处理同一种文件类型,所以可以把 eslint-loader 提取出来。
配置 Webpack.config.js
// webpack配置
module.exports = {
// 入口文件
entry: './src/index.js',
// 输出
output: {
// 输出文件名
filename: 'bundle.js',
// 输出路径 __dirname node.js变量,当前文件的目录绝对路径
path: path.resolve(__dirname, 'dist'),
// 自定义资源文件名
assetModuleFilename: 'images/[hash:10][ext][query]',
// 在生成文件之前清空 output 目录
clean: true,
},
// 开发服务器配置
devServer: {
// 从目录提供静态文件
static: {
directory: path.join(__dirname, "public"),
publicPath: "/",
},
// 启动后打开浏览器
open: true,
// 监听请求的端口号
port: 8080,
},
// loader配置
module: {
作用:提升打包构建速度(生产环境)
rules:[
{
test: /.js$/,
loader: 'eslint-loader',
},
{
// 以下 loader 只会匹配一个
// 注意oneOf中不能有两个配置处理同一种类型的文件,
oneOf: [
// 所以把eslint-loader提出去了
{
test: /.js$/,
loader: 'babel-loader'
},
{
test: /.css$/,
}
]
}
]
}
}
oneOf的作用就是优化生产环境的打包构建速度。
babel-loader 缓存
针对 js 兼容性进行缓存:( babel缓存)
babel-loader 的 options 设置中增加 cacheDirectory 属性,属性值为 true 。表示:开启 babel缓存 ,第二次构建时会读取之前的缓存,构建速度会更快一点。
babel-loader 设置
方式一:在options内添加属性
- cacheDirectory: true
方式二:
- use : ['babel-loader?cacheDirectory=true']
配置 Webpack.config.js
// webpack配置
module.exports = {
// 入口文件
entry: './src/index.js',
// 输出
output: {
// 输出文件名
filename: 'bundle.js',
// 输出路径 __dirname node.js变量,当前文件的目录绝对路径
path: path.resolve(__dirname, 'dist'),
// 自定义资源文件名
assetModuleFilename: 'images/[hash:10][ext][query]',
// 在生成文件之前清空 output 目录
clean: true,
},
// 开发服务器配置
devServer: {
// 从目录提供静态文件
static: {
directory: path.join(__dirname, "public"),
publicPath: "/",
},
// 启动后打开浏览器
open: true,
// 监听请求的端口号
port: 8080,
},
// loader配置
module: {
作用:提升打包构建速度(生产环境)
rules:[
{
test: /.js$/,
loader: 'eslint-loader',
},
{
// 以下 loader 只会匹配一个
// 注意oneOf中不能有两个配置处理同一种类型的文件,
oneOf: [
{
test: /.css$/,
use: [...commonCssLoader]
},
{
test: /.less$/,
use: [...commonCssLoader, 'less-loader']
},
/*
正常来讲,一个文件只能被一个loader处理。
当一个文件要被多个loader处理,那么一定要指定loader执行的先后顺序:
先执行eslint 在执行babel
*/
{
test: /.js$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
// 预设,提示babel怎么样做兼容性处理
// 基本的兼容性处理,
presets: [
[
"@babel/preset-env",
{
// 按需加载
useBuiltIns: "usage",
// 指定core-js版本
corejs: {
version: 3,
},
// 指定兼容性到哪个版本浏览器
targets: {
chrome: "60",
firefox: "60",
ie: "9",
safari: "10",
edge: "17",
},
},
],
],
// 开启babel缓存
// 第二次构建时,会读取之前的缓存
cacheDirectory: true cacheDirectory:true,
// 关闭缓存文件压缩
cacheCompression:false,
// 减少代码体积
plugins:["@babel/plugin-transform-runtime"],
},
},
},
]
}
]
}
}
文件资源缓存
注意,上面的服务器设置了 maxAge ,即开启 vv 。 通过 network 查看资源,刷新页面,可以看到资源来自 cache,即来自缓存,查看请求可以发现请求头设置了 Cache-Control, max-age=3600,即有效期为 3600s,一个小时,意思就是这个资源会被强制缓存一个小时
这个缓存会带来新的问题:
假如我们修改代码,修改一下js代码
import '../css/index.css';
function sum(...args) {
return args.reduce((p, c) => p + c, 0);
}
// eslint-disable-next-line
// console.log(sum(1, 2, 3, 4));// 修改前
// eslint-disable-next-line
console.log(sum(1, 2, 3, 4, 5));// 修改后
再次构建,打开浏览器刷新,会发现结果没有变化
| 这是因为当前资源在强制缓存期间,它是不会访问服务器的,直接读取本地缓存 这就带来了一个问题,假使我们的资源在强缓存期间出现了严重的 bug,开发人员需要紧急修复,但因为资源被强制缓存,就算修复了,也会因为还在强缓存期间而无效。 |
|---|
我们可以通过修改资源名称来解决这个问题
文件资源缓存
在打包输出文件的文件名中添加hash值
hash: 根据每次打包后 wepack 生成的 hash 值不同来加载资源会因为使用的都是 wepack 每 次打包后的 hash 值,导致改动一处,其他资源都改动,都重新加载了。 问题: 因为 js 和 css 同时使用一个 hash 值
如果重新打包,会导致所有缓存失效。(虽然只改动一个文件)
chunkhash: 打包来源同一个 chunk(代码块) 生成的 hash 值就一样 如果 css 在 js 中引入,则因为属于同一个 chunk (即是否属于同一个 entry ),导致改动一处,其 他资源都改动,都重新加载了。 问题: js 和 css 的 hash 值还是一样的
因为css是在js中被引入的,所以同属于一个chunk
contenthash: 根据文件的内容生成 hash 值。不同文件 hash 值一定不一样 。必须使 用了插件: extract-text-webpack-plugin/mini-css-extract-plugin
--> 让代码上线运行缓存更好使用
静态资源服务器
server.js
// server.js
// npm i express nodemon -S
// node server.js 或者 nodemon server.js
/*
服务器代码
启动服务器指令:
npm i nodemon -g
nodemon server.js
不需要下载nodemon
node server.js
访问服务器地址:
http://localhost:3000
*/
const express = require('express');
const app = express();
// express.static向外暴露静态资源
// maxAge 资源缓存的最大时间,单位ms
app.use(express.static('build', { maxAge: 1000 * 3600 }));
app.listen(3000);
配置 webpack.config.js
// webpack.config.js
const { resolve } = require('path');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const OptimizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
/*
缓存:
babel缓存
cacheDirectory: true
--> 让第二次打包构建速度更快
文件资源缓存
hash: 每次wepack构建时会生成一个唯一的hash值。
问题: 因为js和css同时使用一个hash值。
如果重新打包,会导致所有缓存失效。(可能我却只改动一个文件)
chunkhash:根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样
问题: js和css的hash值还是一样的
因为css是在js中被引入的,所以同属于一个chunk
contenthash: 根据文件的内容生成hash值。不同文件hash值一定不一样
--> 让代码上线运行缓存更好使用
*/
// 定义nodejs环境变量:决定使用browserslist的哪个环境
process.env.NODE_ENV = 'production';
// 复用loader
const commonCssLoader = [
MiniCssExtractPlugin.loader,
'css-loader',
{
// 还需要在package.json中定义browserslist
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: () => [require('postcss-preset-env')()]
}
}
];
module.exports = {
// 入口文件
entry: './src/index.js',
// 输出
output: {
// contenthash 只有在内容发生改变才会变
filename: '[name].[contenthash].js',
//输出路径 __dirname 代表当前文件的绝对路径
path: path.resolve(__dirname, 'dist'),
// 自定义资源文件名
//设置图片输出路径以及文件名称
assetModuleFilename: 'images/[hash:10][ext][query]',
// 在生成文件之前清空 output 目录
clean: true,
},
// 开发服务器配置
devServer: {
// 从目录提供静态文件
static: {
directory: path.join(__dirname, "public"),
publicPath: "/",
},
// 启动后打开浏览器
open: true,
// 监听请求的端口号
port: 8080,
},
// loader配置
module: {
rules: [
{
// 在package.json中eslintConfig --> airbnb
test: /.js$/,
exclude: /node_modules/,
// 优先执行
enforce: 'pre',
loader: 'eslint-loader',
options: {
fix: true
}
},
{
// 以下loader只会匹配一个
// 注意:不能有两个配置处理同一种类型文件
oneOf: [
{
test: /.css$/,
use: [...commonCssLoader]
},
{
test: /.less$/,
use: [...commonCssLoader, 'less-loader']
},
/*
正常来讲,一个文件只能被一个loader处理。
当一个文件要被多个loader处理,那么一定要指定loader执行的先后顺序:
先执行eslint 在执行babel
*/
{
test: /.js$/,
exclude: /node_modules/,
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
useBuiltIns: 'usage',
corejs: { version: 3 },
targets: {
chrome: '60',
firefox: '50'
}
}
]
],
// 开启babel缓存
// 第二次构建时,会读取之前的缓存
cacheDirectory: true cacheDirectory:true,
// 关闭缓存文件压缩
cacheCompression:false,
// 减少代码体积
plugins:["@babel/plugin-transform-runtime"],
},
// 图片资源
{
test: /.(jpg|png|gif)/,
loader: 'url-loader',
options: {
limit: 8 * 1024,
name: '[hash:10].[ext]',
outputPath: 'imgs',
esModule: false
}
},
// html 资源
{
test: /.html$/,
loader: 'html-loader'
},
// 其他资源
{
exclude: /.(js|css|less|html|jpg|png|gif)/,
loader: 'file-loader',
options: {
outputPath: 'media'
}
}
]
}
]
},
// 插件配置
plugins: [
//默认传键html文件,并引入打包输出的资源,默认为基本结构
new MiniCssExtractPlugin({
filename: 'css/built.[contenthash:10].css'
}),
new OptimizeCssAssetsWebpackPlugin(),
//将模板复制成指定文件
new HtmlWebpackPlugin({
template: './src/index.html',
minify: {
collapseWhitespace: true, //折叠空格
removeComments:true //移出注释
}
})
],
// 模式
mode: 'production',
devtool: 'source-map'
};
tree shaking 树摇
Tree shaking : 是一个用于优化 JavaScript 应用程序的术语,特别是在构建过程中减少生成的代码大小。它的主要目标是消除未使用的代码,即那些在应用程序中没有被引用或调用的部分,从而减小最终生成的 JavaScript 文件的大小。
在 Webpack 中,启动 Tree shaking功能必须同时满足三个条件:
- 使用 ESM 规范编写模块代码 (必须使用ES6模块化)
- 配置 optimization.usedExports 为
true,启动标记功能 - 启动代码优化功能,可以通过如下方式实现:
-
- 配置
mode = production - 配置
optimization.minimize = true - 提供
optimization.minimizer数组
- 配置
JS-Tree-Shaking
开发环境
配置 webpack.config.js
webpack.config.js 配置, 告诉 webpack 只打包导入模块中用到的内容:
// webpack.config.js
module.exports = {
// mode: "production",
mode: "development",
devtool: false,
optimization: {
// 目的使未被使用的export被标记出来
usedExports: true,
},
};
配置 package.json
package.json 配置, 告诉 webpack 哪些文件不做 Tree shaking:
"sideEffects": false --> 所有的代码都没有副作用。 (都可以进行 tree- shaking)
问题:
可能会把 Css/ @babel/polyfill (副作用) 文件干掉
"sideEffects": ["*.css", "*.less", "*.scss"],
生产环境
无需进行任何配置, webpack 默认已经实现了Tree shaking
注意点: 只有 ES Modle 导入才支持 Tree-Shaking 任何导入的文件都会受到 Tree-Shaking 的影响。
CSS-Tree-Shaking
安装相关插件
npm i -D purifycss-webpack purify-css glob-all
配置webpack.config.js
// webpack.config.js
const path = require('path')
const glob = require('glob')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const PurgeCSSPlugin = require('purgecss-webpack-plugin')
const PATHS = {
src: path.join(__dirname, 'src')
}
modeule.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: "[name].css",
}),
new PurgeCSSPlugin({
paths: glob.sync(`${PATHS.src}/**/*`, { nodir: true }),
}),
]
}
作用
Tree shaking 的主要作用是优化前端应用程序的性能,特别是在减小生成的 JavaScript 文件大小方面发挥关键作用。
| 作用 | 描述 |
|---|---|
| 减小文件大小 | Tree shaking 通过静态分析代码,识别和移除未使用的代码块。这样可以消除应用程序中未被引用或调用的部分,从而减小最终生成的 JavaScript 文件的大小。 |
| 提高加载速度 | 通过减小文件大小,Tree shaking 有助于提高应用程序的加载速度。用户在访问网站时需要下载的 JavaScript 代码更少,因此页面加载时间更短,用户体验更好。 |
| 网络传输优化 | 较小的文件大小意味着在网络上传输数据的成本更低。这对于用户在慢速或不稳定的网络条件下访问网站时尤为重要,可以减少加载时间和提高可访问性。 |
| 资源利用率 | 通过消除未使用的代码,Tree shaking 可以提高资源的利用率。只有实际需要的代码被包含在最终的构建中,因此减小了浏览器需要处理的代码量。 |
| 版本控制和部署 | Tree shaking 在版本控制和部署过程中也有帮助。较小的文件大小意味着在版本控制系统中占用的空间更小,并且在部署到生产环境时传输的数据更少,减少了部署时间和成本。 |
| 优化用户体验 | 更快的加载速度和更小的文件大小可以显著改善用户体验。用户更倾向于与快速加载的应用程序进行交互,因此通过 Tree shaking 优化可以提高应用的用户满意度。 |
code split 代码分割
打包代码时会将所有 js 文件打包到一个文件中,体积太大了。我们如果只要渲染首页,就应该只加载首页的 js 文件,其他文件不应该加载。
所以我们需要将打包生成的文件进行代码分割,生成多个 js 文件,渲染哪个页面就只加载某个 js 文件,这样加载的资源就少,速度就更快。
Code Split是什么
代码分割(Code Split)主要做了两件事:
- 分割文件:将打包生成的文件进行分割,生成多个 js 文件。
- 按需加载:需要哪个文件就加载哪个文件。
通过入口文件进行代码分割
多入口
文件目录
├── public
├── src
| ├── app.js
| └── main.js
├── package.json
└── webpack.config.js
安装依赖
npm i webpack webpack-cli html-webpack-plugin -D
新建文件
内容无关紧要,主要观察打包输出的结果
app.js
console.log("hello app");
main.js
console.log("hello main");
配置 webpack.config.js
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// 单入口
// entry: './src/main.js',
// 多入口: 有一个入口,最终输出就有一个 bundle
entry: {
main: "./src/main.js",
app: "./src/app.js",
},
output: {
path: path.resolve(__dirname, "./dist"),
// [name]是webpack命名规则,使用chunk的name作为输出的文件名。
// 什么是chunk?打包的资源就是chunk,输出出去叫bundle。
// chunk的name是啥呢? 比如: entry中xxx: "./src/xxx.js", name就是xxx。注意 是前面的xxx,和文件名无关。
// 为什么需要这样命名呢?如果还是之前写法main.js,那么打包生成两个js文件都会叫做 main.js会发生覆盖。(实际上会直接报错的)
filename: "js/[name].js",
clear: true,
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
mode: "production",
};
运行指令
npx webpack
此时在 dist 目录我们能看到输出了两个 js 文件。
总结: 配置了几个入口,至少输出几个 js 文件。
多入口提取公共模块
如果多入口文件中都引用了同一份代码,我们不希望这份代码被打包到两个文件中,导致代码重复,体积更大。我们需要提取多入口的重复代码,只打包生成一个 js 文件,其他文件引用它就好。
实例:
修改文件
app.js
// app.js
import { sum } from "./math";
console.log("hello app");
console.log(sum(1, 2, 3, 4));
main.js
// main.js
import { sum } from "./math";
console.log("hello main");
console.log(sum(1, 2, 3, 4, 5));
math.js
// math.js
export const sum = (...args) => {
return args.reduce((p, c) => p + c, 0);
}
配置 webpack.config.js
// webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// 多入口
entry: {
main: "./src/main.js",
app: "./src/app.js",
},
output: {
path: path.resolve(__dirname, "./dist"),
filename: "js/[name].js",
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
mode: "production",
// 压缩代码
optimization: {
// 代码分割配置
splitChunks: {
// 动态导入的模块其依赖会根据规则分离
chunks: 'async',
minSize: 30000,
// 文件至少被 1 个chunk 引用
minChunks: 1,
// 动态导入文件最大并发请求数为 5
maxAsyncRequests: 5,
// 入口文件最大并发请求数为 3
maxInitialRequests: 3,
// 文件名中的分隔符
automaticNameDelimiter: '~',
// 自动命名
name: true,
cacheGroups: {
// 分离第三方库
vendors: {
test: /[\/]node_modules[\/]/,
// 权重
priority: -10
},
// 分离公共的文件
default: {
// 文件至少被 2 个 chunk 引用
minChunks: 2,
priority: -20,
// 复用存在的 chunk
reuseExistingChunk: true
}
}
}
chunks
该参数有四种取值
async: 动态导入的文件其静态依赖会根据规则分离initial: 入口文件的静态依赖会根据规则分离all: 所有的都会根据规则分离chunks ==> Boolean: 返回 true 表示根据规则分离,false 则不分离
运行指令
npx webpack
此时我们会发现生成 3 个 js 文件,其中有一个就是提取的公共模块。
单入口
开发时我们可能是单页面应用(SPA),只有一个入口(单入口)。那么我们需要这样配置:
配置 webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
// 单入口
entry: "./src/main.js",
// 多入口
// entry: {
// main: "./src/main.js",
// app: "./src/app.js",
// },
output: {
path: path.resolve(__dirname, "./dist"),
// [name]是webpack命名规则,使用chunk的name作为输出的文件名。
// 什么是chunk?打包的资源就是chunk,输出出去叫bundle。
// chunk的name是啥呢? 比如: entry中xxx: "./src/xxx.js", name就是xxx。注意 是前面的xxx,和文件名无关。
// 为什么需要这样命名呢?如果还是之前写法main.js,那么打包生成两个js文件都会叫做 main.js会发生覆盖。(实际上会直接报错的)
filename: "js/[name].js",
clean: true,
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
mode: "production",
/*
可以将 node_module 中代码单独打包 一个 chunk 最终输出
*/
optimization: {
// 代码分割配置
splitChunks: {
// 对所有模块都进行分割
chunks: "all",
/*
以下是默认值:
// 分割代码最小的大小
minSize: 20000,
// 类似于minSize,最后确保提取的文件大小不能为0
minRemainingSize: 0,
// 至少被引用的次数,满足条件才会代码分割
minChunks: 1,
// 按需加载时并行加载的文件的最大数量
maxAsyncRequests: 30,
// 入口js文件最大并行请求数量
maxInitialRequests: 30,
// 超过50kb一定会单独打包(此时会忽略minRemainingSize、maxAsyncRequests、 maxInitialRequests)
enforceSizeThreshold: 50000,
// 组,哪些模块要打包到一个组
cacheGroups: {
// 组名
defaultVendors: {
// 需要打包到一起的模块
test: /[\/]node_modules[\/]/,
// 权重(越大越高)
priority: -10,
// 如果当前 chunk 包含已从主 bundle 中拆分出的模块,则它将被重用,而不是生成新 的模块
reuseExistingChunk: true,
},
// 其他没有写的配置会使用上面的默认值
default: {
// 这里的minChunks权重更大
minChunks: 2,
priority: -20,
reuseExistingChunk: true,
},
},
*/
},
};
按需加载,动态导入
当涉及到动态代码拆分时,webpack 提供了两个类似的技术。
第一种,也是推荐选择的方式是,使用符合 ECMAScript 提案 的 import() 语法 来实现动态导入。
第二种,则是 webpack 的遗留功能,使用 webpack 特定的 require.ensure 。
使用 import() 来进行动态导入
- 语法:
import(/* webpackChunkName: chunkName */ chunk)
.then( res => {
// handle something
})
.catch(err => {
// handle err
// 加载失败
});
/* chunkName */ 为指定代码分割包名,chunk指定需要代码分割的文件入口。注意不要把 import关键字和import()方法弄混了,该方法是为了进行动态加载。
main.js
console.log("hello main");
document.getElementById("btn").onclick = function () {
// 动态导入 --> 实现按需加载
// 即使只被引用了一次,也会代码分割
import("./math.js").then(({ sum }) => {
alert(sum(1, 2, 3, 4, 5));
});
};
app.js
console.log("hello app");
public/index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial- scale=1.0"/>
<title>Code Split</title>
</head>
<body>
<h1>hello webpack</h1>
<button id="btn">计算</button>
</body>
</html>
配置 webpack.config.js
// webpack.config.js:
var webpack = require('webpack');
var path = require('path');
module.exports = {
entry: {
'pageA': './src/pageA',
// 指定单独打包的第三方库(和CommonsChunkPlugin结合使用),可以用数组指定多个
'vendor': ['lodash']
},
output: {
path: path.resolve(__dirname, './dist'),
filename: '[name].bundle.js',
// code splitting的chunk是异步(动态)加载,需要指定chunkFilename(具体可以 了解和filename的区别)
// 给打包输出的其他输出文件命名
chunkFilename: '[name].chunk.js',
// 动态加载的路径
publicPath: './dist/'
},
plugins: [
// 为第三方库和和manifest(webpack runtime)单独打包
new webpack.optimize.CommonsChunkPlugin({
// 为异步公共加载的代码打一个的包
async: 'async-common', // 异步公共的代码
// 要加上children,会从入口的子依赖开始找
children: true,
name: ['vendor', 'manifest'],
minChunks: Infinity
}),
]
}
require.ensure代码分割
-
语法:
require.ensure(dependencies: String[], callback: function(require), chunkName: String) -
参数:
- 第一个参数是 dependencies 依赖列表,webpack会加载模块,但不会执行。
- 第二个参数是一个回调,当所有的依赖都加载完成后,webpack 会执行这个回调函数,在其中可以使用
require导入模块,导入的模块会被代码分割到一个分开的 chunk 中。 - 第三个参数指定第二个参数中代码分割的 chunkname。
src/pageA.js
// src/pageA.js import * as _ from 'lodash'; import subPageA from './subPageA'; import subPageB from './subPageB'; console.log('this is pageA'); export default 'pageA'; src/subPageA.js
// src/subPageA.js import module from './module'; console.log('this is subPageA'); export default 'subPageA'; src/subPageB.js
// src/subPageB.js import module from './module'; console.log('this is subPageB'); export default 'subPageB';src/module.js
// src/module.js const s = 'this is module' export default s;
修改src/pageA.js,把import导入方式改成 require.ensure的方式就可以代码分割
// import subPageA from './subPageA';
// import subPageB from './subPageB';
require.ensure([], function() {
// 分割./subPageA模块
var subPageA = require('./subPageA');
}, 'subPageA');
require.ensure([], function () {
var subPageB = require('./subPageB');
}, 'subPageB');
用了 require.ensure 的模块被代码分割了,达到了我们想要的目的。由于 subPageA 和 subPageB 有公共模块 module.js ,打开 subPageA.chunk.js 和 subPageB.chunk.js 发现都有公共模块module.js ,这时候就需要在 require.ensure 代码前面加上require.include('./module')
require.include('./module'); // 加在require.ensure代码前
配置 webpack.config.js
output: {
...
publicPath: './dist/' // 动态加载的路径
}
统一资源输出
配置 webpack.config.js
module.exports = {
// 入口
entry: "./src/main.js"
// 输出
output: {
// 所有文件的输出路径
// __dirname node.js 的变量, 代表当前文件的文件夹目录
path: path.resolve (__dirname, "../dist"), // 绝对路径
// 入口文件打包输出文件名
filename: "static/js/[name].js",
// 给打包输出的其他文件命名
chunkFilename: "static/js/[name].chunk.js",
// 图片、字体等 通过 type: "asset" 处理的资源命名方式
assetModuleFilename: "static/media/[hash:10][ext][query]",
// 自动清空上次打包的内容
// 原理: 在打包前,将 path 整个目录内容清空,再进行打包
clean: true,
},
// 加载器
module: {
rules: [
oneOf: [
{
test:/.css$/, // 只检测 .css 文件
use: getStyleLoader() // 执行顺序,从右到左 (从下到上)
},
{
test:/.less$/,
// loader: 'xxx' 只能使用1个loader
use: getStyleLoader(less-loader)
},
{
test:/.s[ac]ss$/,
use: getStyleLoader(sass-loader)
},
{
test:/.styl$/,
use: getStyleLoader(stylus-loader)
},
{
test:/.(png|jpe?g|gif|webp|svg)$/,
type: "asset",
parser: {
dataUrlCondition: {
// 小于 10 kb的图片转 base64
// 优点: 减少请求数量, 缺点: 体积会更大
maxSize: 10 * 1024, // 10kb
}
},
generator: {
// 输出图片名称
// [hash: 10] 取 hash值 前10位
filename: 'static/images/[hash:10][ext][query]'
}
},
{
test: /.js$/,
// exclude: /node_modules/, 排除 node_modules 下的文件,
其他文件都处理
// 只处理 Src 下的文件, 其他文件不处理
include: path.resolve (__dirname,"../src")
use: [
{
loader: "thread-loader", // 开启多进程
option: {
works: threads, // 进程数量
}
},
{
loader: "babel-loader",
option: {
// presets : ["@babel/preser-env"]
cacheDirectory: true, // 开启babel 缓存
cacheCompression: false, // 关闭缓存文件压缩
// 减少代码体积
plugin: [
"@babel/plugin-transform-runtime"
],
}
}
]
}
]
]
},
// 插件
plugins: [
// plugin 的配置
new ESLintPlugin ({
// 检测哪些文件
conntext: path.resolve(__dirname,"../src"),
exclude: "node_modules", // 默认值
cache: true, // 开启缓存
cacheLocation: path.resolve(
__dirname,"../node_modules/.cache/eslintcache"
),
threads, // 开启多进程和设置进程数量
}),
new HtmlWebpackPlugin ({
// 模版, 以 public/index.html 文件创建 新的 Html 文件
// 新的 Html 文件 特点: 1. 结构和原来一致 2. 自动引入打包输出的资源
template: path.resolve(__dirname,"../public/index.html")
}),
new MinCssExtractPlugin({
// 多入口 css
filename: "static/css/[name].css",
// 给打包输出的其他css文件命名
chunkFilename: "static/css/[name].chunk.css",
})
]
}
懒加载和预加载
我们前面已经做代码分割,同时会使用 import 动态导入语法来进行代码 按需加载 (我们也叫懒加载, 比如路由懒加载就是这样实现的)。
但是加载速度还不够好, 比如 : 用户点击按钮时 才加载这个资源的, 如果资源体积很大, 那么用户就会感觉到明显的卡顿效果。
我们想在浏览器空闲时间,加载后续需要使用的资源。 我们就需要 用上 Preload 和 Prefetch 技术。
懒加载
懒加载或者按需加载,会在文件需要使用时才加载,是一种很好的优化网页或应用的方式。
懒加载的使用加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>webpack</title>
</head>
<body>
<h1>hello lazy loading</h1>
<button id="btn">按钮</button>
</body>
</html>
test.js
console.log('test.js文件被加载了~');
export function mul(x, y) {
return x * y;
}
export function count(x, y) {
return x - y;
}
index.js
// index.js
console.log('index.js文件被加载了~');
import { mul } from './test';
document.getElementById('btn').onclick = function() {
console.log(mul(4, 5));
};
懒加载写法
index.js
// index.js
console.log('index.js文件被加载了~');
// import { mul } from './test';
document.getElementById('btn').onclick = function() {
// 懒加载~:当文件需要使用时才加载~
import(/* webpackChunkName: 'test'*/'./test').then(({ mul }) => {
console.log(mul(4, 5));
});
};
懒加载其实就是用到了 code splitting 文章中动态导入的方法
是什么
Preload(预加载): 告诉浏览器立即加载资源
<!-- 使用 link 标签静态标记需要预加载的资源 -->
<link rel="preload" href="/path/to/style.css" as="style">
•
<!-- 或使用脚本动态创建一个 link 标签后插入到 head 头部 -->
<script>
const link = document.createElement('link');
link.rel = 'preload';
link.as = 'style';
link.href = '/path/to/style.css';
document.head.appendChild(link);
</script>
href 表示需要预加载的资源路径
as属性 指定预加载资源的类型有“script’ /‘style’/ "font"
相比之下, Prefetch 是在页面加载后不紧急需要但将来可能需要使用的资源进行预加载。
Prefetch (预获取): 告诉浏览器空闲时才加载资源
<link rel="prefetch" href="path/to/resource"/>
prefetch(预获取) / preload(预加载) 两种方式
函数的方式
math.js 文件
首先创建 math.js 文件。并且添加两个函数
// math.js
export const add = (x, y) => {
return x + y;
};
export const sub = (x, y) => {
return x - y;
};
index.js
在 index.js 入口文件中使用 math.js
// index.js
const btn = document.createElement("button");
btn.textContent = "按钮";
btn.addEventListener("click", () => {
// /* webpackChunkName: 'math' */ 指定打包后文件名为 math
// 最终文件名为 math.build.js ,这是因为 在 webpack.config.js 配置 output 下 的 filename
import(/* webpackChunkName: 'math' */ "./math.js").then(({ add }) => {
console.log(add(1, 2))
});
});
document.body.appendChild(btn);
执行 npx webpack
执行 npx webpack 打包 src 目录下文件。结果如下:生成了 新的 math.build.js
执行 npx webpack-dev-server 启动 web 服务器。页面展示一个按钮,并且点击按钮后,才会加载 math.build.js 文件。
prefetch 预加载
index.js
// index.js
const btn = document.createElement("button");
btn.textContent = "按钮";
btn.addEventListener("click", () => {
// webpackPrefetch 设置 为 true
import(/* webpackChunkName: 'math',webpackPrefetch: true */ "./math.js").then(({ add }) => {
console.log(add(1, 2))
});
});
document.body.appendChild(btn);
启动 webpack 服务器。
npx webpack-dev-server
默认地址是: 127.0.0.1:8080。 刷新浏览器,查看 network,可以发现math.build.js 已经加载下来。 并且 index.html 文件,增加了link 标签,链接到 math.build.js 文件。 rel=“prefetch” 这里使用 prefetch 预加载,当我们首页内容加载完毕,在网络空闲的时候,才会加载 link对应的文件
index.html
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<link rel="prefetch" as="script" href="http://127.0.0.1:8080/math.build.js">
</head>
preload 预加载
打开 index.js 文件, 在 import() 函数中,加入参数 webpackPreload: true
index.js
// index.js
const btn = document.createElement("button");
btn.textContent = "按钮";
btn.addEventListener("click", () => {
// webpackPreload: true
import(/* webpackChunkName: 'math', webpackPreload: true */ "./math.js").then(({ add }) => {
console.log(add(1, 2))
});
});
document.body.appendChild(btn);
效果和懒加载一样,也是点击按钮后才会加载 math.js 文件。
配置 webpack.config.js
// webpack.config.js
let path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin")
const MinCssExtractPlugin = require("mini-css-extract-plugin")
const CssMinmizerPlugin = require("css-minimizer-webpack-plugin")
module.exports = {
// 入口文件
entry: {
// index: {
// import: "./src/index.js",
// dependOn: 'shared'
// },
// another: {
// import: "./src/index2.js",
// dependOn: 'shared'
// },
// // 当两个入口文件有 lodash 模块时就会抽离出来,并且取名 为 shared.js
// shared: 'lodash'
index: "./src/index.js",
another: "./src/index2.js"
},
// 打包后文件
output: {
// 配置打包后,入口文件名字
filename: "[name].build.js",
path: path.resolve(__dirname, './dist'),
clean: true, // 每次打包前先删除 dist 目录
// contenthash 表示哈希值
// ext 表示扩展名
// 图片资源文件 打包后输出
assetModuleFilename: "images/[contenthash][ext]"
},
// 模式 development/production
mode: 'development',
// 打包后,可以方便的调试代码。代码的位置和源文件一致。
devtool: "inline-source-map",
// 插件使用。
// HtmlWebpackPlugin 用于自动生成 dist 目录下 index.html 魔板文件
plugins: [
new HtmlWebpackPlugin({
// 根据魔板文件生成
template: "./index.html",
// 生成 dist 目录中 html 的文件名
filename: "app.html",
// 生成的 js 文件 引入到body标签中
inject: "body",
minify: {
collapseWhitespace: true, //折叠空格
removeComments:true //移出注释
}
}),
// 把 css 合并成一个文件
new MinCssExtractPlugin({
// 打包文件,放到 dist 下的 styles 目录下
// contenthash 哈希字符串
filename: "styles/[contenthash].css"
})
],
// 指定 dist 作为根路径
devServer: {
static: "./dist"
},
// 配置打包 其它资源文件 规则
module: {
rules: [
{
test: /.png$/,
type: "asset/resource",
// 打包后文件命名
// 优先级高于 output 下 assetModuleFilename
generator: {
filename: "images/[contenthash][ext]"
}
},
{
test: /.svg$/,
// 导出资源类型的 dataurl (base64格式)
type: "asset/inline"
},
{
test: /.txt$/,
// 导出资源的源代码
type: "asset/source"
},
{
test: /.jpg$/,
// 会根据 resource / inline 两种方式进行选择
// 默认情况下,当资源文件大于 8k 选择 resource 模式生成资源
// 当资源文件小于8k 选择 inline 生成 base64 数据。
type: "asset",
parser: {
dataUrlCondition: {
// 设置临界值,超过 maxSize 就会 使用 asset/resource 模式,否则使用 asset/inline 生成 base64 代码
maxSize: 4 * 1024 * 1024 // 4 M
}
}
},
{
test: /.(css|less)$/,
// loader 执行顺序是从右到左(三个loader位置不能颠倒)
use: [MinCssExtractPlugin.loader, 'css-loader', 'less-loader']
},
{
test: /.(woff|woff2|eot|ttf|otf)$/,
// asset/resource 可以帮助我们载入任何类型的资源
type: "asset/resource"
},
{
test: /.(csv|tsv)$/,
use: "csv-loader"
},
{
test: /.xml$/,
use: "xml-loader"
},
{
test: /.js$/,
// 排除 node_modules 目录下的 js 文件
exclude: /node_modules/,
use: {
loader: "babel-loader",
options: {
presets: ["@babel/preset-env"]
}
}
}
]
},
// 优化配置
optimization: {
minimizer: [
new CssMinmizerPlugin()
],
// 实现代码分割,并且保存到单独的一个文件里。
splitChunks: {
chunks: 'all'
}
}
}
插件方式(plugin)
安装依赖
$ npm install --save-dev preload-webpack-plugin
配置 webpack.config.js
// webpack.config.js
const PreloadWebpackPlugin = require('preload-webpack-plugin');
module.exports = {
entry: {
app: './src/main.js',
preload: './src/test.js' //定义需要预加载的文件
},
plugins: [
// ... 其他插件
new PreloadWebpackPlugin({
rel: 'preload', // preload兼容性更好
as(entry) { //资源类型
if (/.css$/.test(entry)) return 'style';
if (/.woff$/.test(entry)) return 'font';
if (/.png$/.test(entry)) return 'image';
return 'script';
},
// 可以是 'initial', 'async', 'all', 或者指定 chunk 名称的数组
include: ['preload']
// rel: 'prefetch', // prefetch兼容性更差
// include: 'allChunks',
fileWhitelist: [/.css$/, /.js$/], // 资源白名单
fileBlacklist: [/.svg/] // 资源黑名单
// 可以是 'low', 'medium', 'high' 或者一个正整数值
priority: 'low' ,
})
//我们将优先级设置为 "low",这意味着被预加载的资源将被赋予较低的优先级。你也可以将优 先级设置为 "medium" 或 "high",以指定更高的优先级
}),
],
// ... 其他webpack配置
};
Preload和Prefetch的共同点 :
- 都只会加载资源,并不执行。
- 都有缓存
Preload和Prefetch的区别 :
-
一个 预加载块(preload) 开始与父块并行加载。预取的块(prefetch) 在父块完成加载后启动。
-
预加载块(preload) 具有中等优先级,可以立即下载。而 预取块(prefetch) 在浏览器空闲时下载预取的块。
-
一个 预加载块(preload) 应该被父块立即请求。预取块(prefetch) 可以在将来的任何时候使用。
-
浏览器的支持能力是不同的。
虽然说prefetch 或者是 preload 好,但是我们千万要记得,不要所有的异步加载模块都使用这个东西,我们应该根据自己的业务去加载,否则页面性能不是达到最优而是长时间的等待加载。
加载方式的对比
- 正常加载:可以认为是并行加载(同一时间加载多个文件)
- 懒加载:当文件需要使用时才加载~
- 预加载 prefetch:会在使用之前,提前加载js文件 等其他资源加载完毕,浏览器空闲了,再偷偷加载资源
最佳实践
基于上面对使用场景的分享,我们可以总结出一个比较通用的最佳实践:
- 大部分场景下无需特意使用preload
- 类似字体文件这种隐藏在脚本、样式中的首屏关键资源,建议使用preload
- 异步加载的模块(典型的如单页系统中的非首页)建议使用prefetch
- 大概率即将被访问到的资源可以使用prefetch提升性能和体验
总结和踩坑:
1、preload 和 prefetch 的本质都是预加载,即先加载、后执行,加载与执行解耦。2、preload 和 prefetch不会阻塞页面的
onload。3、preload 用来声明当前页面的关键资源,强制浏览器尽快加载;而prefetch 用来声明将来可能用到的资源,在浏览器空闲时进行加载。
4、不要滥用preload 和 prefetch,需要在合适的场景中使用。
5、preload 的字体资源必须设置
crossorigin属性,否则会导致重复加载。 原因是如果不指定crossorigin属性(即使同源),浏览器会采用匿名模式的 CORS 去 preload,导致两次请求无法共用缓存。6、关于preload 和prefetch 资源的缓存,在 Google 开发者的一篇文章中是这样说明的:如果资源可以被缓存(比如说存在有效的
cache-control和max-age),它被存储在HTTP 缓存(也就是disk cache)中,可以被现在或将来的任务使用;如果资源不能被缓存在 HTTP 缓存中,作为代替,它被放在内存缓存中直到被使用。
PWA 离线可访问
渐进式网络应用程序(PWA) ,是一种可以提供类似于原生应用程序(native app)体验的网络应用程序(web app)。
PWA 可以用来做很多事。其中最重要的是,在离线时应用程序能够继续运行功能。
下载依赖
npm i workbox-webpack-plugin -D
配置webpack.config.js
// webpack.config.js
const WorkboxPlugin = require('workbox-webpack-plugin');
module.exports = {
plugins: [
new WorkboxPlugin.GenerateSW({
// 这些选项帮助 ServiceWorkers 快速启用
// 不允许遗留任何“旧的” ServiceWorkers
/* 更多配置详见:https://developers.google.com/web
/tools/workbox/modules/workbox-webpack-plugin
*/
/*
1. 帮助 serviceworker 快速启动
2. 删除旧的 serviceWorker
*/
clientsClaim: true,
skipWaiting: true,
//打包到本地, 默认值是'cdn' 访问的是国外cdn需要翻墙
importWorkboxFrom: 'local',
include: [/.html$/, /.js$/, /.css$/], //包含资源
exclude: [/.(png|jpg|gif|svg)/] //排除资源
})
]
}
注册 Service Worker
index.js
// index.js
// 处理兼容性问题
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/service- worker.js').then(registration => {
console.log('SW registered: 成功 ', registration);
}).catch(registrationError => {
console.log('SW registration failed:失败 ', registrationError);
});
});
}
package.json ESLint配置
让其支持浏览器变量(window、navigator等全局变量)
/*
eslint 不认识 window、navigator 全局变量
解决: 需要修改 package.json 中的 EslintConfig 配置
"env": {
"browser": true // 支持浏览器端全局变量
}
*/
{
"eslintConfig": {
"extends": "airbnb-base",
"env": {
"browser": true
}
}
}
启动一个本地服务器
npm install http-server --save-dev
$ npx http-server dist
# 或者
npm i serve
$ npx serve dist
多进程打包
js单线程,同一时间只能干一件事,排队等很久才能干下一件事,所以我们就需要使用多线程
开启电脑的多个进程同时干一件事。我们想要继续提升打包速度,其实就是要提升 js 的打包速度,而对 js 文件处理主要就是 eslint 、babel、Terser 三个工具。
我们启动进程的数量就是我们 CPU 的核数。每个进程启动大概为600ms,进程通信也有开销,不要滥用。
一般用在 babel-loader 下
下载依赖
npm install thread-loader --save -dev
# 或者
npm i thread-loader -D
配置 Webpack.config.js
//webpack.config.js
{
test: /.js$/,
exclude: /node_modules/,
use: [
// 开启多进程打包(babel工作的时候就会开启多进程)
/*
开启多进程打包是有利有弊的(合理使用)
进程启动大概600ms,进程通信也有开销(时间)
只有工作消耗时间比较长,才需要多进程
一般来说js代码比较多,消耗时间比较长
启动进程数(cpu核数-1)
*/
//'thread-loader',
//如下可做调整
{
loader: 'thread-loader',
options: {
// 进程数量可控的配置
workers: 2 // 进程2个
}
},
{
loader: 'babel-loader',
options: {
presets: [
[
'@babel/preset-env',
{
// 按需加载
useBuiltIns: 'usage',
corejs: {version: 3},
targets: { chrome: '60' }
}
]
],
// 开启babel缓存
// 第二次构建时,会读取之前的缓存
cacheDirectory: true
}
]
}
}
cache-loader
缓存资源,提高二次构建的速度,使用方法是将cache-loader 放在比较费时间的 loader 之前,比如 babel-loader,由于启动项目和打包项目都需要加速,所以配置在 webpack.config.js
安装依赖
npm i cache-loader -D
配置 webpack.config.js
// webpack.config.js
{
test: /.js$/,
use: [
'cache-loader'
'thread-loader',
'babel-loader'
],
}
外部扩展(Externals)
externals 配置项提供了阻止将某些 import 的包 (package) 打包到 bundle 中的功能,在运行时 ( runtime) 再从外部获取这些扩展依赖(external dependencies)
externals 用于提取第三方依赖包, 使用 cdn资源的方式 将第三方依赖包引入项目,可以大大减少项目打包体积
externals用法
module.exports={
configureWebpack:congig =>{
externals:{
key: value
}
}
}
语法说明
- key 是第三方依赖库的名称 ,同 package.json 文件中的 dependencies 对象的
key一样
"devDependencies": {
"webpack": "^4.41.6",
"webpack-cli": "^3.3.11",
"webpack-dev-server": "^3.10.3",
"workbox-webpack-plugin": "^5.0.0"
},
- value 值可以是字符串、数组、对象 。应该是第三方依赖编译打包后生成的
js(要用CDN引入的 js 文件)文件,执行后赋值给window的全局变量名称。在控制台打印window.xxx,value 就是xxx
- 有些 JavaScript 运行环境可能内置了一些全局变量或者模块,例如在你的 HTML BODY 标签里通过以下代码:
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.js"></script>
引入 jquery 后,全局变量 jQuery 就会被注入到网页的 JavaScript 运行环境里。就可以直 接通过 window.$ 来访问 jQuery 对象。
- 当运行环境内置了 jQuery 全局变量 (即在 body 中使用
script标签引入了 jquery 框架),如果同时又使用模块化的方式安装并使用了 jQuery(npm install jquery --save),可能这个时候就会出现重复引用框架的问题。
// 我们不想这么用
// const $ = window.jQuery
// 而是这么用
const $ = require("jquery")
$("#content").html("<h1>hello world</h1>")
当构建后你会发现输出的 bundle.js 里包含的 jQuery 库的内容,这导致 jQuery 库出现了2次,第一次是 body 中的 script 标签加载,第二次是 bundle.js 中加载,浪费加载流量,最好是 bundle.js里不会包含 jQuery 库的内容。那这个时候就用上 Externals 的配置项了
配置 webpack.config.js
// webpack.config.js
const path = require('path');
module.exports = {
entry: './src/main.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, './dist'),
},
externals: {
/**
* externals 对象属性解析。
* 基本格式:
* '包名' : '在项目中引入的名字'
*
*/
jquery: "jQuery"
},
/* 函数表达
ctx.context (string): 包含引用的文件目录。
ctc.request (string): 被请求引入的路径。
ctx.contextInfo (object): 包含 issuer 的信息(如,layer 和 compiler)
ctx.getResolve 5.15.0+: 获取当前解析器选项的解析函数。
callback (function (err, result, type)): 用于指明模块如何被外部化的回调函数
回调函数接收三个入参
err (Error): 被用于表明在外部外引用的时候是否会产生错误。如果有错误,这将会是唯 一被用到的参数。
result (string [string] object): 描述外部化的模块。可以接受其它标准化外部 化模块格式,(string, [string],或 object)。
type (string): 可选的参数,用于指明模块的 external type(如果它没在 result 参数中被指明)。
*/
externals: function(context, request, callback) {
if (request === 'lodash') {
return callback(null, '_');
}
callback();
}
};
使用插件
安装依赖
npm i html-webpack-externals-plugin -D
配置webpack.config.js
// webpack.config.js
const HtmlWebpackExternalsPlugin = require('html-webpack-externals-plugin');
module.exports = {
plugins: [
new HtmlWebpackExternalsPlugin({
externals: [{
module: 'vue',
entry:'https://lib.baomitu.com/vue/2.6.12/vue.min.js',
global: 'Vue'
}]
})
],
}
动态添加 CDN 到 index.html中
<script type="text/javascript" src="https://lib.baomitu.com/vue/2.6.12/vue.min.js"></script>
dll (动态链接库)
什么是dll?为什么需要dll?
类似 externals,会指示 webpack那些库是不参与打包的,不同的是 webpack 会单独对某些库进行单独打包,将多个库打包成一个 chunk ,可以有效避免对这些包的重复打包。
node_modules 内的某些库比较大,正常打包的话会被打包成一个文件,这样文件体积增大。通过 dll 将这些库单独拆开,打包成不同的 chunk,更有利于性能优化
dll 打包后能,webpack 运行打包后不会重复打包 第三方依赖库 (第三方库:jquery、react、vue...),提高效率。
特点: 只会在内存中编译打包,不会有任何输出
适用范围
Dll 文件里只适合放置不常改动的代码,比如说第三方库(谁也不会有事无事就升级一下第三方库吧),尤其是本身就庞大或者依赖众多的库。如果你自己整理了一套成熟的框架,开发项目时只需要在上面添砖加瓦的,那么也可以把这套框架也打包进 Dll 文件里,甚至可以做到多个项目共用这一份 Dll 文件。
所需插件
DllPlugin
此插件用于在单独的 webpack 配置中创建一个 dll-only-bundle。 此插件会生成一个名为 manifest.json 的文件,这个文件是用于让 DllReferencePlugin 能够映射到相应的依赖上。
| 属性 | 描述 |
|---|---|
| context(可选) | manifest 文件中请求的 context (默认值为 webpack 的 context) |
| format (boolean = false) | 如果为 true,则 manifest json 文件 (输出文件) 将被格式化 |
| name | 暴露出的DLL 的函数名(TemplatePaths:[fullhash] & [name] ) |
| path | manifest json文件的 绝对路径(输出文件) |
| entryOnly (boolean = true) | 如果为 true,则仅暴露入口 type:dll bundle的类型 |
DllReferencePlugin
此插件配置在 webpack 的主配置文件中,此插件会把 dll-only-bundles 引用到需要的预编译的依赖中。
| 属性 | 描述 |
|---|---|
| context | (绝对路径) manifest (或者是内容属性)中请求的上下文 |
| extensions | 用于解析 dll bundle 中模块的扩展名 (仅在使用 scope 时使用)。 |
| manifest | 用于解析 dll bundle 中模块的扩展名 (仅在使用 scope 时使用)。 |
| content (可选) | 请求到模块 id 的映射(默认值为 manifest.content ) |
| name (可选) | dll 暴露地方的名称(默认值为 manifest.name )(可参考externals ) |
| scope (可选) | dll 中内容的前缀 |
| sourceType (可选) | dll 是如何暴露的 (libraryTarget) |
AddAssetHtmlPlugin
该插件会将给定的 JS 或 CSS 文件添加到 Webpack 知道的文件中,并将其放入 html-webpack-plugin 注入到生成的 html 中的资产列表中。将插件添加到配置中,并为其提供一个文件路径
| 属性 | 描述 | 默认值 |
|---|---|---|
| filepath | 要添加到编译和生成的 HTML文件的绝对路径 | 必须,除非已定义 glob |
| glob | 用于查找要添加到编译中的文件的 glob。使用方法请参阅 globby 4 文档 | 必须,除非已定义 glob |
| files | 默认情况下,资产将包含在所有文件中。如果定义了文件,资产将只包含在指定的文件球中 | [] |
| hash | 如果为 “true”,则会将文件的唯一哈希值追加到文件名中。这对消除缓存很有用 | false |
| includeRelatedFiles | 如果为 “true”,则会在编译时添加 filepath + ‘.*’ 。例如,filepath.map 和 filepath.gz | true |
| outputPath | 如果设置,将用作文件的输出目录 | |
| outputPath | 如果设置,将用作脚本或链接标记的公共路径 | |
| typeOfAsset | 设置为 css,以创建链接标记,而不是脚本标记 | js |
| attributes | 要添加到生成标签中的额外属性。例如,在多填充脚本中添加 nomodule 就很有用。属性对象使用 key 作为属性名称,使用 value 作为属性值。如果 value 仅为 true,则不会添加任何值 | {} |
安装依赖
npm i add-asset-html-webpack-plugin -D
配置Dll文件
- 运行 webpack 时,默认查找 webpack.config.js 配置文件,需要运行下面的指令。
npx webpack --config webpack.dll.config.js
2. 在 webpack.config.js 的同名文件创建一个 webpack.dll.js 文件,并在其中对 jquery 进行单独打包。
// webpack.dll.config.js
/**
* 使用dll技术,对某些库(第三方库:jQuery,vue, react...)进行单独打包
* 使用 DllPlugin 插件 配置
*/
const DllPlugin = require('webpack/lib/DllPlugin');
const { resolve } = require('path');
const webpack = require('webpack');
module.exports = {
// 入口文件
entry: {
vendors: ["jquery"],
// jquery: ['jquery']
},
// 输出
output: {
filename: '[name].dll.js',
path: resolve(__dirname,'dll'),
/*
存放相关的dll文件的全局变量名称,比如对于jquery来说的话就是 _dll_jquery, 在 前面加 _dll
是为了防止全局变量冲突。
*/
library: '[name]_[hash]',
clean: true,
},
// 插件配置
plugins: [
// 用于清除上次生成的包
new CleanWebpackPlugin(),
// 打包生成一个manifest.json提供和jquery的映射
new DllPlugin({
/*
该插件的name属性值需要和 output.library保存一致,该字段值,也就是输出的 manifest.json文件中name字段的值。
比如在jquery.manifest文件中有 name: '_dll_jquery'
*/
name: '_dll_[name]',
/* 生成manifest文件输出的位置和文件名称 */
path: path.join(__dirname, 'lib/', '[name].manifest.json')
})
],
// 模式
mode: 'production'
}
配置 webpack.config.js
告诉 webpack 哪些库不参与打包,并使用插件将 dll 已经打包好的 jquery 文件进行引入,这样以后 jquery 都不需要参与打包了,因为已经打包好了,我们只需要引入即可。
// webpack.config.js
const HtmlWebpackPlugin = require('html-webpack-plugin');
const {resolve} = require('path');
const webpack = require('webpack');
const AddAssetHtmlWebpackPlugin = require('add-asset-html-webpack-plugin')
module.exports = {
entry: './src/index.js',
// 项目中用到的依赖库文件
// echartsbt: ['echarts']
output: {
filename: 'built.js',
path: resolve(__dirname,'build')
// 在生成文件之前清空 output 目录
clean: true,
},
module: {
rules: [
]
},
plugins:
// 复制一份HTML文件,并自动引入打包资源(js/css)
new HtmlWebpackPlugin({
template: "./src/index.html",
}),
// 告诉webpack 哪些库不参与打包
new DllReferencePlugin({
// echarts 映射到json文件上去
manifest: resolve(__dirname,'dll/manifest.json')
}),
// 将某个文件打包输出去,并在html中自动引入该资源
//将生成的dll文件加入到index.html中
new AddAssetHtmlWebpackPlugin({
filepath: resolve(__dirname,'dll/jquery.js')
})
],
// 模式
// mode: "development",
mode: "production",
}
性能优化总结
开发环境性能优化
-
优化打包构建速度
- HMR 开启HMR功能
-
优化代码调试
- source-map 配置 devtool: ‘source-map’
生产环境性能优化
-
优化打包构建速度
-
oneOf 默认情况下,假设设置了多个 loader,每一个文件都得通过这多个 loader处 理(过一遍),浪费性能,使用 oneOf 找到了就能直接用,提升性能。
-
babel缓存 当一个 js 文件发生变化时,其它 js 资源不用变
-
多进程打包 开启多进程打包,主要处理js文件(babel-loader干的活久),
进程启动大 概为600ms,只有工作消耗时间比较长,才需要多进程打包, 提升打包速度
-
externals
-
dll 是将第三方库打包成多个bundle,从而进行速度优化
-
-
优化代码运行的性能
- 缓存(hash-chunkhash-contenthash)
- tree shaking
- code split 将第三方库都打包成一个bundle,这样体积过大,会造成打包速度慢
- 懒加载/预加载
- pwa