深刻淺出 webpack(vue 項目優化)

深刻淺出 webpack

最近在寫一下對本身的思考,發現還有兩篇草稿沒發,記得應該是去年年初的時候寫的,後來公司一直忙,就不多來社區了,今天先發一篇了,這篇記得當時還找到做者,加了微信css

webpack 項目優化

webpack 版本不一樣,配置也會有一些地方不同的,這裏是 webpack 4html

  • 1.優化構建速度。在項目龐大時構建耗時可能會變的很長,每次等待構建的耗時加起來也會是個大數目。
    • 4-1 縮小文件搜索範圍
    • 4-2 使用 DllPlugin
    • 4-3 使用 HappyPack
    • 4-4 使用 ParallelUglifyPlugin
  • 2.優化使用體驗。經過自動化手段完成一些重複的工做,讓咱們專一於解決問題自己。
    • 4-5 使用自動刷新
    • 4-6 開啓模塊熱替換
  • 3.優化輸出質量 優化輸出質量的目的是爲了給用戶呈現體驗更好的網頁,例如減小首屏加載時間、提高性能流暢度等。
    這相當重要,由於在互聯網行業競爭日益激烈的今天,這可能關係到你的產品的生死。 優化輸出質量本質是優化構建輸出的要發佈到線上的代碼,分爲如下幾點:
    • 減小用戶能感知到的加載時間,也就是首屏加載時間。
      • 4-7 區分環境
      • 4-8 壓縮代碼
      • 4-9 CDN 加速
      • 4-10 使用 Tree Shaking
      • 4-11 提取公共代碼
      • 4-12 按需加載
    • 提高流暢度,也就是提高代碼性能。
      • 4-13 使用 Prepack
      • 4-14 開啓 Scope Hoisting
    • 優化的關鍵是找出問題所在,這樣才能一針見血,
      • 4-15 輸出分析 教你如何利用工具快速找出問題所在。
    • 4-16 優化總結 對以上的優化方法作一個總結

4-1 縮小文件搜索範圍

  • 4-1-1 優化 loader 配置
    爲了儘量少的讓文件被 Loader 處理,能夠經過 include 去命中只有哪些文件須要被處理。
    複製代碼
  • 4-1-2 優化 resolve.alias 配置
  • 4-1-3 優化 resolve.extensions 配置

4-2 使用DllPlugin

用過 Windows 系統的人應該會常常看到以 .dll 爲後綴的文件,這些文件稱爲動態連接庫,
在一個動態連接庫中能夠包含給其餘模塊調用的函數和數據。vue

要給 Web 項目構建接入動態連接庫的思想,須要完成如下事情:node

  • 把網頁依賴的基礎模塊抽離出來,打包到一個個單獨的動態連接庫中去。一個動態連接庫中能夠包含多個模塊。
  • 當須要導入的模塊存在於某個動態連接庫中時,這個模塊不能被再次被打包,而是去動態連接庫中獲取。
  • 頁面依賴的全部動態連接庫須要被加載。

爲何給 Web 項目構建接入動態連接庫的思想後,會大大提高構建速度呢? 緣由在於包含大量複用模塊的動態連接庫只須要編譯一次,
在以後的構建過程當中被動態連接庫包含的模塊將不會在從新編譯,
而是直接使用動態連接庫中的代碼react

4-3 使用 HappyPack

分解任務和管理線程的事情 HappyPack 都會幫你作好webpack

4-4 使用 ParallelUglifyPlugin

用過 UglifyJS 的你必定會發如今構建用於開發環境的代碼時很快就能完成,
但在構建用於線上的代碼時構建一直卡在一個時間點遲遲沒有反應,其實卡住的這個時候就是在進行代碼壓縮。git

因爲壓縮 JavaScript 代碼須要先把代碼解析成用 Object 抽象表示的 AST 語法樹,
再去應用各類規則分析和處理 AST,致使這個過程計算量巨大,耗時很是多。github

爲何不把在4-3 使用 HappyPack中介紹過的多進程並行處理的思想也引入到代碼壓縮中呢?web

ParallelUglifyPlugin 就作了這個事情。
當 Webpack 有多個 JavaScript 文件須要輸出和壓縮時,本來會使用 UglifyJS 去一個個挨着壓縮再輸出,
可是 ParallelUglifyPlugin 則會開啓多個子進程,把對多個文件的壓縮工做分配給多個子進程去完成,
每一個子進程其實仍是經過 UglifyJS 去壓縮代碼,可是變成了並行執行。
因此 ParallelUglifyPlugin 能更快的完成對多個文件的壓縮工做。正則表達式

