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按需加載秒開應用》(最重要的一步)

相關文章
相關標籤/搜索