webpack 性能優化小結

背景

現在前端工程化的概念早已經深刻人心,選擇一款合適的編譯和資源管理工具已經成爲了全部前端工程中的標配,而在諸多的構建工具中,webpack以其豐富的功能和靈活的配置而深受業內吹捧,逐步取代了grunt和gulp成爲大多數前端工程實踐中的首選,React,Vue,Angular等諸多知名項目也都相繼選用其做爲官方構建工具,極受業內追捧。可是,隨者工程開發的複雜程度和代碼規模不斷地增長,webpack暴露出來的各類性能問題也愈發明顯,極大的影響着開發過程當中的體驗。css

問題概括

歷經了多個web項目的實戰檢驗,咱們對webapck在構建中逐步暴露出來的性能問題概括主要有以下幾個方面:html

  • 代碼全量構建速度過慢,即便是很小的改動,也要等待長時間才能查看到更新與編譯後的結果(引入HMR熱更新後有明顯改進);
  • 隨着項目業務的複雜度增長,工程模塊的體積也會急劇增大,構建後的模塊一般要以M爲單位計算;
  • 多個項目之間共用基礎資源存在重複打包,基礎庫代碼複用率不高;
  • node的單進程實如今耗cpu計算型loader中表現不佳;

針對以上的問題,咱們來看看怎樣利用webpack現有的一些機制和第三方擴展插件來逐個擊破。前端

1、慢在何處&如何優化

做爲工程師,咱們一直鼓勵要理性思考,用數據和事實說話,「我以爲很慢」,「太卡了」,「太大了」之類的表述不免顯得太籠統和太抽象,那麼咱們不妨從以下幾個方面來着手進行分析:node

圖片描述

  • 從項目結構着手,代碼組織是否合理,依賴使用是否合理;
  • 從webpack自身提供的優化手段着手,看看哪些api未作優化配置;
  • 從webpack自身的不足着手,作有針對性的擴展優化,進一步提高效率;

在這裏咱們推薦使用一個wepback的可視化資源分析工具:webpack-bundle-analyzer,在webpack構建的時候會自動幫你計算出各個模塊在你的項目工程中的依賴與分佈狀況,方便作更精確的資源依賴和引用的分析。react

從上圖中咱們不難發現大多數的工程項目中,依賴庫的體積永遠是大頭,一般體積能夠佔據整個工程項目的7-9成,並且在每次開發過程當中也會從新讀取和編譯對應的依賴資源,這實際上是很大的的資源開銷浪費,並且對編譯結果影響微乎其微,畢竟在實際業務開發中,咱們不多會去主動修改第三方庫中的源碼,改進方案以下:jquery

1.1 縮小文件的搜索範圍:

搜索過程優化方式包括:webpack

  1. resolve字段告訴webpack怎麼去搜索文件,因此首先要重視resolve字段的配置:git

    1. 設置resolve.modules:[path.resolve(__dirname, 'node_modules')]避免層層查找。es6

      resolve.modules告訴webpack去哪些目錄下尋找第三方模塊,默認值爲['node_modules'],會依次查找./node_modules、../node_modules、../../node_modules。github

    2. 設置resolve.mainFields:['main'],設置儘可能少的值能夠減小入口文件的搜索步驟

      第三方模塊爲了適應不一樣的使用環境,會定義多個入口文件,mainFields定義使用第三方模塊的哪一個入口文件,因爲大多數第三方模塊都使用main字段描述入口文件的位置,因此能夠設置單獨一個main值,減小搜索

    3. 對龐大的第三方模塊設置resolve.alias, 使webpack直接使用庫的min文件,避免庫內解析

      如對於react:

      resolve.alias:{
          'react':patch.resolve(__dirname, './node_modules/react/dist/react.min.js')
      }
      這樣會影響Tree-Shaking,適合對總體性比較強的庫使用,若是是像lodash這類工具類的比較分散的庫,比較適合Tree-Shaking,避免使用這種方式。
    4. 合理配置resolve.extensions,減小文件查找

      默認值:extensions:['.js', '.json'],當導入語句沒帶文件後綴時,Webpack會根據extensions定義的後綴列表進行文件查找,因此:

      • 列表值儘可能少
      • 頻率高的文件類型的後綴寫在前面
      • 源碼中的導入語句儘量的寫上文件後綴,如require(./data)要寫成require(./data.json)
  2. module.noParse字段告訴Webpack沒必要解析哪些文件,能夠用來排除對非模塊化庫文件的解析

    如jQuery、ChartJS,另外若是使用resolve.alias配置了react.min.js,則也應該排除解析,由於react.min.js通過構建,已是能夠直接運行在瀏覽器的、非模塊化的文件了。noParse值能夠是RegExp、[RegExp]、function

    module:{ noParse:[/jquery|chartjs/, /react\.min\.js$/] }

  3. 配置loader時,經過test、exclude、include縮小搜索範圍

1.2 合理配置 CommonsChunkPlugin