使用 ParallelUglifyPlugin 也很是簡單,把原來 Webpack 配置文件中內置的 UglifyJsPlugin 去掉後,再替換成 ParallelUglifyPlugin,

不過看到 GitHub 上說是支持並行的uglifyjs-webpack-plugin/#parallel

4-5 使用自動刷新

要讓 Webpack 開啓監聽模式,有兩種方式: 在配置文件 webpack.*.config.js 中設置 watch: true。 在執行啓動 Webpack 命令時,帶上 --watch 參數,完整命令是 webpack --watch

文件監聽工做原理: 在 Webpack 中監聽一個文件發生變化的原理是定時的去獲取這個文件的最後編輯時間,
每次都存下最新的最後編輯時間,若是發現當前獲取的和最後一次保存的最後編輯時間不一致,就認爲該文件發生了變化。
配置項中的 watchOptions.poll 就是用於控制定時檢查的週期,具體含義是每隔多少毫秒檢查一次。

  • 優化文件監聽性能
watchOptions: {
  // 不監聽的 node_modules 目錄下的文件
  ignored: /node_modules/,
}
複製代碼

4-6 開啓模塊熱替換

webpack 內置插件 HotModuleReplacementPlugin,
配置 devServer

// these devServer options should be customized in /config/index.js
  devServer: {
    clientLogLevel: 'warning',
    historyApiFallback: {
      rewrites: [
        { from: /.*/, to: path.posix.join(config.dev.assetsPublicPath, 'index.html') },
      ],
    },
    hot: true,
複製代碼

要優化模塊熱替換的構建性能,思路和在4-5 使用自動刷新中提到的很相似:
監聽更少的文件,忽略掉 node_modules 目錄下的文件。
可是其中提到的關閉默認的 inline 模式手動注入代理客戶端的優化方法不能用於在使用模塊熱替換的狀況下,
緣由在於模塊熱替換的運行依賴在每一個 Chunk 中都包含代理客戶端的代碼。

4-7 區分環境

4-8 壓縮代碼

要在 Webpack 中接入 UglifyJS 須要經過插件的形式,目前有兩個成熟的插件,分別是:
UglifyJsPlugin:經過封裝 UglifyJS 實現壓縮。
ParallelUglifyPlugin:多進程並行處理壓縮

  • 壓縮 CSS

    把 cssnano 接入到 Webpack 中也很是簡單,由於 css-loader 已經將其內置了,
    要開啓 cssnano 去壓縮代碼只須要開啓 css-loader 的 minimize 選項

const path = require('path');
const {WebPlugin} = require('web-webpack-plugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');

module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,// 增長對 CSS 文件的支持
        // 提取出 Chunk 中的 CSS 代碼到單獨的文件中
        use: ExtractTextPlugin.extract({
          // 經過 minimize 選項壓縮 CSS 代碼
          use: ['css-loader?minimize']
        }),
      },
    ]
  },
  plugins: [
    // 用 WebPlugin 生成對應的 HTML 文件
    new WebPlugin({
      template: './template.html', // HTML 模版文件所在的文件路徑
      filename: 'index.html' // 輸出的 HTML 的文件名稱
    }),
    new ExtractTextPlugin({
      filename: `[name]_[contenthash:8].css`,// 給輸出的 CSS 文件名稱加上 Hash 值
    }),
  ],
}
複製代碼
  • 壓縮 ES6

4-9 CDN 加速

以前的相對路徑,都變成了絕對的指向 CDN 服務的 URL 地址,配置中的path 也須要換成 CDN 地址前綴

4-10 使用 Tree Shaking

Tree Shaking 能夠用來剔除 JavaScript 中用不上的死代碼。它依賴靜態的 ES6 模塊化語法

4-11 提取公共代碼

Webpack 內置了專門用於提取多個 Chunk 中公共部分的插件 CommonsChunkPlugin,CommonsChunkPlugin 大體使用方法以下:

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');

new CommonsChunkPlugin({
  // 從哪些 Chunk 中提取
  chunks: ['a', 'b'],
  // 提取出的公共部分造成一個新的 Chunk,這個新 Chunk 的名稱
  name: 'common'
})
複製代碼

4-12 按需加載

router 按需加載

4-13 使用 Prepack

