17项关于webpack的性能优化

10,467 阅读11分钟

本文主要优化两个方面:1.优化开发体验 2.优化输出质量

优化开发体验

  • 提升效率
  • 优化构建速度
  • 优化使⽤体验

优化输出质量

  • 优化要发布到线上的代码,减少⽤户能感知到的加载时间
  • 提升代码性能,性能好,执⾏就快

以下基于webpack4进行优化。

缩⼩⽂件范围 Loader

  • 优化loader配置
  • test、include、exclude三个配置项来缩⼩loader的处理范围
  • 推荐include
include: path.resolve(__dirname, "./src"),

优化前的构建时间:

Time: 2429ms

进行修改代码优化:

rules: [
      {
        test: /\.css$/,
        include: path.resolve(__dirname, "./src"),
        use: ["style-loader", "css-loader"],
      },
      {
        test: /\.less$/,
        include: path.resolve(__dirname, "./src"),
        use: [
          // "style-loader",
          MiniCssExtractPlugin.loader,
          {
            loader: "css-loader",
            options: {
              //css modules 开启
              modules: true,
            },
          },
          {
            loader: "postcss-loader",
          },
          "less-loader",
        ],
      },
      {
        test: /\.(png|jpe?g|gif)$/,
        include: path.resolve(__dirname, "./src"),
        use: {
          loader: "url-loader",
          options: {
            name: "[name]_[hash:6].[ext]",
            outputPath: "images/",
            //推荐使用url-loader 因为url-loader支持limit
            //推荐小体积的图片资源转成base64
            limit: 12 * 1024, //单位是字节 1024=1kb
          },
        },
      },
      {
        test: /\.js$/,
        include: path.resolve(__dirname, "./src"),
        exclude: /node_modules/,
        use: {
          loader: "babel-loader",
        },
      },
    ],

优化之后的构建时间:

Time: 2110ms

减少了319ms

优化resolve.modules

resolve.modules用于配置webpack去哪些目录下寻找第三方模块,默认是 ['node_modules']。寻找第三方,默认是在当前项目目录下的node_modules里面去找,如果没有找到,就会去上一级目录../node_modules找,再没有会去../../node_modules中找,以此类推,和Node.js的模块寻找机制很类似。

如果我们的第三⽅模块都安装在了项⽬根⽬录下,就可以直接指明这个路径。

module.exports={
 resolve:{
 modules: [path.resolve(__dirname, "./node_modules")]
 }
}

优化之后的构建时间:

Time: 1532ms

减少了578ms

优化resolve.alias

resolve.alias配置通过别名来将原导⼊路径映射成⼀个新的导⼊路径。拿react为例,我们引⼊的react库,⼀般存在两套代码:

  • cjs

    采⽤commonJS规范的模块化代码

  • umd

    已经打包好的完整代码,没有采⽤模块化,可以直接执⾏

默认情况下,webpack会从⼊⼝⽂件./node_modules/bin/react/index开始递归解析和处理依赖的⽂件。我们可以直接指定⽂件,避免这处的耗时。

  resolve: {
    //查找第三方优化
    modules: [path.resolve(__dirname, "./node_modules")],
    alias: {
      "@": path.join(__dirname, "./src"),
      react: path.resolve(__dirname, "./node_modules/react/umd/react.production.min.js"),
      "react-dom": path.resolve(__dirname, "./node_modules/react-dom/umd/react-dom.production.min.js")
    },
  },

优化之后的构建时间:

Time: 1362ms

减少了170ms

优化resolve.extensions

resolve.extensions在导⼊语句没带⽂件后缀时,webpack会⾃动带上后缀后,去尝试查找⽂件是否存在。

默认值:

extensions:['.js','.json','.jsx','.ts']
  • 后缀尝试列表尽量的⼩
  • 导⼊语句尽量的带上后缀。

如果想优化到极致的话,不建议用extensionx, 因为它会消耗一些性能。虽然它可以带来一些便利。

使⽤externals优化cdn静态资源

我们可以将⼀些JS⽂件存储在CDN上(减少Webpack打包出来的js体积),在index.html中通过标签引⼊,如:

