阅读 3440

Web性能优化(七):webpack性能优化

webpack性能优化

前一篇文章中,基础 webpack 环境已经搭建起来了,可以愉快地在上面写代码了。

但还不够,以上配置只是一个最基础的配置。我们还想要做一些优化方面的配置。优化有两个方面:

  • 优化开发体验
  • 优化输出质量

其中,为了优化开发体验,webpack 给我们提供了下面的方案:

  1. 使用 source-map 特性查看运行时的源代码
  2. 使用 WebpackDevServer 实现浏览器自动刷新
  3. 缩小文件范围,优化构建速度
  4. CSS 支持:支持 less、sass,并支持自动补齐浏览器前缀 postcss
  5. 优化文件监听范围
  6. DllPlugin 、HardSourceWebpackPlugin 优化构建性能
  7. 【重】使用 happypack 并发执行打包任务

为了优化输出质量,webpack 给我们提供了下面的方案:

  1. 抽离、压缩CSS
  2. 压缩 HTML
  3. 【重】Tree Shaking
  4. 代码分割 Code Splitting

接下来,就来看看对应的配置是怎样的吧。(文末会有对应配置的完整代码文件)

优化开发体验

source-map

在调试过程中,为了体验更好,我们需要使用 webpack 中的 devtool 这个特性。它有一个可设置的 souce-map 特性,包含了源代码与打包后代码的映射关系,可以通过 sourceMap 定位到源代码。

在 devtool 中,有如下几个选项:

  • eval: 使用 eval 包裹模块代码
  • source-map: 产生.map文件
  • cheap: 不包含列信息
  • module: 囊括第三方模块,包含loader的 sourceMap
  • inline: 将.map作为DataURI嵌入,不单独生成.map文件

配置推荐:

//webpack.config.js
devtool: "cheap-module-eval-source-map"//开发环境配置

//线上不推荐开启
复制代码

WebpackDevServer

每次改完代码都需要重新打包一次,打开浏览器,刷新一次,很麻烦。我们可以通过安装 webpackDevServer 来改善这方面的体验。

同时,可以通过代理 proxy 模式,将针对后端的请求代理到本地服务器或者测试服务器上,可以方便自测联调期间的体验。

  • 安装

    npm i webpack-dev-server -D
    复制代码
  • 配置

    修改 package.json:

    "scripts": {
    	"server": "webpack-dev-server"
    }
    复制代码

    修改 webpack.config.js 配置:

    devServer: {
        contentBase: "./dist",
        open: true,
        port: 8081, // webpack-dev-server 的本地端口
        proxy: {    // 通过代理,将以 /api 开头的请求,不再放入到 webpack-dev-server,而是 target 中
           "/api": {
              target: "http://localhost:9092"
              }
           }
    }
    复制代码
  • 启动

    npm run server
    复制代码

缩小文件范围

在开发期间,可以通过缩小文件范围来减少 webpack 不必要的文件扫描,加快每次打包的速度。

优化 resolve.modules 配置

resolve.modules ⽤于配置 webpack 去哪些目录下寻找第三⽅模块,默认是 ['node_modules']。如果没有找到,就会去上⼀级目录 ../node_modules 找,再没有会去 ../../node_modules 中找,以此类推。如果我们的第三方模块都安装在了项⽬根目录下,就可以直接指明这个路径。

优化 resolve.alias 配置

resolve.alias 配置通过别名来将原导⼊路路径映射成一个新的导⼊路径。

拿 react 为例,我们引入的react库,⼀般存在两套代码:

  • cjs:采⽤用commonJS规范的模块化代码

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

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

优化resolve.extensions 配置

在导入语句没带文件后缀时,webpack会⾃动带上后缀后,去尝试查找文件是否存在。这里的 resolve.extensions 指的就是会带上的后缀。建议后缀尝试列表越少越好,我们这里只设置一个:

extensions:['.js']
复制代码

于是,在 resolve 这一项中,得到的配置是这样的:

resolve: {
        modules: [path.resolve(__dirname, "./node_modules")],
        alias: {
            "vue": path.resolve(
              __dirname,
              "./node_modules/vue/dist/vue.esm.js"
            ),
            "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"
              )
        },
        extensions:['.js']
    },