在前面的優化方法中提到了代碼壓縮和分塊,這些都是在網絡加載層面的優化,
除此以外還能夠優化代碼在運行時的效率,Prepack 就是爲此而生。
Prepack 由 Facebook 開源,它採用較爲激進的方法:
在保持運行結果一致的狀況下,改變源代碼的運行邏輯,輸出性能更高的 JavaScript 代碼。
實際上 Prepack 就是一個部分求值器,編譯代碼時提早將計算結果放到編譯後的代碼中,而不是在代碼運行時纔去求值。

Prepack 經過在編譯階段預先執行了源碼獲得執行結果,再直接把運行結果輸出來以提高性能

  • Prepack 的工做原理和流程大體以下:

    經過 Babel 把 JavaScript 源碼解析成抽象語法樹(AST),以方便更細粒度地分析源碼;
    Prepack 實現了一個 JavaScript 解釋器,用於執行源碼。
    藉助這個解釋器 Prepack 才能掌握源碼具體是如何執行的,並把執行過程當中的結果返回到輸出中。
    從表面上看去這彷佛很是美好,但實際上 Prepack 還不夠成熟與完善。

  • Prepack 目前還處於初期的開發階段,侷限性也很大,例如:

    • 不能識別 DOM API 和 部分 Node.js API,若是源碼中有調用依賴運行環境的 API 就會致使 Prepack 報錯;
    • 存在優化後的代碼性能反而更低的狀況;
    • 存在優化後的代碼文件尺寸大大增長的狀況。
    • 總之,如今把 Prepack 用於線上環境還爲時過早
  • 接入 Webpack

    const PrepackWebpackPlugin = require('prepack-webpack-plugin').default;
    
      module.exports = {
        plugins: [
          new PrepackWebpackPlugin()
        ]
      };
    複製代碼

4-14 開啓 Scope Hoisting

Scope Hoisting 可讓 Webpack 打包出來的代碼文件更小、運行的更快, 它又譯做 "做用域提高",
是在 Webpack3 中新推出的功能

好處是:

  • 代碼體積更小,由於函數申明語句會產生大量代碼;

  • 代碼在運行時由於建立的函數做用域更少了,內存開銷也隨之變小。

  • Scope Hoisting 的實現原理其實很簡單: 分析出模塊之間的依賴關係,儘量的把打散的模塊合併到一個函數中去,但前提是不能形成代碼冗餘。
    所以只有那些被引用了一次的模塊才能被合併。

因爲 Scope Hoisting 須要分析出模塊之間的依賴關係,所以源碼必須採用 ES6 模塊化語句,否則它將沒法生效。
緣由和4-10 使用 TreeShaking 中介紹的相似。

4-15 輸出分析

爲了更簡單直觀的分析輸出結果,社區中出現了許多可視化的分析工具。
這些工具以圖形的方式把結果更加直觀的展現出來,讓你快速看到問題所在。

兩種分析工具:

4-15-1 生成 stats.json

在啓動 Webpack 時帶上以上兩個參數,啓動命令以下:

webpack --profile --json > stats.json,
複製代碼

若是沒有問題,你會發現項目中多出了一個 stats.json 文件。
這個 stats.json 文件是給後面介紹的可視化分析工具使用的。

但是我在 vue 項目中使用時出現了一個問題

web>webpack --profile --json > stats.json
No configuration file found and no output filename configured via CLI option.
A configuration file could be named 'webpack.config.js' in the current directory
.
Use --help to display the CLI options.
複製代碼
  • 它的意思是,假如沒有指定配置文件,會在當前目錄尋找webpack.config.js 做爲配置文件
  • 解決: 使用 config 指定配置文件,
    webpack --config ./build/webpack.dev.conf.js --json > stats.json
    複製代碼

webpack --profile --json 會輸出字符串形式的 JSON,
stats.json 是 UNIX/Linux 系統中的管道命令,
含義是把 webpack --profile --json 輸出的內容經過管道輸出到 stats.json 文件中。

4-15-2 官方的可視化分析工具: Webpack Analyse: 在線 Web 應用

打開 Webpack Analyse 連接的網頁後,你就會看到一個彈窗提示你上傳 JSON 文件,
也就是須要上傳上面講到的 stats.json 文件

4-15-3 webpack-bundle-analyzer

發現 vue-cli 2 版本中 webpack.prod.conf.js 裏面有關因而否開啓 webpack-bundle-analyzer 配置; 也就是說 npm run build --report 的時候,BundleAnalyzerPlugin 能以可視化的方式展現打包結果;

若是單獨使用 webpack-bundle-analyzer:

  • 1.安裝 webpack-bundle-analyzer 到全局,執行命令 npm i -g webpack-bundle-analyzer;
  • 2.按照上面提到的方法生成 stats.json 文件;
  • 3.在項目根目錄中執行 webpack-bundle-analyzer 後,瀏覽器會打開對應網頁看到以上效果