<!DOCTYPE html>
<html lang="en">
<head>
 <meta charset="UTF-8">
 <meta name="viewport" content="width=device-width, initial-scale=1.0">
 <meta http-equiv="X-UA-Compatible" content="ie=edge">
 <title>Document</title>
</head>
<body>
 <div id="root">root</div>
 <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
</body>
</html>

我们希望在使⽤时,仍然可以通过import的⽅式去引⽤(如:import $ from 'jquery' ),并且希望webpack不会对其进⾏打包,此时就可以配置 externals 。

//webpack.config.js
module.exports = {
 //...
 externals: {
 //jquery通过script引⼊之后,全局中即有了 jQuery 变量
 'jquery': 'jQuery'
 }
}

使用静态资源路径publicPath(CDN)

CDN通过将资源部署到世界各地,使得⽤户可以就近访问资源,加快访问速度。要接⼊CDN,需要把网页的静态资源上传到CDN服务上,在访问这些资源时,使⽤CDN服务提供的URL。

##webpack.config.js
output:{
 publicPath: '//cdnURL.com', //指定存放JS⽂件的CDN地址
}

使用条件:

  • 公司得有cdn服务器地址
  • 确保静态资源⽂件的上传与否

借助MiniCssExtractPlugin完成抽离css

npm install mini-css-extract-plugin -D
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
 {
 test: /\.less$/,
 use: [
 // "style-loader", // 不再需要style-loader,⽤MiniCssExtractPlugin.loader代替
  MiniCssExtractPlugin.loader,
  "css-loader", // 编译css
  "postcss-loader",
  "less-loader" // 编译less
 ]
 },
plugins: [
  new MiniCssExtractPlugin({
   filename: "css/[name]_[contenthash:6].css",
   chunkFilename: "[id].css"
  })
 ]

压缩css

  • 借助optimize-css-assets-webpack-plugin
  • 借助cssnano

先安装:

npm install cssnano -D
npm i optimize-css-assets-webpack-plugin -D
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

new OptimizeCSSAssetsPlugin({
 cssProcessor: require("cssnano"), //引⼊cssnano配置压缩选项
 cssProcessorOptions: {
 discardComments: { removeAll: true }
 }
})

压缩HTML

借助html-webpack-plugin

new htmlWebpackPlugin({
   title: "京东商城",
   template: "./index.html",
   filename: "index.html",
   minify: {
     // 压缩HTML⽂件
     removeComments: true, // 移除HTML中的注释
     collapseWhitespace: true, // 删除空⽩符与换⾏符
     minifyCSS: true // 压缩内联css
  }
 }),

development vs Production模式区分打包

先安装

npm install webpack-merge -D

创建webapck.dev.js文件

const {merge} = require("webpack-merge")
const commonConfig = require("./webpack.common.js")
const devConfig = {
 ...
}
module.exports = merge(commonConfig,devConfig)

创建webpack.prod.js文件

const {merge} = require("webpack-merge")
const commonConfig = require("./webpack.common.js")
const prodConfig = {
 ...
}
module.exports = merge(commonConfig,prodConfig)

package.js

"scripts":{
 "dev":"webpack-dev-server --config ./webpack.dev.js",
 "build":"webpack --config  ./webpack.prod.js"
}

基于环境变量区分

借助cross-env

npm i cross-env -D

package⾥⾯配置命令脚本,传⼊参数

"test:build": "cross-env NODE_ENV=production webpack --config ./webpack.test.config.js",
"test:dev": "webpack-dev-server --config ./webpack.test.config.js"

在webpack.config.test.js⾥拿到参数进行判断

const baseConfig = require("./webpack.config.base.js");
const devConfig = require("./webpack.config.dev.js");
const prodConfig = require("./webpack.config.prod.js");
const {merge} = require("webpack-merge");

console.log(process.env.NODE_ENV)
module.exports = (process.env.NODE_ENV)=>{
 if(process.env.NODE_ENV && process.env.NODE_ENV.production){
 return merge(commonConfig,prodConfig)
 }else{
 return merge(commonConfig,devConfig)
 } }

tree Shaking