复制代码

CSS 的支持

关于 CSS 的开发体验优化有两块:

  • 支持使用 less 或 sass 作为 css 技术栈
  • 使用 postcss 为样式自动补齐浏览器前缀。

这两块都是可以为开发过程省很多时间的。我习惯使用 less,于是特此安装对应的 loader。

安装
npm i less less-loader -D
npm i post-css-loader autoprefixer -D
复制代码
使用
//webpack.config.js 中增加 less 类型对应的 loader
      rules: [
            {
                test: /\.less$/,
                use:["style-loader","css-loader","less-loader"]
            }
          ]
          
//新建 postcss.config.js 配置 postcss中,需要增加的浏览器前缀
module.exports = {
  plugins: [
    require("autoprefixer")({
      overrideBrowserslist: ["last 2 versions", ">1%"] //覆盖的浏览器类型
    })
  ]
};

复制代码

优化文件监听的性能

我们在调试的时候,期望开启文件监听模式,这样 webpack 就可以在发现文件有变动的时候,做对应的操作。上面的 devServer 就开启了文件监听。当然我们也可以自己开启,通过命令 wepack --watch即可实现。

一般情况下,我们是不需要监听第三方模块的,于是我们可以配置监听模式时候,不去监听第三方模块的文件夹。这样, webpack 消耗的内存和 CPU 都将会大大减少。

//webpack.config.js

module.export = {
		watchOptions : {
      ignored : '/node_modules/'
    }
}
复制代码

DllPlugin、HardSourceWebpackPlugin

还是针对第三方库模块。项目中引入了很多第三方库,这些库在很长的一段时间内,都是不会更新的。那么我们打包的时候可以分开打包来提升打包速度。只需要编译一次,编译完成后存在指定的文件(这里可以称为动态链接库)中。在之后的构建过程中不会再对这些模块进行编译,而是直接使用对应的插件来引用动态链接库的代码。因此可以大大提高构建速度。

本质上就是做缓存嘛!

这样的工作,之前是 DllPlugin 与 DllReferencePlugin 合力完成的。但是整个配置过程较为繁琐(读者若有经历过,可知。且 React Vue 都已经摒弃了 DLL 的方式)。现在有了一个新的插件叫:HardSourceWebpackPlugin。它可以完成与 DllPlugin 、 DllReferencePlugin 同样的工作,但是配置的过程极为简单:

//webpack.config.js
//引用
const HardSourceWebpackPlugin = require('hard-source-webpack-plugin');

//使用
new HardSourceWebpackPlugin()
复制代码

效率对比

在第一次打包的时候,它的速度是相对而言较慢的,需要将对应的第三方库模块写入到 cache 中。但是之后的打包速度提升,效果是极为明显的!!

happypack 并发执行任务

运行在 Node 之上的 webpack 是单线程工作的,不能同时处理多个任务。 Happypack 就能够让 webpack 做到这一点:

happypack 通过new HappyPack(),去实例化一个 HappyPack 对象,其实就是告诉 Happypack 核心调度器如何通过一系列 loader 去转换一类文件,并且可以指定如何为这类转换器作分配子进程。 核心调度器的逻辑代码在主进程里,也就是运行 webpack 的进程中,核心调度器会将一个个任务分配给当前空闲的子进程,子进程处理完后会将结果发送给核心调度器,它们之间的数据交换是通过进进程间的通讯 API 实现的。 核心调度器收到来自子进程处理完毕的结果后,会通知 webpack 该文件已经处理完毕。

  • 配置 happyPack
//webpack.config.js
const HappyPack = require("happypack");
const os = require("os");
//充分发挥多核的作用,进程数量设置为设备的核数
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length });
复制代码
  • 设置 loader 与 happyPack 对应关系,通过ID来对应。
module: {
        rules: [
            {
                test: /\.css$/,
                use: ["happypack/loader?id=css"]
            },
            {
                test: /\.less$/,
                use: ["happypack/loader?id=less"]
            },
            {
                test:/\.(png|jpe?g|gif)$/,
                use: ["happypack/loader?id=pic"]
            },
            {
                test: /\.(eot|ttf|woff|woff2)$/,
                use: ["happypack/loader?id=ttf"]
            },
            {
                test: /\.js$/,
                include: path.resolve(__dirname, "./src"),
                use: ["happypack/loader?id=babel"]
              }
        ]
    },
