Webpack4+Babel7优化70%速度

做者 DBCdouble

项目源码demo:点击这里javascript

1、前言

随着2018年2月15号webpack4.0.0出来已经有一段时间了,webpack依靠着“零配置”,“最高可提高98%的速度”成功吸粉无数,对于饱受项目打包时间过长的我,无疑是看到了曙光,因而决定开始试水。
css

2、项目框架与环境

升级前:
  • Node: v8.11.4
  • webpack: ^1.12.9
  • babel相关: ^6.x
  • react: ^0.14.8(第一次看到react版本的时候,我有点懵,再看一下是真的哈哈😂,不由赞叹最初架构这个项目的人必定是个react大佬,后续会更新文章升级到react16.x)
  • react-router: ^2.6.1(后续会更新文章升级到react-router4.x)
  • 相关loaders
  • 路由组件(页面): 130个(项目采用SPA应用,目前有130个路由页面,因此,若是在足够大的应用上能成功提高构建速度或减少文件大小,那么webpack4.0的版本更新才显得有意义)

升级后:html

  • Node: v8.11.4
  • webpack: ^4.29.5
  • babel相关: ^7.x
  • react: ^0.14.8
  • react-router: ^2.6.1
  • 相关loaders(在后面会详细说明升级的loaders)
  • 路由组件(页面)数量不变

3、背景

随着项目的不断迭代,样式文件和js文件的数量愈来愈多,形成webpack的打包花费的时间愈来愈多,在开发环境下,常常须要频繁调试某一段代码ctrl+s会出现长时间等待的现象(等得好烦),日积月累,浪费了太多的时间在等待打包上。生产环境就更不用说了,平均时长100s~120s左右,一般状况状况下,输入npm run deploy打包以后,我会选择出去抽根烟。而若是状况是要解决线上的bug,则是分秒必争,因此优化打包时间势在必行java

4、分析

webpack的构建流程

  • 初始化:启动构建,读取与合并配置参数,加载 Plugin,实例化 Compiler
  • 编译:从 Entry 发出,针对每一个 Module 串行调用对应的 Loader 去翻译文件内容,再找到该 Module 依赖的 Module,递归地进行编译处理。
  • 输出:对编译后的 Module 组合成 Chunk,把 Chunk 转换成文件,输出到本地。

打包分析

webpack2.x生产环境花费时间: 104.145snode


webpack2.x开发环境花费时间: 68099msreact


虽然能直观得看到webpack2打包所花费的时间,但咱们并不知道webpack打包通过了哪些步骤,在哪一个环节花费了大量时间。这里可使用speed-measure-webpack-plugin来检测webpack打包过程当中各个部分所花费的时间,在终端输入如下命令进行安装。webpack

npm install speed-measure-webpack-plugin -D复制代码

安装完成以后,咱们再webpack的配置文件中配置它git

webpack.config.jses6



参考speed-measure-webpack-plugin的使用方式,查看这里github

配置好以后,启动项目(这里只对开发环境进行分析了)后,以下图


从上图能够看出,webpack打包过程当中绝大部分时间花在了loader上,也就是webpack构建流程的第二个环节,编译阶段。注意上面还能看到ProgressPlugin花费了28.87s,因此在咱们不须要分析webpack打包流程花费的时间后,可在webpack.config.js中注释掉

5、安装和配置

一、webpack

先删除以前的webpack、webpack-cli、webpack-dev-server

npm uninstall webpack webpack-dev-server webpack-cli &&  npm uninstall webpacl-cli -g复制代码

安装最新版本的webpack、webpack-cli(webpack4把脚手架webpack-cli从webpack中抽离出来的,因此必须安装webpack-cli)、webpack-dev-server

npm install webpack webpack-dev-server webpack-cli -D复制代码

我这里顺便再把webpack的相关插件更新到最新版本,由于webpack作了很大的改动相对webpakc2,以防以前老版本的插件不兼容webpack4,因此我这边将项目中的webpack相关插件的模块都先删除掉,以便更新的时候分析错误

npm uninstall extract-text-webpack-plugin html-webpack-plugin webpack-dev-middleware webpack-hot-middleware复制代码

二、升级babel7

删除以前的babel相关模块

npm uninstall babel-core babel-loader babel-cli babel-eslint babel-plugin-react-transform babel-plugin-transform-runtime babel-preset-es2015 babel-preset-react babel-preset-stage-0 babel-runtime复制代码