webpack2.x开始⽀持tree shaking概念,顾名思义,"摇树",就是清除无用css,js(Dead Code)

Dead Code⼀般具有以下⼏个特征:

  • 代码不会被执⾏,不可到达
  • 代码执⾏的结果不会被⽤到
  • 代码只会影响死变量(只写不读)
  • Js tree shaking只⽀持ES module的引⼊⽅式!!!!

Css tree shaking

npm i glob-all purify-css purifycss-webpack --save-dev
const PurifyCSS = require('purifycss-webpack')
const glob = require('glob-all')
plugins:[
 new PurifyCSS({
      paths: glob.sync([
        // 要做 CSS Tree Sharking 的路径文件
        path.resolve(__dirname, "./src/*.html"),// 同样需要对html文件进行tree shaking
        path.resolve(__dirname, "./src/*.js")
      ])
    }),
]

JS tree shaking

只⽀持import⽅式引⼊,不⽀持commonjs的⽅式引⼊

案例:增加expo.js文件

//expo.js
export const add = (a, b) => {
 return a + b;
};
export const minus = (a, b) => {
 return a - b;
};
//index.js
import { add } from "./expo";
add(1, 2);
//webpack.config.js
optimization: {
 usedExports: true // 哪些导出的模块被使⽤了,再做打包
}

只要mode是production就会⽣效,mode是develpoment的tree shaking是不⽣效的,因为webpack为了⽅便你的调试。

可以查看打包后的代码注释以辨别是否⽣效。

⽣产模式不需要配置,默认开启。

sideEffects 处理副作用

//package.json
"sideEffects":false //正常对所有模块进⾏tree shaking , 仅⽣产模式有效,需要配合usedExports

或者在数组⾥⾯排除不需要tree shaking的模块

"sideEffects":['*.css','@babel/polyfill']

代码分割 code Splitting

单⻚⾯应⽤spa:

打包完后,所有⻚⾯只⽣成了⼀个bundle.js。

  • 代码体积变⼤,不利于下载
  • 没有合理利⽤浏览器资源

多⻚⾯应⽤mpa:

如果多个⻚⾯引⼊了⼀些公共模块,那么可以把这些公共的模块抽离出来,单独打包。公共代码只需要下载⼀次就缓存起来了,避免了重复下载。

假如我们引⼊⼀个第三⽅的⼯具库,体积为1mb,⽽我们的业务逻辑代码也有1mb,那么打包出来的体积⼤⼩会在2mb,这就会导致问题:

  • 文件体积⼤,加载时间⻓。
  • 业务逻辑会变化,而第三⽅⼯具库不会,所以业务逻辑⼀变更,第三⽅⼯具库也要跟着变。

例如我们使用第三方库lodash:

import _ from "lodash";
console.log(_.join(['a','b','c','****']))

其实code Splitting概念与webpack并没有直接的关系,只不过webpack中提供了⼀种更加⽅便的⽅法供我们实现代码分割。

基于webpack.js.org/plugins/spl…

 optimization: {
   splitChunks: {
      chunks: "all", // 所有的 chunks 代码公共的部分分离出来成为⼀个单独的⽂件
    },
 },

打包后dist目录下多了一个文件vendors~main.js,这个文件就是分离出来的lodash image.png

splitChunks的其他可配置项:

optimization: {
splitChunks: {
      chunks: "async", //对同步 initial,异步 async,所有的模块有效 all
      minSize: 30000, //最⼩尺⼨,当模块⼤于30kb
      maxSize: 0, //对模块进⾏⼆次分割时使⽤,不推荐使⽤
      minChunks: 1, //打包⽣成的chunk⽂件最少有⼏个chunk引⽤了这个模块
      maxAsyncRequests: 5, //最⼤异步请求数,默认5
      maxInitialRequests: 3, //最⼤初始化请求书,⼊⼝⽂件同步请求,默认3
      automaticNameDelimiter: "-", //打包分割符号
      name: true, //打包后的名称,除了布尔值,还可以接收⼀个函数function
      cacheGroups: {
        //缓存组
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          name: "vendor", // 要缓存的 分隔出来的 chunk 名称
          priority: -10, //缓存组优先级 数字越⼤,优先级越⾼
        },
        other: {
          chunks: "initial", // 必须三选⼀: "initial" | "all" | "async"(默认就async)
          test: /react|lodash/, // 正则规则验证,如果符合就提取 chunk,
          name: "other",
          minSize: 30000,
          minChunks: 1,
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true, //可设置是否重⽤该chunk
        },
      },
    },
    }