4-16 優化總結

按照開發環境和線上環境爲該項目配置了兩份文件,下面是使用 webpack4 版本

  • 側重優化開發體驗的配置文件 webpack.config.js:
const path = require('path');
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const {AutoWebPlugin} = require('web-webpack-plugin');
const HappyPack = require('happypack');

// 自動尋找 pages 目錄下的全部目錄,把每個目錄當作一個單頁應用
const autoWebPlugin = new AutoWebPlugin('./src/pages', {
  // HTML 模版文件所在的文件路徑
  template: './template.html',
  // 提取出全部頁面公共的代碼
  commonsChunk: {
    // 提取出公共代碼 Chunk 的名稱
    name: 'common',
  },
});

module.exports = {
  // AutoWebPlugin 會找爲尋找到的全部單頁應用,生成對應的入口配置,
  // autoWebPlugin.entry 方法能夠獲取到生成入口配置
  entry: autoWebPlugin.entry({
    // 這裏能夠加入你額外須要的 Chunk 入口
    base: './src/base.js',
  }),
  output: {
    filename: '[name].js',
  },
  resolve: {
    // 使用絕對路徑指明第三方模塊存放的位置,以減小搜索步驟
    // 其中 __dirname 表示當前工做目錄,也就是項目根目錄
    modules: [path.resolve(__dirname, 'node_modules')],
    // 針對 Npm 中的第三方模塊優先採用 jsnext:main 中指向的 ES6 模塊化語法的文件,使用 Tree Shaking 優化
    // 只採用 main 字段做爲入口文件描述字段,以減小搜索步驟
    mainFields: ['jsnext:main', 'main'],
  },
  module: {
    rules: [
      {
        // 若是項目源碼中只有 js 文件就不要寫成 /\.jsx?$/,提高正則表達式性能
        test: /\.js$/,
        // 使用 HappyPack 加速構建
        use: ['happypack/loader?id=babel'],
        // 只對項目根目錄下的 src 目錄中的文件採用 babel-loader
        include: path.resolve(__dirname, 'src'),
      },
      {
        test: /\.js$/,
        use: ['happypack/loader?id=ui-component'],
        include: path.resolve(__dirname, 'src'),
      },
      {
        // 增長對 CSS 文件的支持
        test: /\.css$/,
        use: ['happypack/loader?id=css'],
      },
    ]
  },
  plugins: [
    autoWebPlugin,
    // 使用 HappyPack 加速構建
    new HappyPack({
      id: 'babel',
      // babel-loader 支持緩存轉換出的結果,經過 cacheDirectory 選項開啓
      loaders: ['babel-loader?cacheDirectory'],
    }),
    new HappyPack({
      // UI 組件加載拆分
      id: 'ui-component',
      loaders: [{
        loader: 'ui-component-loader',
        options: {
          lib: 'antd',
          style: 'style/index.css',
          camel2: '-'
        }
      }],
    }),
    new HappyPack({
      id: 'css',
      // 如何處理 .css 文件,用法和 Loader 配置中同樣
      loaders: ['style-loader', 'css-loader'],
    }),
    // 4-11提取公共代碼
    new CommonsChunkPlugin({
      // 從 common 和 base 兩個現成的 Chunk 中提取公共的部分
      chunks: ['common', 'base'],
      // 把公共的部分放到 base 中
      name: 'base'
    }),
  ],
  watchOptions: {
    // 4-5使用自動刷新:不監聽的 node_modules 目錄下的文件
    ignored: /node_modules/,
  }
};
複製代碼
  • 側重優化輸出質量的配置文件 webpack-dist.config.js:
const path = require('path');
const DefinePlugin = require('webpack/lib/DefinePlugin');
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const {AutoWebPlugin} = require('web-webpack-plugin');
const HappyPack = require('happypack');
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');

// 自動尋找 pages 目錄下的全部目錄,把每個目錄當作一個單頁應用
const autoWebPlugin = new AutoWebPlugin('./src/pages', {
  // HTML 模版文件所在的文件路徑
  template: './template.html',
  // 提取出全部頁面公共的代碼
  commonsChunk: {
    // 提取出公共代碼 Chunk 的名稱
    name: 'common',
  },
  // 指定存放 CSS 文件的 CDN 目錄 URL
  stylePublicPath: '//css.cdn.com/id/',
});