webpack的資源入口一般是以entry爲單元進行編譯提取,那麼當多entry共存的時候,CommonsChunkPlugin的做用就會發揮出來,對全部依賴的chunk進行公共部分的提取,可是在這裏可能不少人會誤認爲抽取公共部分指的是能抽取某個代碼片斷,其實並不是如此,它是以module爲單位進行提取。

假設咱們的頁面中存在entry1,entry2,entry3三個入口,這些入口中可能都會引用如utils,loadash,fetch等這些通用模塊,那麼就能夠考慮對這部分的共用部分機提取。一般提取方式有以下四種實現:

一、傳入字符串參數,由chunkplugin自動計算提取

new webpack.optimize.CommonsChunkPlugin('common.js')

這種作法默認會把全部入口節點的公共代碼提取出來, 生成一個common.js

二、有選擇的提取公共代碼

new webpack.optimize.CommonsChunkPlugin('common.js',['entry1','entry2']);

只提取entry1節點和entry2中的共用部分模塊, 生成一個common.js

三、將entry下全部的模塊的公共部分(可指定引用次數)提取到一個通用的chunk中

new webpack.optimize.CommonsChunkPlugin({
    name: 'vendors',
    minChunks: function (module, count) {
       return (
          module.resource &&
          /\.js$/.test(module.resource) &&
          module.resource.indexOf(
            path.join(__dirname, '../node_modules')
          ) === 0
       )
    }
});

提取全部node_modules中的模塊至vendors中,也能夠指定minChunks中的最小引用數;

四、抽取enry中的一些lib抽取到vendors中

entry = {
    vendors: ['fetch', 'loadash']
};
new webpack.optimize.CommonsChunkPlugin({
    name: "vendors",
    minChunks: Infinity
});

添加一個entry名叫爲vendors,並把vendors設置爲所須要的資源庫,CommonsChunk會自動提取指定庫至vendors中。

1.3 經過 externals 配置來提取經常使用庫

在實際項目開發過程當中,咱們並不須要實時調試各類庫的源碼,這時候就能夠考慮使用external選項了。

 

 簡單來講external就是把咱們的依賴資源聲明爲一個外部依賴,而後經過script外鏈腳本引入。這也是咱們早期頁面開發中資源引入的一種翻版,只是經過配置後能夠告知webapck遇到此類變量名時就能夠不用解析和編譯至模塊的內部文件中,而改用從外部變量中讀取,這樣能極大的提高編譯速度,同時也能更好的利用CDN來實現緩存。

external的配置相對比較簡單,只須要完成以下三步:

一、在頁面中加入須要引入的lib地址,以下:

<head>
<script src="//cdn.bootcss.com/jquery.min.js"></script>
<script src="//cdn.bootcss.com/underscore.min.js"></script>
<script src="/static/common/react.min.js"></script>
<script src="/static/common/react-dom.js"></script>
<script src="/static/common/react-router.js"></script>
<script src="/static/common/immutable.js"></script>
</head>

二、在webapck.config.js中加入external配置項:

module.export = {
    externals: {
        'react-router': {
            amd: 'react-router',
            root: 'ReactRouter',
            commonjs: 'react-router',
            commonjs2: 'react-router'
        },
        react: {
            amd: 'react',
            root: 'React',
            commonjs: 'react',
            commonjs2: 'react'
        },
        'react-dom': {
            amd: 'react-dom',
            root: 'ReactDOM',
            commonjs: 'react-dom',
            commonjs2: 'react-dom'
        }
    }
}

這裏要提到的一個細節是:此類文件在配置前,構建這些資源包時須要採用amd/commonjs/cmd相關的模塊化進行兼容封裝,即打包好的庫已是umd模式包裝過的,如在node_modules/react-router中咱們能夠看到umd/ReactRouter.js之類的文件,只有這樣webpack中的require和import * from 'xxxx'才能正確讀到該類包的引用,在這類js的頭部通常也能看到以下字樣:

if (typeof exports === 'object' && typeof module === 'object') {
    module.exports = factory(require("react"));
} else if (typeof define === 'function' && define.amd) {
    define(["react"], factory);
} else if (typeof exports === 'object') {
    exports["ReactRouter"] = factory(require("react"));
} else {
    root["ReactRouter"] = factory(root["React"]);
}

三、很是重要的是必定要在output選項中加入以下一句話:

output: {
  libraryTarget: 'umd'
}

因爲經過external提取過的js模塊是不會被記錄到webapck的chunk信息中,經過libraryTarget可告知咱們構建出來的業務模塊,當讀到了externals中的key時,須要以umd的方式去獲取資源名,不然會有出現找不到module的狀況。

經過配置後,咱們能夠看到對應的資源信息已經能夠在瀏覽器的source map中讀到了。

externals.png

對應的資源也能夠直接由頁面外鏈載入,有效地減少了資源包的體積。

圖片描述

1.4 利用 DllPlugin 和 DllReferencePlugin 預編譯資源模塊

咱們的項目依賴中一般會引用大量的npm包,而這些包在正常的開發過程當中並不會進行修改,可是在每一次構建過程當中卻須要反覆的將其解析,如何來規避此類損耗呢?這兩個插件就是幹這個用的。