我们可以配置缓存组

splitChunks: {
      chunks: "all", // 所有的 chunks 代码公共的部分分离出来成为⼀个单独的⽂件
      automaticNameDelimiter: '-',//打包分割符号
      cacheGroups: {
        lodash: {
          test:/lodash/,
          name: "lodash"
        },
        react: {
          test: /react|react-dom/,
          name: "react"
        }
      }
    },

打包之后

image.png

一般使⽤下⾯配置即可:

optimization:{
 //帮我们⾃动做代码分割
 splitChunks:{
  chunks:"all",//默认是⽀持异步,我们使⽤all
 } 
}

Scope Hoisting

作⽤域提升(Scope Hoisting)是指webpack通过ES6语法的静态分析,分析出模块之间的依赖关系,尽可能地把模块放到同⼀个函数中。下⾯通过代码示例来理解:

新建hello.js

export default 'Hello, Webpack';

新建index.js

import str from './hello.js';
console.log(str);

打包后,hello.js的内容和index.js会分开

image.png

我们通过配置optimization.concatenateModules=true开启Scope Hoisting

// webpack.config.js
module.exports = {
 optimization: {
 concatenateModules: true
 }
};

我们发现hello.js内容和index.js的内容合并在⼀起了! image.png

所以通过Scope Hoisting的功能可以让Webpack打包出来的代码⽂件更⼩、运⾏的更快。

使⽤⼯具量化

  • speed-measure-webpack-plugin:可以测量各个插件和 loader 所花费的时间:
npm i speed-measure-webpack-plugin -D
//webpack.config.js
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin");
const smp = new SpeedMeasurePlugin();
const config = {
 //...webpack配置
}
module.exports = smp.wrap(config);

打包后 image.png

  • webpack-bundle-analyzer:分析webpack打包后的模块依赖关系:
npm install webpack-bundle-analyzer -D
//webpack.config.js
const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
 //....
 plugins: [
 //...
 new BundleAnalyzerPlugin(),
 ]

启动webpack构建,会默认打开⼀个窗⼝

image.png

DllPlugin插件打包第三⽅类库 优化构建性能

Dll动态链接库,其实就是做缓存。

.dll⽂件称为动态链接库,在windows系统会经常看到.百度百科:baike.baidu.com/item/.dll/2…

项⽬中引⼊了很多第三⽅库,这些库在很⻓的⼀段时间内,基本不会更新,打包的时候分开打包来提升打包速度,⽽DllPlugin动态链接库插件,其原理就是把⽹⻚依赖的基础模块抽离出来打包到dll⽂件中, 当需要导⼊的模块存在于某个dll中时,这个模块不再被打包,⽽是去dll中获取。

  • 动态链接库只需要被编译⼀次。项⽬中⽤到的第三⽅模块,版本比较稳定,例如react,react-dom,只要没有升级的需求,就可以使用。

webpack已经内置了对动态链接库的⽀持:

  • DllPlugin:⽤于打包出⼀个个单独的动态链接库⽂件
  • DllReferencePlugin:⽤于在主要的配置⽂件中引⼊DllPlugin插件打包好的动态链接库⽂件

新建webpack.dll.config.js⽂件,打包基础模块。

我们在index.js中使⽤了第三⽅库react、react-dom,接下来,我们先对这两个库先进⾏打包。

const path = require("path");
const { DllPlugin } = require("webpack");

module.exports = {
    mode: "development",
    entry: {
        react: ["react", "react-dom"]
    },
    output: {
        path: path.resolve(__dirname, "./dll"),
        filename: "[name].dll.js",
        library: "react"
    },
    plugins: [
        new DllPlugin({
            // manifest.json⽂件的输出位置
            path: path.join(__dirname, "./dll","[name]-manifest.json"),
            // 定义打包的公共vendor⽂件对外暴露的函数名
            name: "react"
        })
    ]
}