复制代码
  • 设置 HappyPackPlugin 中更详细的 loader 配置信息:
    plugins: [
	        new HappyPack({
            id: "css",
            loaders: ["style-loader", "css-loader"],
            threadPool: happyThreadPool
          }),
          new HappyPack({
            id: "less",
            loaders: ["style-loader", "css-loader","less-loader"],
            threadPool: happyThreadPool
          }),
          new HappyPack({
            id: "pic",
            loaders: [
              {
                loader: "file-loader",
                options: {
                  name: "[name]_[hash:6].[ext]",
                  outputPath: "images/"
                }
              }
            ],
            threadPool: happyThreadPool
          }),
          new HappyPack({
            id: "ttf",
            loaders: [
              {
                loader: "file-loader",
                options: {
                  name: "[name].[ext]",
                }
              }
            ],
            threadPool: happyThreadPool
          }),
          new HappyPack({
            id: "babel",
            loaders: [
              {
                loader: "babel-loader"
              }
            ],
            threadPool: happyThreadPool
          }),
     ]
复制代码

优化输出质量

上面的配置都是为了优化开发体验而做的配置。在开发完成之后,我们希望我们的文件输出质量也能够得到保障。简单讲,就是最终生成的文件大小越小越好。为了实现这个目标,webpack 给我们提供了一些解决方案:

抽离、压缩CSS

如果不做抽取配置,我们的 css 是直接打包进 JS 里面的,我们希望能够单独生成 css 文件。因为 单独生成的 css 文件可以和 js 文件并行下载(在网页加载的过程中),可以提高文件的加载效率。

webpack 给我们提供了 MiniCssExtractPlugin 来完成 CSS 的抽离。用法如下:

//安装
npm i mini-css-extract-plugin -D

//webpack.config.js
const MiniCssExtractPlugin = require("mini-css-extract-plugin");

module: {
        rules: [
          {
            test: /\.less$/,
            include: path.resolve(__dirname, "./src"),
            use: [
              MiniCssExtractPlugin.loader, //此处需要特换 style-loader
              "css-loader",
              "postcss-loader",
              "less-loader"
            ]
          }
        ]
      },
    plugins : [
        // 抽离css为独立文件输出
        new MiniCssExtractPlugin({
            filename: "css/[name]_[contenthash:6].css"
        })
    ]
复制代码

同时,还有插件可以让我们进行 css 文件的压缩:optimize-css-assets-webpack-plugin、cssnano。用法如下:

//安装
npm i cssnano optimize-css-assets-webpack-plugin -D

//webpack.config.js
const OptimizeCSSAssetsPlugin = require("optimize-css-assets-webpack-plugin");

  plugins: [
  	// 抽离css为独立文件输出
    new MiniCssExtractPlugin({
      filename: "css/[name]_[contenthash:6].css"
    }),
    new OptimizeCSSAssetsPlugin({
      cssProcessor: require("cssnano"), //引入cssnano配置压缩选项
      cssProcessorOptions: {
        discardComments: { removeAll: true } // 将未用到的样式全部移除
      }
    }),
  ]
复制代码

压缩 HTML

对于 HTML 的压缩,可以借助我们前一篇文章提到的 插件:htmlwebpackplugin。用法如下:

new htmlwebpackplugin({
      title: "首页",
      template: "./src/index.html",
      filename: "index.html",
      minify: {
        removeComments: true, // 移除HTML中的注释
        collapseWhitespace: true, // 删除空白符与换行符
        minifyCSS: true // 压缩内联css
      }
    }),
复制代码

JavaScript Tree Shaking

关于 Tree Shaking 的知识点,可以参考百度外卖写的这篇文章。需要了解的一个关键一点是:

在 JavaScript 中,tree-shaking 的消除原理是依赖于 ES6 模块特性的。ES6模块依赖关系是确定的,和运行时的状态无关,可以进行可靠的静态分析,这就是tree-shaking的基础。所谓静态分析就是不执行代码,从字面量上对代码进行分析。