簡單來講DllPlugin的做用是預先編譯一些模塊,而DllReferencePlugin則是把這些預先編譯好的模塊引用起來。這邊須要注意的是DllPlugin必需要在DllReferencePlugin執行前先執行一次,dll這個概念應該也是借鑑了windows程序開發中的dll文件的設計理念。

相對於externals,dllPlugin有以下幾點優點:

  • dll預編譯出來的模塊能夠做爲靜態資源連接庫可被重複使用,尤爲適合多個項目之間的資源共享,如同一個站點pc和手機版等;
  • dll資源能有效地解決資源循環依賴的問題,部分依賴庫如:react-addons-css-transition-group這種原先從react核心庫中抽取的資源包,整個代碼只有一句話:

module.exports = require('react/lib/ReactCSSTransitionGroup');
  • 卻由於從新指向了react/lib中,這也會致使在經過externals引入的資源只能識別react,尋址解析react/lib則會出現沒法被正確索引的狀況。

  • 因爲externals的配置項須要對每一個依賴庫進行逐個定製,因此每次增長一個組件都須要手動修改,略微繁瑣,而經過dllPlugin則能徹底經過配置讀取,減小維護的成本;

一、配置dllPlugin對應資源表並編譯文件

那麼externals該如何使用呢,其實只須要增長一個配置文件:webpack.dll.config.js:

const webpack = require('webpack');
const path = require('path');
const isDebug = process.env.NODE_ENV === 'development';
const outputPath = isDebug ? path.join(__dirname, '../common/debug') : path.join(__dirname, '../common/dist');
const fileName = '[name].js';

// 資源依賴包,提早編譯
const lib = [
  'react',
  'react-dom',
  'react-router',
  'history',
  'react-addons-pure-render-mixin',
  'react-addons-css-transition-group',
  'redux',
  'react-redux',
  'react-router-redux',
  'redux-actions',
  'redux-thunk',
  'immutable',
  'whatwg-fetch',
  'byted-people-react-select',
  'byted-people-reqwest'
];

const plugin = [
  new webpack.DllPlugin({
    /**
     * path
     * 定義 manifest 文件生成的位置
     * [name]的部分由entry的名字替換
     */
    path: path.join(outputPath, 'manifest.json'),
    /**
     * name
     * dll bundle 輸出到那個全局變量上
     * 和 output.library 同樣便可。
     */
    name: '[name]',
    context: __dirname
  }),
  new webpack.optimize.OccurenceOrderPlugin()
];

if (!isDebug) {
  plugin.push(
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': JSON.stringify('production')
    }),
    new webpack.optimize.UglifyJsPlugin({
      mangle: {
        except: ['$', 'exports', 'require']
      },
      compress: { warnings: false },
      output: { comments: false }
    })
  )
}

module.exports = {
  devtool: '#source-map',
  entry: {
    lib: lib
  },
  output: {
    path: outputPath,
    filename: fileName,
    /**
     * output.library
     * 將會定義爲 window.${output.library}
     * 在此次的例子中,將會定義爲`window.vendor_library`
     */
    library: '[name]',
    libraryTarget: 'umd',
    umdNamedDefine: true
  },
  plugins: plugin
}

而後執行命令:

 $ NODE_ENV=development webpack --config  webpack.dll.lib.js --progress
 $ NODE_ENV=production webpack --config  webpack.dll.lib.js --progress 

便可分別編譯出支持調試版和生產環境中lib靜態資源庫,在構建出來的文件中咱們也能夠看到會自動生成以下資源:

common
├── debug
│   ├── lib.js
│   ├── lib.js.map
│   └── manifest.json
└── dist
    ├── lib.js
    ├── lib.js.map
    └── manifest.json

文件說明:

  • lib.js能夠做爲編譯好的靜態資源文件直接在頁面中經過src連接引入,與externals的資源引入方式同樣,生產與開發環境能夠經過相似charles之類的代理轉發工具來作路由替換;
  • manifest.json中保存了webpack中的預編譯信息,這樣等於提早拿到了依賴庫中的chunk信息,在實際開發過程當中就無須要進行重複編譯;

二、dllPlugin的靜態資源引入

lib.js和manifest.json存在一一對應的關係,因此咱們在調用的過程也許遵循這個原則,如當前處於開發階段,對應咱們能夠引入common/debug文件夾下的lib.js和manifest.json,切換到生產環境的時候則須要引入common/dist下的資源進行對應操做,這裏考慮到手動切換和維護的成本,咱們推薦使用add-asset-html-webpack-plugin進行依賴資源的注入,可獲得以下結果:

<head>
<script src="/static/common/lib.js"></script>
</head>

在webpack.config.js文件中增長以下代碼:

const isDebug = (process.env.NODE_ENV === 'development');
const libPath = isDebug ? '../dll/lib/manifest.json' : 
'../dll/dist/lib/manifest.json';

// 將mainfest.json添加到webpack的構建中