在package.json中添加

"dev:dll": "webpack --config ./webpack.dll.config.js"

运⾏

npm run dev:dll

你会发现多了⼀个dll⽂件夹,⾥边有dll.js⽂件,这样我们就把我们的React这些已经单独打包了:

image.png

  • dll⽂件包含了⼤量模块的代码,这些模块被存放在⼀个数组⾥。⽤数组的索引号为ID,通过变量将⾃⼰暴露在全局中,就可以在window.xxx访问到其中的模块。
  • Manifest.json 描述了与其对应的dll.js包含了哪些模块,以及ID和路径。

接下来怎么使⽤呢?

要给web项⽬构建接⼊动态链接库,需要完成以下事情:

  • 将⽹⻚依赖的基础模块抽离,打包到单独的动态链接库,⼀个动态链接库是可以包含多个模块的。
  • 当需要导⼊的模块存在于某个动态链接库中时,不要再次打包,直接使⽤构建好的动态链接库即可。
##webpack.dev.config.js
const { DllReferencePlugin } = require("webpack");
new DllReferencePlugin({
 manifest: path.resolve(__dirname,"./dll/react-manifest.json")
}),
  • ⻚⾯依赖的所有动态链接库都需要被加载。 这⾥推荐使⽤add-asset-html-webpack-plugin插件帮助我们做这个事情。

安装⼀个依赖 npm i add-asset-html-webpack-plugin ,它会将我们打包后的dll.js⽂件注⼊到我们⽣成的index.html中.在webpack.base.config.js⽂件中进⾏更改。

const AddAssetHtmlWebpackPlugin = require("add-asset-html-webpack-plugin");
new AddAssetHtmlWebpackPlugin({
    filepath: path.resolve(__dirname, '../dll/react.dll.js') // 对应的 dll ⽂件路径
 }),

这个理解起来不费劲,操作起来很费劲。所幸,在Webpack5中已经不⽤它了,⽽是⽤HardSourceWebpackPlugin,⼀样的优化效果,但是使⽤却及其简单

  • 提供中间缓存的作⽤
  • ⾸次构建没有太⼤的变化
  • 第⼆次构建时间就会有较⼤的节省

我们在webpack4中使用hard-source-webpack-plugin:

const HardSourceWebpackPlugin = require('hard-source-webpack-plugin')
const plugins = [
 new HardSourceWebpackPlugin()
]

使⽤happypack并发执⾏任务

运⾏在Node.js之上的Webpack是单线程模型的,也就是说Webpack需要⼀个⼀个地处理任务,不能同时处理多个任务。Happy Pack就能让Webpack做到这⼀点,它将任务分解给多个⼦进程去并发执⾏,⼦进程处理完后再将结果发送给主进程。从⽽发挥多核CPU电脑的威⼒。

主要在构建时间比较久,项目复杂的时候用。

npm i -D happypack

const HappyPack = require("happypack");
//构造出一个共享进程池,在进程池中包含5个子进程
var happyThreadPool = HappyPack.ThreadPool({ size: 5 });

// webpack.config.js
rules: [
     {
        test: /\.jsx?$/,
        exclude: /node_modules/,
        use: [
          {
            // ⼀个loader对应⼀个id
            loader: "happypack/loader?id=babel",
          },
        ],
      },
      {
        test: /\.css$/,
        include: path.resolve(__dirname, "./src"),
        use: ["happypack/loader?id=css"],
      },
 ]
//在plugins中增加
plugins:[
    new HappyPack({
      // ⽤唯⼀的标识符id,来代表当前的HappyPack是⽤来处理⼀类特定的⽂件
      id: "babel",
      // 如何处理.js⽂件,⽤法和Loader配置中⼀样
      loaders: ["babel-loader?cacheDirectory"],
      threadPool: happyThreadPool,
    }),
    new HappyPack({
      id: "css",
      loaders: ["style-loader", "css-loader"],
    }),
]