本文承接上一篇 深入浅出webpack -- loader和plugin原理及区别
主要从下面几个点入手优化:
项目本身:
webpack层面:
一般vue打包后的文件中会有三个常见的js文件,app.js业务代码 vendor.js第三方代码,manifest.js,webpack的代码,拆分的规范一般是:
对于单页面,主要是拆分,减少体积,把需要异步加载的改成异步加载,主要就是做一个异步的拆分。
在webpack3中 需要用下面的插件来进行代码分割
new webpack.optimize.CommonsChunksPlugin({
name:'vendor',
minChunks:'infinity'
}),
new webpack.optimize.CommonsChunksPlugin({
name:'manifest',
minChunks:'infinity'
}),
new webpack.optimize.CommonsChunksPlugin({
name:'app.js',
minChunks:2
}),
webpack4 配置一个属性
optimization:{
minimize:true, // 压缩代码,减少体积
splitChunks:{
name:true,
chunks:"all", // 'initial' 只对入口文件进行公共模块分析, 'all' 对所有文件进行模块分析 'async'
minSize: 30000, // 默认30000, 大于30kb的文件进行提取
cacheGroups:{
mode1:{ // 自定义要提取的模块
test:/mode1/,
},
vendor: { // 提取node_modules中文件 都喜欢把第三方依赖都打包在一起
test: /([\\/]node_modules[\\/])/,
name: "vendor"
}
}
},
runtimeChunk:true // 把webpack运行代码的提取出来
},
运行webpack
打包后的文件也都被引入到了index.html中。
(1)chunks
:分割代码的模式异步代码指的是异步引入的代码模块单独打包成一个或多个文件,下面是异步引入的例子:
//异步加载模块
function getComponent () {
return import(/* webpackChunkName:"lodash" */ 'lodash').then(({ default: _ }) => {
var element = document.createElement('div')
element.innerHTML = _.join(['Dell', ' ', 'Lee', '-'])
return element
})
}
getComponent().then(el => {
document.body.appendChild(el)
})
//同步加载模块
import _ from 'lodash' //第三方库
import test from './test.js' //业务代码
import jquery from 'jquery' //第三方库
console.log(test.name)
var element = document.createElement('div')
element.innerHTML = _.join(['Dell', ' ', 'Lee', '-'])
document.body.appendChild(element)
console.log(jquery('div'))
minSize
指的是引入的模块的最小值 maxSize
指的是引入的模块的最大值,当引入的模块大小大于最大值时,weback会尝试将这个模块以最大值为准分割成多个模块,前提是这个模块可以分割,比如lodash的提交大于50KB,那么设置maxSize:5000时,依然打包出一个文件来,故此属性一般不用
当值为2时,代表只引用了一次的模块不做分割打包处理
当需要分割的模块同步引入个数超出限时时,webpack只会分割限制值内的模块,其它的将不做处理
首次加载引入模块可分割的最大值
缓存组名称和生成文件名称之间的连接字符串
设置为true时,缓存组里面的filename生效,覆盖默认命名方式
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
new CleanWebpackPlugin(),
清除之前的dist
webpack3的压缩
new webpack.optimize.UglifyJsPlugin(),
对于多页面,我们要做的就是提取公共依赖,把几个页面中都用到的依赖打包到一个文件中。
有时候我们并不希望业务代码中混入了第三方代码,或者webpack 的代码。就需要把这些不希望出现在业务中的代码拆分成单独的文件。
多个html文件,多个入口
var extractTextCss=require('extract-text-webpack-plugin');
var htmlWebpackPlugin=require('html-webpack-plugin');
module.exports= {
mode:'production',
entry:{
app:"./src/app.js",
app2: "./src/app2.js"
},
output:{
path:__dirname+"/dist",
filename:"./[name].bundle.js",
},
module:{
rules: [
{
test:/\.css$/,
use:extractTextCss.extract({
fallback:{
loader:'style-loader',
options:{
//insertInto:"#mydiv",
//transform:"./transform.js"
}
},
use:[
{
loader:'css-loader',
options:{
/*modules:{
localIdentName:'[path][name]_[local]_[hash:4]'
} */
}
},
]
})
},
{
test:/\.(png|jpg|jgeg|gif)$/,
use:[
{
loader:'url-loader',
options:{
//默认是[hash].[ext]
name:'[name].[hash:4].[ext]',
outputPath:"assets/img",
publicPath:"assets/img",
limit:5000
}
},
{
loader:'img-loader',
options:{
plugins:[
require('imagemin-pngquant')({
speed:2 //1-11
}),
require('imagemin-mozjpeg')({
quality:80 //1-100
}),
require('imagemin-gifsicle')({
optimizationLevel:1 //1,2,3
})
]
}
},
]
},
{
test:/\.html$/,
use:{
loader:'html-loader',
options:{
attrs:["img:data-src"]
}
}
}
]
},
plugins:[
new extractTextCss({
filename:'[name].min.css'
}),
new htmlWebpackPlugin({
filename:"index.html",
template:"./src/index.html",
chunks:['app']
}),
new htmlWebpackPlugin({
filename:"index1.html",
template:"./src/index1.html",
chunks:['app2']
}),
]
}
index.html 和 index1.html内容一样
项目目录结构
打包后
配置代码拆分:(下面代码中的Setting可以理解为一个存储当前常量,环境信息的对象)
// 代码拆分
optimization: {
minimize: Setting.NODE_ENV === 'production', // This is true by default in production mode.
runtimeChunk: {
name: 'manifest',
}, // 把webpack运行代码的提取出来
splitChunks: {
chunks: 'initial',
minChunks: 1,
minSize: 30000,
maxAsyncRequests: 5,
maxInitialRequests: 3,
name: false,
cacheGroups: {
commons: {
test: /common/, // 把公共代码提取出来
name: Setting.common,
},
vendor: { // 提取node_modules中文件 都喜欢把第三方依赖都打包在一起
test: /([\\/]node_modules[\\/])/,
name: 'vendor',
},
},
},
},
打包后:
看index.html知道 只引入了app2.js文件。
还需要进行下面的配置,才可以把所有文件都引入:
config.plugins.push(new Html({
inject: true,
title,
env: Setting.NODE_ENV,
template: 'public/index.html',
filename: `${k}.html`,
chunks: [k, Setting.common, 'vendor', 'manifest'],
minify: Setting.NODE_ENV === 'production' || Setting.NODE_ENV === 'analyze' ? {
removeComments: true,
} : false,
}));
两个html文件中都引入了webapck代码,和第三方依赖
下面两种方式可以获得可视化的打包结果分析
然后将输出的json文件上传到如下网站进行分析
http://webpack.github.io/analyse/
选择stats.json文件可以看到详细的信息,生成了可视化的页面,然后打开 Modules页面,可以分析每一个文件的打包时间和大小
npm i webpack-bundle-analyzer --save
webpack引入插件
const wba = require('webpack-bundle-analyzer').BundleAnalyzerPlugin
new wba()
可以用这样的方式分析每个模块的大小
介绍一种提取css文件的方式, 比较支持webpack4
npm install mini-css-extract-plugin
const miniCssExtractPlugin = require('mini-css-extract-plugin');
new extractTextCss({ // 不支持hash命名
filename:'[name].min.css'
}),
new miniCssExtractPlugin({ // 支持hash
filename: '[name].[hash].css'
}),
style-loader换成 miniCssExtractPlugin.loader
可以把css文件也加上hash。
为什么为We b项目构建接入动态链接库的思想后,会大大提升构建 速度呢?原因在于,包含大量复用模块的动态链接库只需被编译一次, 在之后的构建过程中被动态链接库包含的模块将不会重新编译,而是直 接使用动态链接库中的代码。由于动态链接库中大多数包含的是常用的 第三方模块,例如 react、react-dom,所以只要不升级这些模块的版 本,动态链接库就不用重新编译。
Webpack已经内置了对动态链接库的支持,需要通过以下两个内置的插件接入。
webpack.dll.js
const webpack=require('webpack');
module.exports={
entry:{
jquery:["jquery"],
loadsh:["loadsh"]
},
output:{
path:__dirname+"/src/dll",
filename:"./[name].js",
//引用名
library:'[name]'
},
plugins:[
new webpack.DllPlugin({
path:__dirname+"/src/dll/[name].json",
name:"[name]"
})
]
}
注意这里的output.library属性要与DllPlugin中的name属性保持一致。dll打包后的文件需要单独引入html,因为不会再被打包进主文件内,也相当于做了代码拆分。
webpack --config webpack.dll.js现把第三方依赖打包好,src下面多了一个dll文件夹,这就是构建出的动态链接库文件。
如果需要压缩 可以增加配置,例子:
const webpack = require('webpack');
const TerserPlugin = require('terser-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
module.exports = {
mode: "development",
entry:{
vue: ["vue"],
},
output: {
path: __dirname + "/src/dll/",
filename: '[name].dll.js',
library: 'dll_[name]'
},
optimization: {
minimize: true,
// 压缩js
minimizer: [
new TerserPlugin({
parallel: true, // 启动多进程压缩 官方建议
})
]
},
plugins:[
new CleanWebpackPlugin(), // 清空得是output中得path
new webpack.DllPlugin({
path: __dirname+'/src/dll/[name].dll.json',
name: 'dll_[name]',
})
]
}
entry: ['vue-router', 'vue/dist/vue.esm.js'] vue项目中处理
json是给到webpack的
webpack.config.js 增加两个插件,这样每次打包的时候jquery和loadsh就不用再打包了,直接用打包好的。大大提升了打包速度。
new webpack.DllReferencePlugin({
manifest:require('./src/dll/jquery.json')
}),
new webpack.DllReferencePlugin({
manifest:require('./src/dll/loadsh.json')
})
并且用下面的插件,把dll/*.js文件插入html。
// 将某个文件打包输出去,并在html中自动引入该资源
new AddAssetHtmlPlugin([{
outputPath: "js/",
filepath: path.resolve(__dirname,'src/dll/vue.dll.js') // 文件路径
}])
由于有大量文件需要解析和处理,所以构建是文件读写和计算密集 型的操作,特别是当文件数量变多后,Webpack构建慢的问题会显得更 为严重。运行在Node.js之上的Webpack是单线程模型的,也就是说 Webpack需要一个一个地处理任务,不能同时处理多个任务。
文件读写和计算操作是无法避免的,那能不能让 Webpack 在同一 时刻处理多个任务,发挥多核CPU电脑的功能,以提升构建速度呢?
HappyPack就能让Webpack 做到这一点,它将任务分解给多个子进程去并发执行,子进程处理完后 再将结果发送给主进程。
由于 JavaScript 是单线程模型,所以要想发挥多核 CPU 的功能,就 只能通过多进程实现,而无法通过多线程实现。
//建议当文件较多的时候再使用这个,如果只有一两个反而会拖慢。
const HappyPack=require('happypack');
const os=require('os'); // 拿到操作系统
const happyThreadPool=HappyPack.ThreadPool({size:os.cpus().length}) // 新建进程池
{
test: /\.js$/,
loader: 'happypack/loader?id=happyBabel', // 对js的处理使用happypack
include: [resolve('src')]
},
new HappyPack({
id:'happyBabel',
loaders:[
{
loader:'babel-loader?cacheDirectory=true' // 代替babel-loader
}
],
threadPool:happyThreadPool,
verbose:true
}),
主要原理是充分利用浏览器的缓存机制,提高首页渲染速度。
使用hash
output:{
path:__dirname+"/dist",
filename:"./[name].[hash].js",
},
每一个文件的hash都是一样的。
改变一下app.js文件,把ma和mb文件换一个循序
import lod from "loadsh";
//import './css/test1.css';
import mb from "./moduleb.js";
import ma from "./modulea.js";
a(988989893232);
/*require.ensure(["./moduleb"],function(){
var ma=require('./modulea.js');
})*/
console.log(22);
webpack 打包
只改变了一个文件,导致多个文件的hash都发生了改变。
把hash换成chunkhash
output:{
path:__dirname+"/dist",
filename:"./[name].[chunkhash].js",
},
webapck 打包发现每一个文件的hash 都不一样。
import lod from "loadsh";
//import './css/test1.css';
import ma from "./modulea.js";
import mb from "./moduleb.js";
a(988989893232);
/*require.ensure(["./moduleb"],function(){
var ma=require('./modulea.js');
})*/
console.log(22);
再把循序换一下,webpack打包,发现还是所有文件的hash都改变了
增加两个插件
new webpack.NamedChunksPlugin(),
new webpack.NamedModulesPlugin(),
打包发现 chunks不一样了
再改变一下app.js模块引入循序,打包发现只有改变的文件的hash改变了,其它的还保持原来的hash,这样上线发布以后,那些没有改变的文件就可以继续使用浏览器的缓存。
resolve.modules的默认值是['node_modules'],含义是先去当前目录 的./node_modules 目录下去找我们想找的模块,如果没找到,就去上一 级目录../node_modules中找,再没有就去../../node_modules中找,以此类 推,这和Node.js的模块寻找机制很相似。
当安装的第三方模块都放在项目根目录的./node_modules 目录下 时,就没有必要按照默认的方式去一层层地寻找,可以指明存放第三方 模块的绝对路径,以减少寻找,配置如下:
resolve: {
...
modules: [path.resolve(__dirname, 'node_modules')],
},
在实战项目中经常会依赖一些庞大的第三方模块,以 React库为 例,安装到 node_modules目录下的React库的目录结构如下:
可以看到在发布出去的React库中包含两套代码。
在默认情况下,Webpack会从入口文件./node_modules/react/react.js 开始递归解析和处理依赖的几十个文件,这会是一个很耗时的操作。通 过配置resolve.alias,可以让Webpack在处理React库时,直接使用单独、 完整的react.min.js文件,从而跳过耗时的递归解析操作。
resolve: {
extensions: ['.js', '.jsx'],
alias: {
'@': path.resolve(__dirname, 'src'),
react: path.resolve(__dirname, './node_modules/react/dist/react.min.js'),
},
modules: [path.resolve(__dirname, 'node_modules')],
},
此处具体项目还需要看具体路径,引入的需要是react生产环境的压缩代码。
除了 React库,大多数库被发布到 Npm仓库中时都会包含打包好的 完整文件,对于这些库,也可以对它们配置alias。
这里注意,要这样引入:
react: isDev ? path.resolve(__dirname, './node_modules/react/cjs/react.development.js') : path.resolve(__dirname, './node_modules/react/cjs/react.production.min.js'),
开发环境引入开发环境的react,不然页面会不显示。
但是,对某些库使用本优化方法后,会影响到后面要讲的使用 Tree-Sharking 去除无效代码的优化,因为打包好的完整文件中有部分代 码在我们的项目中可能永远用不上。一般对整体性比较强的库采用本方 法优化,因为完整文件中的代码是一个整体,每一行都是不可或缺的。 但是对于一些工具类的库如 lodash,我们的项目中可能只用到 了其中几个工具函数,就不能使用本方法去优化了,因为这会导致在我们的输出代码中包含很多永远不会被执行的代码。
Tree-Sharking是一种按需进入的技术,只会引入我们用到的函数。
resolve: {
extensions: ['.js', '.json'],
}
当遇到 require('./data')这样的导入语句时,Webpack 会先去寻找./data.js文件,如果该文件不存在,就去寻找./data.json文 件,如果还是找不到就报错。
如果这个列表越长,或者正确的后缀越往后,就会造成尝试的次数 越多,所以resolve.extensions 的配置也会影响到构建的性能。在配置 resolve.extensions时需要遵守以下几点,以做到尽可能地优化构建性 能。
module.noParse配置项可以让Webpack忽略对部 分没采用模块化的文件的递归解析处理,这样做的好处是能提高构建性 能。原因是一些库如 jQuery、ChartJS庞大又没有采用模块化标准,让 Webpack解析这些文件既耗时又没有意义。
在前面讲解优化 resolve.alias 配置时讲到,单独、完整的 react.min.js 文件没有采用模块化,让我们通过配置module.noParse忽略 对react.min.js文件的递归解析处理,相关的Webpack配置如下:
注意,被忽略掉的文件里不应该包含import、require、define等模块 化语句,不然会导致在构建出的代码中包含无法在浏览器环境下执行的 模块化语句。
以上就是所有和缩小文件搜索范围相关的构建性能优化方面的内容 了,在根据自己项目的需要按照以上方法改造后,构建速度一定会有所 提升。
module: {
noParse: [/react\.min\.js$/],
...
}
当Webpack有多个JavaScript文件需要输出和 压缩时,原本会使用UglifyJS去一个一个压缩再输出,但是 ParallelUglifyPlugin会开启多个子进程,将对多个文件的压缩工作分配 给多个子进程去完成,每个子进程其实还是通过 UglifyJS 去压缩代码, 但是变成了并行执行。所以ParallelUglifyPlugin能更快地完成对多个文 件的压缩工作。
cnpm i webpack-parallel-uglify-plugin --D
plugins: [
...
new ParallelUglifyJsPlugin({
cacheDir: '.cache/', // 开启缓存
uglifyJS: {
output: {
// 最紧凑的输出
beautify: false,
// 删除注释
comments: false,
},
warnings: false,
compress: {
// 在uglifyJS时删除么有用到的代码时不输出警告
// 删除console
drop_console: true,
// 内嵌已经定义但是只用到一次的变量
collapse_vars: true,
// 提取出出现多次但是没有定义成变量去引用的静态值
reduce_vars: true,
},
},
}),
]
cnpm run build 会发现打包速度快了很多
优化后
在通过new ParallelUglifyPlugin()实例化时,支持以下参数。
其中的test、include、exclude与配置Loader时的思想和用法一样。
UglifyES(https://github.com/mishoo/UglifyJS2/tree/harmony)是 UglifyJS的变种,专门用于压缩ES6代码,它们都出自同一个项目,并 且不能同时使用。
UglifyES一般用于为比较新的 JavaScript运行环境压缩代码,例如用 于 ReactNative 的代码运行在兼容性较好的 JavaScriptCore 引擎中,为了 得到更好的性能和尺寸,可采用UglifyES压缩。
ParallelUglifyPlugin同时内置了UglifyJS和UglifyES,也就是说 ParallelUglifyPlugin支持并行压缩ES6代码。
安装成功后重新执行构建,会发现速度变快了许多。如果设置cacheDir 开启缓存,则在之后的构建中速度会更快。
文件监听是在发现源码文件发生变化时,自动重新构建出新的输出 文件。
Webpack官方提供了两大模块,一个是核心的 webpack(https://www.npmjs.com/package/webpack),webpack-dev-server。而文件监听功能是Webpack提供的。
Externals用来告诉在Webpack要构建的代码中使用了哪些不用被打 包的模块,也就是说这些模板是外部环境提供的,Webpack在打包时可 以忽略它们。
假设:我们开发了一个自己的库,里面引用了lodash这个包,经过webpack打包的时候,发现如果把这个lodash包打入进去,打包文件就会非常大。那么我们就可以externals的方式引入。也就是说,自己的库本身不打包这个lodash,需要用户环境提供。
import _ from 'lodash';
配置externals
externals: {
"lodash": {
commonjs: "lodash",//如果我们的库运行在Node.js环境中,import _ from 'lodash'等价于const _ = require('lodash')
commonjs2: "lodash",//同上
amd: "lodash",//如果我们的库使用require.js等加载,等价于 define(["lodash"], factory);
root: "_"//如果我们的库在浏览器中使用,需要提供一个全局的变量‘_’,等价于 var _ = (window._) or (_);
}
}
有些JavaScript运行环境可能内置了一些全局变量或者模块,例如在 我们的HTML HEAD标签里通过以下代码引入jQuery:
这时,全局变量jQuery就会被注入网页的JavaScript运行环境里。
如果想在使用模块化的源代码里导入和使用jQuery,则可能需要这 样:
import $ from 'jQuery'
构建后我们会发现输出的Chunk里包含的jQuery库的内容,这导致 jQuery库出现了两次,浪费加载流量,最好是Chunk里不会包含jQuery 库的内容。
Externals配置项就是用于解决这个问题的。
通过externals可以告诉Webpack在JavaScript运行环境中已经内置了 哪些全局变量,不用将这些全局变量打包到代码中而是直接使用它们。 要解决以上问题,可以这样配置externals:
module.export = {
externals: {
// 把导入语句里的 jquery 替换成运行环境里的全局变量 jQuery
jquery: 'jQuery',
react: 'React'
}
}
externals:{
react:'commonjs2 react',
jquery:'commonjs2 jquery'
}
1.如果需要requirejs等符合AMD规范的环境中加载,那就要添加amd
externals:{
react:'amd React',
jquery:'amd jQuery'
}
2.如果要在浏览器中运行,那么不用添加什么前缀,默认设置就是global。
externals:{
react:'React',
jquery:'jQuery'
}
也可以这样
externals:["React","jQuery"]
这种方式配置下,就是配置你所引用你的库暴露出的全局变量。上面两种模式下或者说,如果你想运行代码在浏览器中,你所引用的包,必须暴露出一个全局变量。如果没有,这种方式不适合在浏览器下使用 。
externals
和libraryTarget
的关系把模块中异步加载的组件都打包到一起
了解更多,移步webpack专题分类专栏:https://blog.csdn.net/qq_41831345/category_9640180.html