module.export = {
  plugins: [
       new webpack.DllReferencePlugin({
       context: __dirname,
       manifest: require(libPath),
      })
  ]
}

配置完成後咱們能發現對應的資源包已經完成了純業務模塊的提取

圖片描述

多個工程之間若是須要使用共同的lib資源,也只須要引入對應的lib.js和manifest.js便可,plugin配置中也支持多個webpack.DllReferencePlugin同時引入使用,以下:

module.export = {
  plugins: [
     new webpack.DllReferencePlugin({
        context: __dirname,
        manifest: require(libPath),
      }),
      new webpack.DllReferencePlugin({
        context: __dirname,
        manifest: require(ChartsPath),
      })
  ]
}

1.5 使用 Happypack 加速你的代碼構建

以上介紹均爲針對webpack中的chunk計算和編譯內容的優化與改進,對資源的實際體積改進上也較爲明顯,那麼除此以外,咱們可否針對資源的編譯過程和速度優化上作些嘗試呢?

衆所周知,webpack中爲了方便各類資源和類型的加載,設計了以loader加載器的形式讀取資源,可是受限於node的編程模型影響,全部的loader雖然以async的形式來併發調用,可是仍是運行在單個 node的進程以及在同一個事件循環中,這就直接致使了當咱們須要同時讀取多個loader文件資源時,好比babel-loader須要transform各類jsx,es6的資源文件。在這種同步計算同時須要大量耗費cpu運算的過程當中,node的單進程模型就無優點了,那麼happypack就針對解決此類問題而生。

開啓happypack的線程池

happypack的處理思路是將原有的webpack對loader的執行過程從單一進程的形式擴展多進程模式,本來的流程保持不變,這樣能夠在不修改原有配置的基礎上來完成對編譯過程的優化,具體配置以下:

const HappyPack = require('happypack');
 const os = require('os')
 const HappyThreadPool = HappyPack.ThreadPool({ size: os.cpus().length}); // 啓動線程池});

module:{
    rules: [
      {
        test: /\.(js|jsx)$/,
        // use: ['babel-loader?cacheDirectory'],
        use: 'happypack/loader?id=jsx',
        exclude: /^node_modules$/
      }
    ]
  },
  plugins:[
    new HappyPack({
     id: 'jsx',
     cache: true,
     threadPool: HappyThreadPool,
     loaders: ['babel-loader']
   })
  ]

咱們能夠看到經過在loader中配置直接指向happypack提供的loader,對於文件實際匹配的處理 loader,則是經過配置在plugin屬性來傳遞說明,這裏happypack提供的loader與plugin的銜接匹配,則是經過id=happybabel來完成。配置完成後,laoder的工做模式就轉變成了以下所示:

圖片描述

happypack在編譯過程當中除了利用多進程的模式加速編譯,還同時開啓了cache計算,能充分利用緩存讀取構建文件,對構建的速度提高也是很是明顯的,通過測試,最終的構建速度提高以下:

優化前:
圖片描述

優化後:
圖片描述

關於happyoack的更多介紹能夠查看:

1.6 加強 uglifyPlugin

uglifyJS憑藉基於node開發,壓縮比例高,使用方便等諸多優勢已經成爲了js壓縮工具中的首選,可是咱們在webpack的構建中觀察發現,當webpack build進度走到80%先後時,會發生很長一段時間的停滯,經測試對比發現這一過程正是uglfiyJS在對咱們的output中的bunlde部分進行壓縮耗時過長致使,針對這塊咱們可使用webpack-uglify-parallel來提高壓縮速度。

從插件源碼中能夠看到,webpack-uglify-parallel的是實現原理是採用了多核並行壓縮的方式來提高咱們的壓縮速度。

plugin.nextWorker().send({
    input: input,
    inputSourceMap: inputSourceMap,
    file: file,
    options: options
});

plugin._queue_len++;
                
if (!plugin._queue_len) {
    callback();
}               

if (this.workers.length < this.maxWorkers) {
    var worker = fork(__dirname + '/lib/worker');
    worker.on('message', this.onWorkerMessage.bind(this));
    worker.on('error', this.onWorkerError.bind(this));
    this.workers.push(worker);
}

this._next_worker++;
return this.workers[this._next_worker % this.maxWorkers];

使用配置也很是簡單,只須要將咱們原來webpack中自帶的uglifyPlugin配置:

new webpack.optimize.UglifyJsPlugin({
   exclude:/\.min\.js$/
   mangle:true,
   compress: { warnings: false },
   output: { comments: false }
})

修改爲以下代碼便可:

   const os = require('os');
    const UglifyJsParallelPlugin = require('webpack-uglify-parallel');
    
    new UglifyJsParallelPlugin({
      workers: os.cpus().length,
      mangle: true,
      compressor: {
        warnings: false,
        drop_console: true,
        drop_debugger: true
       }
    })

目前webpack官方也維護了一個支持多核壓縮的UglifyJs插件:uglifyjs-webpack-plugin,使用方式相似,優點在於徹底兼容webpack.optimize.UglifyJsPlugin中的配置,能夠經過uglifyOptions寫入,所以也作爲推薦使用,參考配置以下:

const UglifyJsPlugin = require('uglifyjs-webpack-plugin');
  new UglifyJsPlugin({
    uglifyOptions: {
      ie8: false,
      ecma: 8,
      mangle: true,
      output: { comments: false },
      compress: { warnings: false }
    },
    sourceMap: false,
    cache: true,
    parallel: os.cpus().length * 2
  })

1.7 Tree-shaking & Scope Hoisting

wepback在2.X和3.X中從rolluo中借鑑了tree-shakingScope Hoisting,利用es6的module特性,利用AST對全部引用的模塊和方法作了靜態分析,從而能有效地剔除項目中的沒有引用到的方法,並將相關方法調用概括到了獨立的webpack_module中,對打包構建的體積優化也較爲明顯,可是前提是全部的模塊寫法必須使用ES6 Module進行實現,具體配置參考以下:

// .babelrc: 經過配置減小沒有引用到的方法
  {
    "presets": [
      ["env", {
        "targets": {
          "browsers": ["last 2 versions", "safari >= 7"]
        }
      }],
      // https://www.zhihu.com/question/41922432
      ["es2015", {"modules": false}]  // tree-shaking
    ]
  }

  // webpack.config: Scope Hoisting
  {
    plugins:[
      // https://zhuanlan.zhihu.com/p/27980441
      new webpack.optimize.ModuleConcatenationPlugin()
    ]
  }

適用場景

在實際的開發過程當中,可靈活地選擇適合自身業務場景的優化手段。

優化手段 開發環境 生產環境
CommonsChunk
externals  
DllPlugin
Happypack  
uglify-parallel  

工程演示demo

1.8 使用ParallelUglifyPlugin開啓多進程壓縮JS文件

使用UglifyJS插件壓縮JS代碼時,須要先將代碼解析成Object表示的AST(抽象語法樹),再去應用各類規則去分析和處理AST,因此這個過程計算量大耗時較多。ParallelUglifyPlugin能夠開啓多個子進程,每一個子進程使用UglifyJS壓縮代碼,能夠並行執行,能顯著縮短壓縮時間。

使用也很簡單,把原來的UglifyJS插件換成本插件便可,使用以下:

npm i -D webpack-parallel-uglify-plugin

// webpack.config.json
const ParallelUglifyPlugin = require('wbepack-parallel-uglify-plugin');
//...
plugins: [
    new ParallelUglifyPlugin({
        uglifyJS:{
            //...這裏放uglifyJS的參數
        },
        //...其餘ParallelUglifyPlugin的參數,設置cacheDir能夠開啓緩存,加快構建速度
    })
]

2、優化開發體驗

開發過程當中修改源碼後,須要自動構建和刷新瀏覽器,以查看效果。這個過程可使用Webpack實現自動化,Webpack負責監聽文件的變化,DevServer負責刷新瀏覽器。

2.1 使用自動刷新

2.1.1 Webpack監聽文件

Webpack可使用兩種方式開啓監聽:1. 啓動webpack時加上--watch參數;2. 在配置文件中設置watch:true。此外還有以下配置參數。合理設置watchOptions能夠優化監聽體驗。

module.exports = {
    watch: true,
    watchOptions: {
        ignored: /node_modules/,
        aggregateTimeout: 300,  //文件變更後多久發起構建,越大越好
        poll: 1000,  //每秒詢問次數,越小越好
    }
}

ignored:設置不監聽的目錄,排除node_modules後能夠顯著減小Webpack消耗的內存

aggregateTimeout:文件變更後多久發起構建,避免文件更新太快而形成的頻繁編譯以致卡死,越大越好

poll:經過向系統輪詢文件是否變化來判斷文件是否改變,poll爲每秒詢問次數,越小越好

2.1.2 DevServer刷新瀏覽器

DevServer刷新瀏覽器有兩種方式

  1. 向網頁中注入代理客戶端代碼,經過客戶端發起刷新
  2. 向網頁裝入一個iframe,經過刷新iframe實現刷新效果

默認狀況下,以及 devserver: {inline:true} 都是採用第一種方式刷新頁面。第一種方式DevServer由於不知道網頁依賴哪些Chunk,因此會向每一個chunk中都注入客戶端代碼,當要輸出不少chunk時,會致使構建變慢。而一個頁面只須要一個客戶端,因此關閉inline模式能夠減小構建時間,chunk越多提高月明顯。關閉方式:

  1. 啓動時使用webpack-dev-server --inline false
  2. 配置 devserver:{inline:false}

關閉inline後入口網址變爲http://localhost:8080/webpack-dev-server/

另外devServer.compress 參數可配置是否採用Gzip壓縮,默認爲false

2.2 開啓模塊熱替換HMR

模塊熱替換不刷新整個網頁而只從新編譯發生變化的模塊,並用新模塊替換老模塊,因此預覽反應更快,等待時間更少,同時不刷新頁面能保留當前網頁的運行狀態。原理也是向每個chunk中注入代理客戶端來鏈接DevServer和網頁。開啓方式:

  1. webpack-dev-server --hot
  2. 使用HotModuleReplacementPlugin,比較麻煩