安装babel7

npm install @babel/cli @babel/core babel-loader @babel/plugin-proposal-class-properties @babel/plugin-proposal-decorators @babel/plugin-proposal-export-default-from @babel/plugin-transform-runtime @babel/preset-env @babel/preset-react复制代码


.babelrc 文件为babel的配置文件(我这边是直接在webpack.config.js的babel-loader的options下配置的,.babelrc文件中注意须要转换为json格式,须要将属性名加双引号)


三、安装ESlint

在项目的根目录下,安装eslinteslint-loader

npm install eslint eslint-loader -D复制代码


.eslintrc是ESlint的配置文件,咱们须要在项目的根目录下增长.eslintrc文件。

{
  "parser": "babel-eslint",
  "env": {
      "browser": true,
      "es6": true,
      "node": true
  },
  "globals" : {
    "Action"       : false,
    "__DEV__"      : false,
    "__PROD__"     : false,
    "__DEBUG__"    : false,
    "__DEBUG_NEW_WINDOW__" : false,
    "__BASENAME__" : false
  },
  "parserOptions": {
      "ecmaVersion": 6,
      "sourceType": "module"
  },
  "extends": "airbnb",
  "rules": {
      "semi": [0],
      "react/jsx-filename-extension": [0]
  }}
复制代码

webpack.config.js中,为须要检测的文件添加eslint-loader加载器。通常咱们是在代码编译前进行检测。

webpack.config.js



注意,这里的isEslint是经过npm scripts传的参数eslint来判断当前环境是否须要进行代码格式检查,以便开发者有更多选择,而且eslint-loader必须配置在babel-loader以前,因此这里用unshift来添加eslint-loader

packack.json

在package.json文件中添加以下命令

{
    "scripts": {
        "eslint": "eslint --ext .js --ext .jsx src/"
    }
}复制代码

到这里,就能够经过执行 npm run eslint来检测src文件下的代码格式了

四、安装打包须要插件

npm install webpack-merge yargs-parser clean-webpack-plugin progress-bar-webpack-plugin webpack-build-notifier html-webpack-plugin mini-css-extract-plugin add-asset-html-webpack-plugin uglifyjs-webpack-plugin optimize-css-assets-webpack-plugin friendly-errors-webpack-plugin happypack复制代码

  • webpack-merge: 用于合并webpack的公共配置和环境配置(合并webpack.config.js和webpack.development.js或者webpack.production.js)
  • yargs-parser: 用于将咱们的npm scripts中的命令行参数转换成键值对的形式如 --mode development会被解析成键值对的形式mode: "development",便于在配置文件中获取参数
  • clean-webpack-plugin: 用于清除本地文件,在进行生产环境打包的时候,若是不清除dist文件夹,那么每次打包都会生成不一样的js文件或者css文件堆积在文件夹中,由于每次打包都会生成不一样的hash值致使每次打包生成的文件名与上次打包不同不会覆盖上次打包留下来的文件
  • progress-bar-webpack-plugin: 打包编译的时候以进度条的形式反馈打包进度
  • webpack-build-notifier: 当你打包以后切换到别的页面的时候,完成时会在本地系统弹出一个提示框告知你打包结果(成功或失败或警告)
  • html-webpack-plugin: 自动生成html,并默认将打包生成的js、css引入到html文件中
  • mini-css-extract-plugin: webpack打包样式文件中的默认会把样式文件代码打包到bundle.js中,mini-css-extract-plugin这个插件能够将样式文件从bundle.js抽离出来一个文件,而且支持chunk css

  • add-asset-html-webpack-plugin: 从命名能够看出,它的做用是能够将静态资源css或者js引入到html-webpack-plugin生成的html文件中

  • uglifyjs-webpack-plugin: 代码丑化,用于js压缩(能够调用系统的线程进行多线程压缩,优化webpack的压缩速度)

  • optimize-css-assets-webpack-plugin: css压缩,主要使用 cssnano 压缩器(webpack4的执行环境内置了cssnano,因此不用安装)

  • friendly-errors-webpack-plugin: 可以更好在终端看到webapck运行的警告和错误
  • happypack: 多线程编译,加快编译速度(加快loader的编译速度),注意,thread-loader不能够和 mini-css-extract-plugin 结合使用

  • splitChunks: CommonChunkPlugin 的后世,用于对bundle.js进行chunk切割(webpack的内置插件)
  • DllPlugin: 将模块预先编译,它会在第一次编译的时候将配置好的须要预先编译的模块编译在缓存中,第二次编译的时候,解析到这些模块就直接使用缓存,而不是去编译这些模块(webpack的内置插件)
  • DllReferencePlugin: 将预先编译好的模块关联到当前编译中,当 webpack 解析到这些模块时,会直接使用预先编译好的模块(webpack的内置插件)
  • HotModuleReplacementPlugin: 实现局部热加载(刷新),区别与在webpack-dev-server的全局刷新(webpack的内置插件)