module.exports = {
  // AutoWebPlugin 會找爲尋找到的全部單頁應用,生成對應的入口配置,
  // autoWebPlugin.entry 方法能夠獲取到生成入口配置
  entry: autoWebPlugin.entry({
    // 這裏能夠加入你額外須要的 Chunk 入口
    base: './src/base.js',
  }),
  output: {
    // 給輸出的文件名稱加上 Hash 值
    filename: '[name]_[chunkhash:8].js',
    path: path.resolve(__dirname, './dist'),
    // 指定存放 JavaScript 文件的 CDN 目錄 URL
    publicPath: '//js.cdn.com/id/',
  },
  resolve: {
    // 使用絕對路徑指明第三方模塊存放的位置,以減小搜索步驟
    // 其中 __dirname 表示當前工做目錄,也就是項目根目錄
    modules: [path.resolve(__dirname, 'node_modules')],
    // 只採用 main 字段做爲入口文件描述字段,以減小搜索步驟
    mainFields: ['jsnext:main', 'main'],
  },
  module: {
    rules: [
      {
        // 若是項目源碼中只有 js 文件就不要寫成 /\.jsx?$/,提高正則表達式性能
        test: /\.js$/,
        // 使用 HappyPack 加速構建
        use: ['happypack/loader?id=babel'],
        // 只對項目根目錄下的 src 目錄中的文件採用 babel-loader
        include: path.resolve(__dirname, 'src'),
      },
      {
        test: /\.js$/,
        use: ['happypack/loader?id=ui-component'],
        include: path.resolve(__dirname, 'src'),
      },
      {
        // 增長對 CSS 文件的支持
        test: /\.css$/,
        // 提取出 Chunk 中的 CSS 代碼到單獨的文件中
        use: ExtractTextPlugin.extract({
          use: ['happypack/loader?id=css'],
          // 指定存放 CSS 中導入的資源(例如圖片)的 CDN 目錄 URL
          publicPath: '//img.cdn.com/id/'
        }),
      },
    ]
  },
  plugins: [
    autoWebPlugin,
    // 4-14開啓ScopeHoisting
    new ModuleConcatenationPlugin(),
    // 4-3使用HappyPack
    new HappyPack({
      // 用惟一的標識符 id 來表明當前的 HappyPack 是用來處理一類特定的文件
      id: 'babel',
      // babel-loader 支持緩存轉換出的結果,經過 cacheDirectory 選項開啓
      loaders: ['babel-loader?cacheDirectory'],
    }),
    new HappyPack({
      // UI 組件加載拆分
      id: 'ui-component',
      loaders: [{
        loader: 'ui-component-loader',
        options: {
          lib: 'antd',
          style: 'style/index.css',
          camel2: '-'
        }
      }],
    }),
    new HappyPack({
      id: 'css',
      // 如何處理 .css 文件,用法和 Loader 配置中同樣
      // 經過 minimize 選項壓縮 CSS 代碼
      loaders: ['css-loader?minimize'],
    }),
    new ExtractTextPlugin({
      // 給輸出的 CSS 文件名稱加上 Hash 值
      filename: `[name]_[contenthash:8].css`,
    }),
    // 4-11提取公共代碼
    new CommonsChunkPlugin({
      // 從 common 和 base 兩個現成的 Chunk 中提取公共的部分
      chunks: ['common', 'base'],
      // 把公共的部分放到 base 中
      name: 'base'
    }),
    new DefinePlugin({
      // 定義 NODE_ENV 環境變量爲 production 去除 react 代碼中的開發時才須要的部分
      'process.env': {
        NODE_ENV: JSON.stringify('production')
      }
    }),
    // 使用 ParallelUglifyPlugin 並行壓縮輸出的 JS 代碼
    new ParallelUglifyPlugin({
      // 傳遞給 UglifyJS 的參數
      uglifyJS: {
        output: {
          // 最緊湊的輸出
          beautify: false,
          // 刪除全部的註釋
          comments: false,
        },
        compress: {
          // 在UglifyJs刪除沒有用到的代碼時不輸出警告
          warnings: false,
          // 刪除全部的 `console` 語句,能夠兼容ie瀏覽器
          drop_console: true,
          // 內嵌定義了可是隻用到一次的變量
          collapse_vars: true,
          // 提取出出現屢次可是沒有定義成變量去引用的靜態值
          reduce_vars: true,
        }
      },
    }),
  ]
};
複製代碼

吳浩麟擁有本書的著做權。
其它人不能將本書用於商用用途,不能轉載,不能以任何形式發行,違者將追究法律責任。

參考

相關文章
相關標籤/搜索