開啓後若是修改子模塊就能夠實現局部刷新,但若是修改的是根JS文件,會整頁刷新,緣由在於,子模塊更新時,事件一層層向上傳遞,直到某層的文件接收了當前變化的模塊,而後執行回調函數。若是一層層向外拋直到最外層都沒有文件接收,就會刷新整頁。

使用 NamedModulesPlugin 可使控制檯打印出被替換的模塊的名稱而非數字ID,另外同webpack監聽,忽略node_modules目錄的文件能夠提高性能。

3、優化輸出質量-壓縮文件體積

3.1 區分環境--減少生產環境代碼體積

代碼運行環境分爲開發環境和生產環境,代碼須要根據不一樣環境作不一樣的操做,許多第三方庫中也有大量的根據開發環境判斷的if else代碼,構建也須要根據不一樣環境輸出不一樣的代碼,因此須要一套機制能夠在源碼中區分環境,區分環境以後可使輸出的生產環境的代碼體積減少。Webpack中使用DefinePlugin插件來定義配置文件適用的環境。

const DefinePlugin = require('webpack/lib/DefinePlugin');
//...
plugins:[
    new DefinePlugin({
        'process.env': {
            NODE_ENV: JSON.stringify('production')
        }
    })
]

注意,JSON.stringify('production') 的緣由是,環境變量值須要一個雙引號包裹的字符串,而stringify後的值是'"production"'

而後就能夠在源碼中使用定義的環境:

if(process.env.NODE_ENV === 'production'){
    console.log('你在生產環境')
    doSth();
}else{
    console.log('你在開發環境')
    doSthElse();
}

當代碼中使用了process時,Webpack會自動打包進process模塊的代碼以支持非Node.js的運行環境,這個模塊的做用是模擬Node.js中的process,以支持process.env.NODE_ENV === 'production' 語句。

3.2 壓縮代碼-JS、ES、CSS

  1. 壓縮JS:Webpack內置UglifyJS插件、ParallelUglifyPlugin

    會分析JS代碼語法樹,理解代碼的含義,從而作到去掉無效代碼、去掉日誌輸入代碼、縮短變量名等優化。經常使用配置參數以下:

const UglifyJSPlugin = require('webpack/lib/optimize/UglifyJsPlugin');
//...
plugins: [
    new UglifyJSPlugin({
        compress: {
            warnings: false,  //刪除無用代碼時不輸出警告
            drop_console: true,  //刪除全部console語句,能夠兼容IE
            collapse_vars: true,  //內嵌已定義但只使用一次的變量
            reduce_vars: true,  //提取使用屢次但沒定義的靜態值到變量
        },
        output: {
            beautify: false, //最緊湊的輸出,不保留空格和製表符
            comments: false, //刪除全部註釋
        }
    })
]
  • 使用webpack --optimize-minimize 啓動webpack,能夠注入默認配置的UglifyJSPlugin

  • 壓縮ES6:第三方UglifyJS插件

    隨着愈來愈多的瀏覽器支持直接執行ES6代碼,應儘量的運行原生ES6,這樣比起轉換後的ES5代碼,代碼量更少,且ES6代碼性能更好。直接運行ES6代碼時,也須要代碼壓縮,第三方的uglify-webpack-plugin提供了壓縮ES6代碼的功能:

npm i -D uglify-webpack-plugin@beta //要使用最新版本的插件
//webpack.config.json
const UglifyESPlugin = require('uglify-webpack-plugin');
//...
plugins:[
    new UglifyESPlugin({
        uglifyOptions: {  //比UglifyJS多嵌套一層
            compress: {
                warnings: false,
                drop_console: true,
                collapse_vars: true,
                reduce_vars: true
            },
            output: {
                beautify: false,
                comments: false
            }
        }
    })
]
  1. 另外要防止babel-loader轉換ES6代碼,要在.babelrc中去掉babel-preset-env,由於正是babel-preset-env負責把ES6轉換爲ES5。

  2. 壓縮CSS:css-loader?minimize、PurifyCSSPlugin

    cssnano基於PostCSS,不只是刪掉空格,還能理解代碼含義,例如把color:#ff0000 轉換成 color:red,css-loader內置了cssnano,只須要使用 css-loader?minimize 就能夠開啓cssnano壓縮。

    另一種壓縮CSS的方式是使用PurifyCSSPlugin,須要配合 extract-text-webpack-plugin 使用,它主要的做用是能夠去除沒有用到的CSS代碼,相似JS的Tree Shaking。

3.3 使用Tree Shaking剔除JS死代碼

4、優化輸出質量--加速網絡請求