五、webpack相关文件配置

如下文件直接在你的项目copy就能使用

webpack.config.js

const path = require('path')
const webpack = require('webpack')
const os = require('os')
const merge = require('webpack-merge')
const argv = require('yargs-parser')(process.argv.slice(2))
const mode = argv.mode || 'development'
const interface = argv.interface || 'development'
const isEslint = !!argv.eslint 
const isDev = mode === 'development'
const mergeConfig = require(`./config/webpack.${mode}.js`)
const CleanWebpackPlugin = require('clean-webpack-plugin')
const ProgressBarPlugin = require('progress-bar-webpack-plugin')
const SpeedMeasurePlugin = require("speed-measure-webpack-plugin")
const WebpackBuildNotifierPlugin = require('webpack-build-notifier')
const HtmlWebpackPlugin = require('html-webpack-plugin')
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin')
const FirendlyErrorePlugin = require('friendly-errors-webpack-plugin')
const HappyPack = require('happypack')
const happyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length })
const smp = new SpeedMeasurePlugin()
const loading = {  html:"加载中..."}
const apiConfig = {
  development: 'http://xxxxx/a',
  production: 'http://xxx/b'
}
let commonConfig = {
  module: {
    rules: [{
      test: /\.js$/,
      loaders: ['happypack/loader?id=babel'],
      include: path.resolve(__dirname, 'src'),
      exclude: /node_modules/
    },{
      test: /\.css$/,
      loaders: [
        MiniCssExtractPlugin.loader,
        'css-loader'
      ]
    },{
      test: /\.less$/,
      loaders: [
        isDev ? 'style-loader' : MiniCssExtractPlugin.loader,
        'css-loader',
        {
          loader:'less-loader?sourceMap=true',
          options:{
              javascriptEnabled: true
          },
        }
        // include: path.resolve(__dirname, 'src')
      ]
    },{
        test: /\.(png|svg|jpg|gif)$/,
        use: [
            'url-loader'
        ]
    },{
        test: /\.(woff|woff2|eot|ttf|otf|ico)$/,
        use: [
            'file-loader'
        ]
    },{
        test: /\.(csv|tsv)$/,
        use: [
            'csv-loader'
        ]
    },{
        test: /\.xml$/,
        use: [
            'xml-loader'
        ]
    },{
        test: /\.md$/,
        use: [
            "html-loader",
             "markdown-loader"
        ]
    }]
  },
  //解析 resolve: {
      extensions: ['.js', '.jsx'], // 自动解析肯定的扩展
  },
  plugins: [
    new HappyPack({
      id: 'babel',
      loaders: [{
        loader: 'babel-loader',
        options: {
          cacheDirectory: true,
          presets: ['@babel/preset-env', '@babel/preset-react'],
          plugins: [
            ['@babel/plugin-proposal-decorators', { "legacy": true }],
            '@babel/plugin-proposal-class-properties',
            '@babel/plugin-proposal-export-default-from',
            '@babel/plugin-transform-runtime',
            // 'react-hot-loader/babel',
            // 'dynamic-import-webpack',
            ['import',{
              libraryName:'antd',
              libraryDirectory: 'es',
              style:true
            }]
          ]
        }
      }],
      //共享进程池
      threadPool: happyThreadPool,
      //容许 HappyPack 输出日志
      verbose: true,
    }),
    new CleanWebpackPlugin(['dist']),
    new ProgressBarPlugin(),
    new WebpackBuildNotifierPlugin({
      title: "xxx后台管理系统🍎",
      logo: path.resolve(__dirname, "src/static/favicon.ico"),
      suppressSuccess: true
    }),
    new webpack.DefinePlugin({
      'process.env'  : {
        'NODE_ENV' : JSON.stringify(mode)
      },
      'NODE_ENV'     : JSON.stringify(mode),
      'baseUrl': JSON.stringify(apiConfig[interface]),
      '__DEV__'      : mode === 'development',
      '__PROD__'     : mode === 'production',
      '__TEST__'     : mode === 'test',
      '__DEBUG__'    : mode === 'development' && !argv.no_debug,
      '__DEBUG_NEW_WINDOW__' : !!argv.nw,
      '__BASENAME__' : JSON.stringify(process.env.BASENAME || '')
    }),
    new FirendlyErrorePlugin(),
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, 'public/index.html'),
      favicon: path.resolve(__dirname, 'public/favicon.ico'),
      filename: 'index.html',
      loading
    }),
    new MiniCssExtractPlugin({
      filename: isDev ? 'styles/[name].[hash:4].css' : 'styles/[name].[hash:8].css',
      chunkFilename:isDev ? 'styles/[name].[hash:4].css' : 'styles/[name].[hash:8].css'
    }),
    // 告诉 Webpack 使用了哪些动态连接库
    new webpack.DllReferencePlugin({
      // 描述 vendor 动态连接库的文件内容
      manifest: require('./public/vendor/vendor.manifest.json')
    }),
    // 该插件将把给定的 JS 或 CSS 文件添加到 webpack 配置的文件中,并将其放入资源列表 html webpack插件注入到生成的 html 中。
    new AddAssetHtmlPlugin([
        {
            // 要添加到编译中的文件的绝对路径,以及生成的HTML文件。支持 globby 字符串
            filepath: require.resolve(path.resolve(__dirname, 'public/vendor/vendor.dll.js')),
            // 文件输出目录
            outputPath: 'vendor',
            // 脚本或连接标记的公共路径
            publicPath: 'vendor'
        }
    ]),
    new webpack.HotModuleReplacementPlugin()
  ],
  devServer: {
    host: 'localhost',
    port: 8080,
    historyApiFallback: true,
    overlay: {//当出现编译器错误或警告时,就在网页上显示一层黑色的背景层和错误信息
      errors: true
    },
    inline: true,
    open: true,
    hot: true
  },
  performance: {
    // false | "error" | "warning" // 不显示性能提示 | 以错误形式提示 | 以警告...
    hints: false,    // 开发环境设置较大防止警告
    // 根据入口起点的最大致积,控制webpack什么时候生成性能提示,整数类型,以字节为单位
    maxEntrypointSize: 50000000,
    // 最大单个资源体积,默认250000 (bytes)
    maxAssetSize: 30000000
  }
}
if (isEslint) {
    commonConfig.module.rules.unshift[{
        //前置(在执行编译以前去执行eslint-loader检查代码规范,有报错就不执行编译)
        enforce: 'pre',
        test: /.(js|jsx)$/,
        loaders: ['eslint-loader'],
        exclude: /node_modules/
    }]
}
module.exports = merge(commonConfig, mergeConfig)复制代码

