Webpack性能优化系列之缓存 (详解如何做babel缓存和文件资源缓存)
本系列的主题是 Webpack,每期讲解一个技术要点。如果你还不了解各系列内容,文末点击查看全部文章,点我跳转到文末。
如果觉得本系列不错,欢迎 Star,你的支持是我创作分享的最大动力。
缓存
缓存我们会从两个点出发:一个是对babel进行缓存,另一个是对整体的文件资源进行缓存
babel缓存
日常开发中,我们永远是js代码最多,像结构和样式要么少要么很少,即使多也没有办法做更多的处理。
为什么要对babel进行缓存呢?因为babel要对我们写的js代码做编译处理,编译成一种浏览器能识别的语法,即所谓的js兼容性处理。在编译过程中,假设有100个js模块,只改动其中1个js模块,不可能把全部的模块都重新编译一遍,其他99个应该是不变的,这一点跟前面文章讲过的HMR功能一样:一个模块变,只变这一个模块,其他模块不变。而生产环境下又不能使用HMR功能,因为HMR是基于devServer,生产环境是不需要devServer的。
开启babel缓存只需要在babel-loader的配置里加一个参数即可:cacheDirectory: true
{
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
}
},
开启babel缓存,之后的 webpack 构建,将会尝试读取缓存,来避免在每次执行时,可能产生的、高性能消耗的 Babel 重新编译过程(recompilation process)。
文件资源缓存
写个例子来演示一下文件资源缓存
为了演示缓存,我们需要写一个服务器代码
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);

关于上述服务器代码疑问参考:Express细节探究(1)——app.use(express.static) - ADAM亚当 - 博客园
node server.js命令, 启动服务器
注意,上面的服务器设置了maxAge,即开启Cache-Control
通过network查看资源,刷新页面,可以看到资源来自cache,即来自缓存

查看请求可以发现请求头设置了Cache-Control,max-age=3600,即有效期为3600s,一个小时,意思就是这个资源会被强制缓存一个小时

这个缓存会带来新的问题:
假如我们修改代码,修改一下js代码
index.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));// 修改后
再次构建,打开浏览器刷新,会发现结果没有变化

修改background-color: deeppink;,原来为pink,再次构建,发现样式同样的有没有变化

这是为什么呢?
这是因为当前资源在强制缓存期间,它是不会访问服务器的,直接读取本地缓存
这就带来了一个问题,假使我们的资源在强缓存期间出现了严重的bug,开发人员需要紧急修复,但因为资源被强制缓存,就算修复了,也会因为还在强缓存期间而无效。
我们可以通过修改资源名称来解决这个问题
有三种方法可以修改资源名称:
hash: 每次wepack构建时会生成一个唯一的hash值。
问题: 因为js和css同时使用一个hash值。
如果重新打包,会导致所有缓存失效。(可能我却只改动一个文件)
chunkhash:根据chunk生成的hash值。如果打包来源于同一个chunk,那么hash值就一样
问题: js和css的hash值还是一样的
因为css是在js中被引入的,所以同属于一个chunk
contenthash: 根据文件的内容生成hash值。不同文件hash值一定不一样
这三种方法,可以发现只有contenthash是没有副作用的,所以最后的结论就是使用contenthash来修改资源名称
使用方式:
入口文件
entry: './src/js/index.js',
output: {
filename: 'js/built.[contenthash:10].js',
path: resolve(__dirname, 'build')
},
样式文件
new MiniCssExtractPlugin({
filename: 'css/built.[contenthash:10].css'
}),
完整的配置:
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/js/index.js',
output: {
filename: 'js/built.[contenthash:10].js',
path: resolve(__dirname, 'build')
},
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
}
},
{
test: /\.(jpg|png|gif)/,
loader: 'url-loader',
options: {
limit: 8 * 1024,
name: '[hash:10].[ext]',
outputPath: 'imgs',
esModule: false
}
},
{
test: /\.html$/,
loader: 'html-loader'
},
{
exclude: /\.(js|css|less|html|jpg|png|gif)/,
loader: 'file-loader',
options: {
outputPath: 'media'
}
}
]
}
]
},
plugins: [
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'
};
参考
博文系列目录
- JavaScript 深入系列
- JavaScript 专题系列
- JavaScript 基础系列
- 网络系列
- 浏览器系列
- Webpack 系列
- Vue 系列
- 性能优化与网络安全系列
- HTML 应知应会系列
- CSS 应知应会系列
交流
各系列文章汇总:https://github.com/yuanyuanbyte/Blog
我是圆圆,一名深耕于前端开发的攻城狮。