ES6 模块特点:

  • 只能作为模块顶层的语句出现
  • import 的模块名只能是字符串常量
  • import binding 是 immutable的

在最新的 webpack 4 中,只要设置为生产模式,Tree Shaking 是默认开启的。

为了更好地达到 Tree Shaking 的目的,我们应该做到:

  • 使用 ES6 模块语法(即 importexport)。
  • 规范代码习惯,多使用纯函数,少写副作用代码。
  • 如果有明确的副作用代码(比如 @babel/polyfill),须在项目 package.json 文件中,添加一个 "sideEffects" 入口。

tree shaking

CSS Tree Shaking

既然 JavaScript 代码中有 Tree Shaking,那么我们的 CSS 代码是不是也能够有一个这样的插件呢?当然有。

//安装
npm i -D purifycss-webpack purify-css glob-all

//webpack.config.js
const PurifyCSSPlugin = require("purifycss-webpack");
const glob = require("glob-all");

        new PurifyCSSPlugin({
            paths: glob.sync([// 要做 CSS Tree Shaking 的路径文件
              path.resolve(__dirname, "./src/*.html"),
              // 请注意,我们同样需要对 js 文件进行 tree shaking
              // 因为 JS 能够执行 CSSOM 操作与 DOM 操作
              path.resolve(__dirname, "./src/*.js")
            ])
          })
复制代码

代码分割 Code Splitting

打包完之后,所有页面只生成一个 bundle.js (已将 CSS 文件单独剥离)。此时,之所以要代码分割,考虑的是两个方面:

  • 代码体积大,不利于下载。
  • 没有合理利用浏览器下载资源( chrome中,默认最大同时可以有6个 HTTP 请求)

在 Webpack 4 之前,有一些插件是专门用来做代码分割的,如 SplitChunksPlugin、CommonsChunkPlugin。而现在,webpack 4将这个代码分割的工作内置了。对于动态导入模块,默认使用 webpack v4+ 提供的全新的通用分块策略(common chunk strategy)。我们只需要在配置文件中书写即可。

//开启 optimization.splitChunks 时的 default 配置

  optimization: {
    splitChunks: {
      chunks: 'async',//同步(initial)、异步(async)、所有模块有效(all)
      minSize: 30000,//最小尺寸,当模块大于30KB
      maxSize: 0,//对模块进行二次分割时候使用,不推荐使用
      minChunks: 1,//打包生成的chunk文件最少有另外多少个chunk引用了它
      maxAsyncRequests: 5,//最大异步请求数
      maxInitialRequests: 3,//最大初始化请求数
      automaticNameDelimiter: '~',// 打包分割符号
      name: true, //打包后的名称
      cacheGroups: { //缓存组
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10
        },
        default: {
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true
        }
      }
    }
  }
复制代码

在我们这里,会这样使用这个代码分割配置:

   optimization: {
        usedExports: true,
        splitChunks: {
            chunks: "all",
            automaticNameDelimiter: "-",
            cacheGroups: {
            //缓存组
            lodash: {
                test: /lodash/,
                name: "lodash",
                minChunks: 1
            },
            react: {
                test: /react|react-dom/,
                name: "react",
                minChunks: 1
            },
            vue: {
                test: /vue/,
                name: "vue",
                minChunks: 1
            }
            }
        }
    }
复制代码

总结

因为上面的配置有开发模式与生产模式的区别,最终将其配置项做了一个区分与合并。分为三个文件:

//webpack.config.js

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

module.exports = env => {
  if (env && env.production) {
    return merge(baseConfig, prodConfig);
  } else {
    return merge(baseConfig, devConfig);
  }
};
复制代码

对应的 package.json 配置为:

"scripts": {
    "dev": "webpack",
    "build": "webpack --env.production",
    "server": "webpack-dev-server"
  },
复制代码

两种模式下的效果图:

最终效果图

最终配置好的代码在这里可以看到,取需即可。


前端性能优化系列:

(一):从TCP的三次握手讲起

(二):针对TCP传输过程中的堵塞

(三):HTTP协议的优化

(四):图片优化

(五):浏览器缓存策略

(六):浏览器是如何工作的?

(七):webpack性能优化

文章分类
前端
文章标签