注意:这里在最后导出配置的时候并无使用speed-measure-webpack-plugin,由于会报错,不知道是否是由于跟happypack不兼容的缘由。interface用来判断当前打包js网络请求的地址,isEslint判断是否须要执行代码检测,isDev用来判断当前执行环境是development仍是production,具体问题看代码


webpack.config.dll.js

const path = require('path');
const webpack = require('webpack');
const CleanWebpaclPlugin = require('clean-webpack-plugin');
const FirendlyErrorePlugin = require('friendly-errors-webpack-plugin');
module.exports = {
    mode: 'production',
    entry: {
        // 将 lodash 模块做为入口编译成动态连接库
        vendor: ['react', 'react-dom', 'react-router', 'react-redux', 'react-router-redux']
    },
    output: {
        // 指定生成文件所在目录
        // 因为每次打包生产环境时会清空 dist 文件夹,所以这里我将它们存放在了 public 文件夹下
        path: path.resolve(__dirname, 'public/vendor'),
        // 指定文件名
        filename: '[name].dll.js',
        // 存放动态连接库的全局变量名称,例如对应 vendor 来讲就是 vendor_dll_lib // 这个名称须要与 DllPlugin 插件中的 name 属性值对应起来
        // 之因此在前面 _dll_lib 是为了防止全局变量冲突
        library: '[name]_dll_lib'
    },
    plugins: [
        new CleanWebpaclPlugin(['vendor'], {
            root: path.resolve(__dirname, 'public')
        }),
        new FirendlyErrorePlugin(),                // 接入 DllPlugin
        new webpack.DllPlugin({
            // 描述动态连接库的 manifest.json 文件输出时的文件名称
            // 因为每次打包生产环境时会清空 dist 文件夹,所以这里我将它们存放在了 public 文件夹下
            path: path.join(__dirname, 'public', 'vendor', '[name].manifest.json'),
            // 动态连接库的全局变量名称,须要和 output.library 中保持一致
            // 该字段的值也就是输出的 manifest.json 文件 中 name 字段的值
            // 例如 vendor.manifest.json 中就有 "name": "vendor_dll_lib" name: '[name]_dll_lib'
        })
    ],
    performance: {
        // false | "error" | "warning" // 不显示性能提示 | 以错误形式提示 | 以警告...
        hints: "warning",        // 开发环境设置较大防止警告
        // 根据入口起点的最大致积,控制webpack什么时候生成性能提示,整数类型,以字节为单位
        maxEntrypointSize: 5000000,         // 最大单个资源体积,默认250000 (bytes)
        maxAssetSize: 3000000
    }}复制代码