4.1 使用CDN加速靜態資源加載

  1. CND加速的原理

    CDN經過將資源部署到世界各地,使得用戶能夠就近訪問資源,加快訪問速度。要接入CDN,須要把網頁的靜態資源上傳到CDN服務上,在訪問這些資源時,使用CDN服務提供的URL。

    因爲CDN會爲資源開啓長時間的緩存,例如用戶從CDN上獲取了index.html,即便以後替換了CDN上的index.html,用戶那邊仍會在使用以前的版本直到緩存時間過時。業界作法:

    • HTML文件:放在本身的服務器上且關閉緩存,不接入CDN
    • 靜態的JS、CSS、圖片等資源:開啓CDN和緩存,同時文件名帶上由內容計算出的Hash值,這樣只要內容變化hash就會變化,文件名就會變化,就會被從新下載而不論緩存時間多長。

    另外,HTTP1.x版本的協議下,瀏覽器會對於向同一域名並行發起的請求數限制在4~8個。那麼把全部靜態資源放在同一域名下的CDN服務上就會遇到這種限制,因此能夠把他們分散放在不一樣的CDN服務上,例如JS文件放在js.cdn.com下,將CSS文件放在css.cdn.com下等。這樣又會帶來一個新的問題:增長了域名解析時間,這個能夠經過dns-prefetch來解決 <link rel='dns-prefetch' href='//js.cdn.com'> 來縮減域名解析的時間。形如**//xx.com 這樣的URL省略了協議**,這樣作的好處是,瀏覽器在訪問資源時會自動根據當前URL採用的模式來決定使用HTTP仍是HTTPS協議。

  2. 總之,構建須要知足如下幾點:

    • 靜態資源導入的URL要變成指向CDN服務的絕對路徑的URL
    • 靜態資源的文件名須要帶上根據內容計算出的Hash值
    • 不一樣類型資源放在不一樣域名的CDN上
  3. 最終配置:

const ExtractTextPlugin = require('extract-text-webpack-plugin');
const {WebPlugin} = require('web-webpack-plugin');
//...
output:{
 filename: '[name]_[chunkhash:8].js',
 path: path.resolve(__dirname, 'dist'),
 publicPatch: '//js.cdn.com/id/', //指定存放JS文件的CDN地址
},
module:{
 rules:[{
     test: /\.css/,
     use: ExtractTextPlugin.extract({
         use: ['css-loader?minimize'],
         publicPatch: '//img.cdn.com/id/', //指定css文件中導入的圖片等資源存放的cdn地址
     }),
 },{
    test: /\.png/,
    use: ['file-loader?name=[name]_[hash:8].[ext]'], //爲輸出的PNG文件名加上Hash值 
 }]
},
plugins:[
  new WebPlugin({
     template: './template.html',
     filename: 'index.html',
     stylePublicPath: '//css.cdn.com/id/', //指定存放CSS文件的CDN地址
  }),
 new ExtractTextPlugin({
     filename:`[name]_[contenthash:8].css`, //爲輸出的CSS文件加上Hash
 })
]

4.2 多頁面應用提取頁面間公共代碼,以利用緩存

  1. 原理

    大型網站一般由多個頁面組成,每一個頁面都是一個獨立的單頁應用,多個頁面間確定會依賴一樣的樣式文件、技術棧等。若是不把這些公共文件提取出來,那麼每一個單頁打包出來的chunk中都會包含公共代碼,至關於要傳輸n份重複代碼。若是把公共文件提取出一個文件,那麼當用戶訪問了一個網頁,加載了這個公共文件,再訪問其餘依賴公共文件的網頁時,就直接使用文件在瀏覽器的緩存,這樣公共文件就只用被傳輸一次。

  2. 應用方法

    1. 把多個頁面依賴的公共代碼提取到common.js中,此時common.js包含基礎庫的代碼

const CommonsChunkPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
//...
plugins:[
    new CommonsChunkPlugin({
        chunks:['a','b'], //從哪些chunk中提取
        name:'common',  // 提取出的公共部分造成一個新的chunk
    })
]

      2. 找出依賴的基礎庫,寫一個base.js文件,再與common.js提取公共代碼到base中,common.js就剔除了基礎庫代碼,而base.js保持不變

//base.js
import 'react';
import 'react-dom';
import './base.css';
//webpack.config.json
entry:{
    base: './base.js'
},
plugins:[
    new CommonsChunkPlugin({
        chunks:['base','common'],
        name:'base',
        //minChunks:2, 表示文件要被提取出來須要在指定的chunks中出現的最小次數,防止common.js中沒有代碼的狀況
    })        
]
  1. 獲得基礎庫代碼base.js,不含基礎庫的公共代碼common.js,和頁面各自的代碼文件xx.js。

     頁面引用順序以下:base.js--> common.js--> xx.js

4.3 分割代碼以按需加載

  1. 原理

    單頁應用的一個問題在於使用一個頁面承載複雜的功能,要加載的文件體積很大,不進行優化的話會致使首屏加載時間過長,影響用戶體驗。作按需加載能夠解決這個問題。具體方法以下:

    1. 將網站功能按照相關程度劃分紅幾類
    2. 每一類合併成一個Chunk,按需加載對應的Chunk
    3. 例如,只把首屏相關的功能放入執行入口所在的Chunk,這樣首次加載少許的代碼,其餘代碼要用到的時候再去加載。最好提早預估用戶接下來的操做,提早加載對應代碼,讓用戶感知不到網絡加載
  2. 作法

    一個最簡單的例子:網頁首次只加載main.js,網頁展現一個按鈕,點擊按鈕時加載分割出去的show.js,加載成功後執行show.js裏的函數

