天下没有白吃的午餐。
前言
有一位波斯国的国王,他勤政爱民,善良聪明,全国的人民在他的领导下,过着衣食无忧、安居乐业的生活。
可是随着国王一天天老去,他开始为一件事担心,就是怕自己死了之后,人民不能再像现在一样过着幸福的生活,怎么办呢?一天,他把全国的有识之士都召集了起来,让他们找寻出一个能确保人民生活幸福的永世法则。
3个月后,学者们呈给国王3本6寸厚的帛书,并且说:“这3本书囊括了天下的知识,只要人民能够把它读完,就能世代无忧无虑地生活下去了”。
国王看了看那3本厚厚的书,很不以为然,因为他知道,人民是不会花那么多的时间看完这些书的。他让这些学者继续钻研。
2个月后,学者们呈给国王一张纸,国王在看了这张纸之后,非常满意,说:“很好,有这几个字我死也瞑目了,只要我的子民能按照纸条上所写的那样去做,相信他们一定能过上幸福安康的生活。”
请问纸上写的是什么?
以上内容是一个简单的鸡汤故事,与本篇内容无关,却蕴含人生哲理。
今天的主题是关于webpack的性能优化,以及即将发版的webpack5,有什么新的更新。
webpack性能优化
之前在拉钩教育买过一节课《webpack原理与实践》。此课程中以独到的见解来阐述webpack的工作原理和运行机制,并传递一种思想就是「 带着问题去看源码 」,明确“查阅”而不是“死扣”。
文中提到一个生产环境下的优化插件DefinePlugin,DefinePlugin 是用来为我们代码中注入全局成员的。
// ./webpack.config.js
const webpack = require('webpack')
module.exports = {
// ... 其他配置
plugins: [
new webpack.DefinePlugin({
// 值要求的是一个代码片段
API_BASE_URL: JSON.stringify('https://api.example.com')
})
]
}
文中会讲解一些小技巧,以及一些插件常见的坑。
比较有意思的是我提了一个问题:「 程序是如何识别rc类型文件的,webpack是如何做到.webpackrc.js来替换webpack.config.js的?」
老师的回复是一个库:「 cosmiconfig 」这个库会自动搜索配置文件。
除了拉钩教育还在开课吧买过一个小课程《Web前端架构师高级进阶必备技能》,此课程中讲的东西比较多,有一节课是「 webpack原理剖析 」,是从源码层面讲解webpack如何打包编译,以及手动实现一个简单的webpack。
除了以上,还去B站,找到了一份条理比较清晰的《weebpack实战课程》。此课程是出自尚硅谷,从使用层面出发,讲解webpack能解决什么问题,在项目中如何优化,如何配置。
本文以下的大部分内容都是以上课程的笔记,如果说的不明白,可以自行去查阅实战,也可留言探讨。
关于webpack的性能优化,从两方面入手:开发环境和生产环境。
开发环境性能优化
在开发环境下,程序员会着重关注两点,一个是实时编译,另一个调试方便。
提到热更新就会想到HMR功能,开启webpack配置中的devServer的hot属性即可开启HMR功能,HMR会极大的提高构建速度,并且一个模块发生变化,只会重新打包一个模块,而不是打包所有模块。
相对于样式文件,style-loader已经内部实现了对HMR的支持。
相对于HTML文件,改变不会自动更新(默认不支持HMR),需要我们做一些配置。(引文html很少变化,一般情况我们不作处理)
// 修改entry入口,将html文件引入
module.exports = {
entry: ["./src/index.js", "./src/index.html"],
// ... 其他配置
}
相对于JS文件,每次修改会全局刷新,我们想要的效果是每次修改那个模块,那个模块更新,不需要浏览器刷新。如下例子
//a.js
function add(x,y){
console.log("add加载了")
return x+y;
}
export default add
//index.js
import add from "./a.js"
console.log("index.js被加载了")
if(module.hot){
module.hot.accept("./a.js",function(){
//监听a.js的变化,一旦发生变化,其他模块不会重新打包构建
add()
})
}
通过module.hot来判断是否开启HMR功能,通过accept监听某个模块是否变化。
source-map 是一种提供源代码构建后代码映射的技术,如果构建后的代码出现问题,可以通过映射追踪源代码的错误位置。
source-map分两种类型:内联和外部。内联就是直接会在打包的js内部生成,而且内联的速度比较快。而外部就是在外部单独生成一个souce-map文件。
//[inline-|hidden-|eval-][nosources-][cheap-[module-]]source-map
//source-map:外部
//错误代码的准确位置
//inline-source-map:内联 只生成一个内联的source-map
//错误代码的准确位置
//hidden-source-map:外部 在外部生成一个source-map文件
//错误代码的错误原因,不能追踪到源代码的错误位置,只能提示构建后的代码错误位置。
//eval-source-map:内联 每一个文件对应的source-map都在eval里
//错误代码的准确位置
//nosource-source-map:外部
//错误代码的准确位置,没有任何源代码信息
//cheap-source-map
//错误代码的准确位置,只精确到行
//cheap-module-source-map
//module会将loader的source-map加入
//开发环境:速度快,调试友好
//速度 eval>inline>cheap>...
//eval-cheap-source-map会变得更快
//调试更友好的是
//source-map>cheap-module-source-map>cheap-source-map
//折中化方案(脚手架默认配置方案)
//eval-source-map
//生产环境:源代码要不要隐藏?
//内联会让体积非常大,所以一般不考虑内联
//如果需要隐藏源代码可以考虑
//nosource-source-map 全部隐藏
//hidden-source-map 只隐藏源代码
生产环境性能优化
正常情况下,一个文件只能被一个loader处理。当一个文件要被多个loader处理,那么一定要指定loader执行的先后顺序。
oneOf是让一个loader处理一类文件,会让loader的性能更好。需要注意的是不能有两个配置处理同一种文件,比如eslint-loader,所以我们要把eslint-loader放外面。
module: {
rules: [
{
test: /\.js$/,
exclude:/node_modules/,
enforce:"pre",
loader:"eslint-loader",
options:{
fix:true,
}
},
{
oneOf: [
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
{
test: /\.(png|jpg|gif|svg)$/,
use: ["file-loader"],
},
//...
]
}
]
}
除了oneOf这种优化构建速度的方案,我们还可以从缓存入手。babel-loader提供了一个cacheDirectory的属性可以帮我们实现babel缓存,他会在第二次构建的时候,读取之前的缓存。
module: {
rules: [
{
test: /\.js$/,
exclude:/node_modules/,
loader:"babel-loader",
options:{
cacheDirection:true,
}
}
//...
]
}
除了babel缓存还有一种缓存是文件资源缓存,通过hash来实现。
在webpack中3种hash,每次webpack构建会生成一个唯一的hash值。
//因为css和js同时使用一个hash值
//如果重新打包,会导致所有缓存失效。
output: {
filename: "bundle.[hash:10].js",
path: path.resolve(__dirname, "dist"),
},
plugins: [
new MiniCssExtractPlugin({
filename:"css/main.[hash:10].css"
}),
new HtmlWebpackPlugin({
template: "./index.html",
}),
],
chunkhash是根据chunk生成的hash值,如果打包来源于同一个chunk,那么hash值就一样。
还有一种hash是contenthash,它是根据文件内容生成hash值。不同文件hash值一定不一样。
所以我们常用contenthash来做文件资源缓存,它可以让代码在线上运行的时候,更好的利用缓存。
接下来我们聊一下 tree-shaking,tree-shaking是用来去除无用的代码。它的前提是你必须使用ES6模块化,环境为生产环境。举个例子,比如untils.js里有很多导出的方法,但是我们只用了其中的一个。那么untils.js中的其他没有用的代码将不会被webpack打包编译。
//untils.js
export const add=()=>{
//...
}
export const muilt=()=>{
//...
}
//index.js
import { add } from "../../untils"
//tree-shaking 就是通过import将没用使用过的代码去除掉,比如muilt方法
但是如果代码中引入css模块呢?css模块也会被tree-shaking处理掉吗?
这里需要我们去package.json中设置://...
//"sideEffects":false,//所有代码都没有副作用(都可以进行tree-shaking)
"sideEffects":["*.css"],//css等文件不会进行tree-shaking。
如果我们生产环境我们需要第三方依赖,我们想要单独打包,我们如何做呢?
这里会用到optimization:
//...
optimization:{
splitChunks:{
chunk:"all"
}
}
他会自动分析多入口chunk中,有没有公共的文件,如果有会打包成单独的一个chunk。
还有一种方法就是在import动态导入的时候添加,在路由中比较常见。
//...
component:()=>import(/*webpackChunkName: "text"*/ "./../views/text.vue")
import动态导入语法,能将此文件单独打包。
如果我们想按需加载,也就是当事件触发的时候,才加载所需文件,怎么设置?
这是典型的懒加载,在使用的时候加载。除了懒加载还有预加载,看起来效果和懒加载一样,但是其实已经偷偷加载了,当你点击的时候会从内存中获取。
//如果把import放在头部是正常加载,也是并行加载。
//把import语法放在异步的回掉中处理。
document.getElementById("btn").onclick=function(){
//懒加载
import(/*webpackChunkName:'test'*/"./test").then(
(res)=>{
//....
}
)
//预加载:会提前加载(空闲的时候偷偷加载,不是并行加载)
//等其他资源加载完毕的时候,偷偷加载,兼容行不是很好
import(/*webpackPreFetch:true */"./test").then(
(res)=>{
//....
}
)
}
如果项目比较大,js文件比较多,还有一种优化打包速度的方案就是
多进程打包,它是通过thread-loader来实现的。比如babel-loader需要把编译转换大量js。
{
test: /\.js$/,
exclude: /node_modules/,
use: [
{
//多进程打包
loader: "thread-loader",
options: {
workers: 2,
},
},
{
loader: "babel-loader",
options: {
cacheDirection:true,
}
}
]
}
在一些项目中,我们可能会通过CDN引入一些第三方的库或资源,我们不希望webpack打包编译的时候打包进来,怎么设置?
这个时候就会用到exterals,它的主要作用就是在运行编译的时候排除外部依赖。官网给了一个很贴切的例子:
//index.html
"https://code.jquery.com/jquery-3.1.0.js"
integrity="sha256-slogkvB1K3VOkzAI8QITxV3VzpOnkeNVsKvtkYLMjfk="
crossorigin="anonymous">
script>
//webpack.config.js
module.exports = {
//...
externals: {
jquery: 'jQuery'
}
};
//打包编译后仍然工作
import $ from 'jquery';
$('.my-element').animate(/* ... */);
exterals是完全不需要打包,但是如果我们不想用CDN,就想要本地打包,并且不想重复打包,怎么办?
这里就用到dll技术,这里需要分几步,第一步:对node_modules里的第三方库单独打包,首先我们创建一个webpack.dll.js(名字可以随意,这里用dll)文件。
//webpack.dll.js
const path = require("path");
const webpack = require("webpack");
module.exports = {
mode: "production",
entry: {
//打包输出的名字为键,值中的jquery为要打包的库
jquery: ["jquery"],
},
output: {
path: path.resolve(__dirname, "dll"),
filename: "[name].js",
//打包的库里面向外暴露出去的内容叫什么
library: "[name]_[hash]",
},
plugins: [
new webpack.DllPlugin({
//映射库暴露的内容名字
name: "[name]_[hash]",
path: path.resolve(__dirname, "dll/manifest.json"),
}),
],
};
通过webpack --config webpack.dll.js命令打包之后,会生成dll文件目录,其下有两个文件,一个是jquery,一个是manifest.json映射文件。
第二步就是告诉webpack,哪些库不需要打包,并把之前dll打包的库映射到项目中(使用时名字得变)。
第三步,借助add-asset-html-webpack-plugin插件引入到html中。
//webpack.config.js
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const webpack = require("webpack");
const AddAssetHtmlWebpackPlugin = require("add-asset-html-webpack-plugin");
module.exports = {
mode: "production",
entry: "./src/index.js",
output: {
path: path.resolve(__dirname, "dist"),
filename: "bundle.js",
},
plugins: [
new HtmlWebpackPlugin({
title: "Hello",
template: "src/index.html",
// minify: {
// //移除空格
// collapseWhitespace: true,
// //移除注释
// removeComments: true,
// },
// filename: "html/admin.html",
}),
//告诉webpack哪些库不需要打包,同时使用时名称也的变。
new webpack.DllReferencePlugin({
manifest: path.resolve(__dirname, "dll/manifest.json"),
}),
//将某个文件打包输出去,并在html中自动引入该资源
new AddAssetHtmlWebpackPlugin({
filepath: path.resolve(__dirname, "dll/jquery.js"),
}),
],
}
输出的Html:
<body>
<div id="title">Hello Worlddiv>
<p>say you hello-p>
<script src="jquery.js">script>
<script src="bundle.js">script>
body>
性能优化总结
以上性能优化,从两方面入手:开发环境和生产环境。
主要优化的点是代码构建速度,代码调试性能,代码运行效率等。
webpack5展望
webpack5还没有正式发布,但是提供了实验版。github关于webpack5的说明文档地址为:https://github.com/webpack/changelog-v5
文章底部原文链接即为github地址,可以点进去查看webpack5的详细说明文档。
我们也可以下载webpack5实验版,尝试一下新的更新。
npm i webpack@next webpack-cli -D
此版本重点关注以下内容:
通过持久化缓存提高构建性能。
通过更好的算法和默认值来改善长期缓存。
通过更好的tree-shaking(webpack4遗留问题)和代码生成来改善捆绑包的大小。
清除处于怪异状态的内部结构,同时在V4中引入功能而不引入破坏性的更改。
我们试图通过引入突破性的变化来为将来的特性做准备,允许我们尽可能长时间地使用v5。
webpack5会自动停止填充一些核心模块,比如自动删除Node.js Polyfills,而专注于与前端兼容的模块。
添加一些长期缓存的hash算法,在生产环境是默认启用的。
不再以id(0,1,2)命名,优化内部chunk命名规则。
tree-shaking功能更加强大,能够处理多个模块、嵌套模块之前的关系。也能处理对CommonJS的tree-shaking。
优化output,可以生成ES5和ES6/ES2015的代码。
优化代码分割SplitChunk,可以更精确的实现js,以及css的代码分割。
更多的内容可以移步github(点击下方原文链接查看)。