运行 npm run dll 指令以后,能够看到项目中 public 目录下多出了一个 vendor 的文件夹,能够看到其中包含两个文件:

  • vendor.dll.js 里面包含 react react-dom react-router react-redux react-router-redux 的基础运行环境,将这些基础模块打到一个包里,只要这些包的包的版本没升级,之后每次打包就不须要再编译这些模块,提升打包的速率
  • vendor.manifest.json 也是由 DllPlugin 生成出,用于描述动态连接库文件中包含哪些模块

config/webpack.development.js

module.exports = {
  mode: 'development',
  //devtool: 'cheap-module-source-map',
  devtool: 'eval',
  output: {
    filename: 'scripts/[name].bundle.[hash:4].js'
  }
}复制代码

在开发环境下,咱们不作js压缩和css压缩,来提升开发环境下调试保存页面打包的速度


config/webpack.production.js

const UglifyJsPlugin = require('uglifyjs-webpack-plugin'); //开启多核压缩
const OptmizeCssAssetsWebpackPlugin = require('optimize-css-assets-webpack-plugin')
const os = require('os');
module.exports = {
  mode: 'production',
  devtool: 'hidden-source-map',
  output: {
    filename: 'scripts/[name].bundle.[hash:8].js'
  },
  optimization: {
    splitChunks: {
      chunks: 'all',   // initial、async和all
      minSize: 30000,   // 造成一个新代码块最小的体积
      maxAsyncRequests: 5,   // 按需加载时候最大的并行请求数
      maxInitialRequests: 3,   // 最大初始化请求数
      automaticNameDelimiter: '~',   // 打包分割符
      name: true,
      cacheGroups: {
        vendors: { // 项目基本框架等
          chunks: 'all',
          test: /antd/,
          priority: 100,
          name: 'vendors',
        }
      }
    },
    minimizer: [
      new UglifyJsPlugin({
        parallel: os.cpus().length,
        cache:true,
        sourceMap:true,
        uglifyOptions: {
          compress: {
              // 在UglifyJs删除没有用到的代码时不输出警告
              warnings: false,
              // 删除全部的 `console` 语句,能够兼容ie浏览器
              drop_console: true,
              // 内嵌定义了可是只用到一次的变量
              collapse_vars: true,
              // 提取出出现屡次可是没有定义成变量去引用的静态值
              reduce_vars: true,
          },
          output: {
              // 最紧凑的输出
              beautify: false,
              // 删除全部的注释
              comments: false,
          }
        }
      }),
      new OptmizeCssAssetsWebpackPlugin({
        assetNameRegExp: /\.css$/g,
        cssProcessor: require('cssnano'),
        cssProcessorOptions: {
           safe: true,
           discardComments: {
             removeAll: true
          }
        }
      })
    ],
  }}复制代码

在生产环境的配置中,作了js的压缩和css压缩,还有从打包的入口文件中使用splitChunks分离出来了antd来减少bundle.js的大小


public/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> </body> </html>复制代码

package.json



6、打包应用

一、执行 npm run dll 生成public/vendor(以后打包再也不须要执行此命令,除非vendor中的包版本有变动)


二、执行 npm run start:dev  本地自动开启webpack-dev-server


三、执行 npm run deploy 打包生产环境


四、打包时长比对分析


使用异步加载组件的分割代码的方式进行体积优化见《Webpack按需加载秒开应用》(最重要的一步)

相关文章
相关标签/搜索