//main.js
document.getElementById('btn').addEventListener('click',function(){
    import(/* webpackChunkName:"show" */ './show').then((show)=>{
        show('Webpack');
    })
})
//show.js
module.exports = function (content) {
    window.alert('Hello ' + content);
}
import(/* webpackChunkName:show */ './show').then() 是實現按需加載的關鍵,Webpack內置對import( *)語句的支持,Webpack會以 ./show.js爲入口從新生成一個Chunk。代碼在瀏覽器上運行時只有點擊了按鈕纔會開始加載show.js,且import語句會返回一個Promise,加載成功後能夠在then方法中獲取加載的內容。這要求瀏覽器支持Promise API,對於不支持的瀏覽器,須要注入Promise polyfill。 /* webpackChunkName:show */ 是定義動態生成的Chunk的名稱,默認名稱是[id].js,定義名稱方便調試代碼。爲了正確輸出這個配置的ChunkName,還須要配置Webpack:
//...
output:{
    filename:'[name].js',
    chunkFilename:'[name].js', //指定動態生成的Chunk在輸出時的文件名稱
}

5、優化輸出質量--提高代碼運行時的效率

5.1 使用Prepack提早求值

  1. 原理:

    Prepack是一個部分求值器,編譯代碼時提早將計算結果放到編譯後的代碼中,而不是在代碼運行時纔去求值。經過在便一階段預先執行源碼來獲得執行結果,再直接將運行結果輸出以提高性能。可是如今Prepack還不夠成熟,用於線上環境還爲時過早。

  2. 使用方法

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

5.2 使用Scope Hoisting

  1. 原理

    譯做「做用域提高」,是在Webpack3中推出的功能,它分析模塊間的依賴關係,儘量將被打散的模塊合併到一個函數中,但不能形成代碼冗餘,因此只有被引用一次的模塊才能被合併。因爲須要分析模塊間的依賴關係,因此源碼必須是採用了ES6模塊化的,不然Webpack會降級處理不採用Scope Hoisting。

  2. 使用方法

const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
//...
plugins:[
    new ModuleConcatenationPlugin();
],
resolve:{
    mainFields:['jsnext:main','browser','main']
}

6、使用輸出分析工具

啓動Webpack時帶上這兩個參數能夠生成一個json文件,輸出分析工具大多依賴該文件進行分析:

webpack --profile --json > stats.json 其中 --profile 記錄構建過程當中的耗時信息,--json 以JSON的格式輸出構建結果,>stats.json 是UNIX / Linux系統中的管道命令,含義是將內容經過管道輸出到stats.json文件中。

  1. 官方工具Webpack Analyse

    打開該工具的官網http://webpack.github.io/analyse/上傳stats.json,就能夠獲得分析結果

  2. webpack-bundle-analyzer

    可視化分析工具,比Webapck Analyse更直觀。使用也很簡單:

    1. npm i -g webpack-bundle-analyzer安裝到全局
    2. 按照上面方法生成stats.json文件
    3. 在項目根目錄執行webpack-bundle-analyzer ,瀏覽器會自動打開結果分析頁面。

7、其餘Tips

  1. 配置babel-loader時,use: [‘babel-loader?cacheDirectory’] cacheDirectory用於緩存babel的編譯結果,加快從新編譯的速度。另外注意排除node_modules文件夾,由於文件都使用了ES5的語法,不必再使用Babel轉換。

  2. 配置externals,排除由於已使用<script>標籤引入而不用打包的代碼,noParse是排除沒使用模塊化語句的代碼。

  3. 配置performance參數能夠輸出文件的性能檢查配置。

  4. 配置profile:true,是否捕捉Webpack構建的性能信息,用於分析是什麼緣由致使構建性能不佳。

  5. 配置cache:true,是否啓用緩存來提高構建速度。

  6. 可使用url-loader把小圖片轉換成base64嵌入到JS或CSS中,減小加載次數。

  7. 經過imagemin-webpack-plugin壓縮圖片,經過webpack-spritesmith製做雪碧圖。

  8. 開發環境下將devtool設置爲cheap-module-eval-source-map,由於生成這種source map的速度最快,能加速構建。在生產環境下將devtool設置爲hidden-source-map

舒適提醒

本文中的全部例子已經從新優化,支持最新的webpack3特性,並附帶有分享ppt地址,能夠在線點擊查看

小結

性能優化無小事,追求快沒有止境,在前端工程日益龐大複雜的今天,針對實際項目,持續改進構建工具的性能,對項目開發效率的提高和工具深度理解都是極其有益的。

 

參考連接:

https://segmentfault.com/a/1190000007891318?utm_source=tag-newest

http://www.javashuo.com/article/p-wfvgtmoy-bv.html

相關文章
相關標籤/搜索