衆所周知,webpack做爲主流的前端項目利器,從編譯到打包提供了不少方便的功能。本文主要從編譯和體積兩個篇章闡述筆者總結的實踐心得,但願對你們有幫助。css
vendor文件即依賴庫文件,通常在項目中不多改動。單獨打包能夠在後續的項目迭代過程當中,保證vendor文件可從客戶端緩存讀取,提高客戶端的訪問體驗。 html
解決方案:經過在vendor.config.js文件中定義,在webpack.config.{evn}.js中引用來使用。 vendor.config.js示例前端
module.exports = { entry: { vendor: ['react', 'react-dom', 'react-redux', 'react-router-dom', 'react-loadable', 'axios'], } };
vendor單獨打包以後,仍是有一個問題。編譯的過程當中,每次都須要對vendor文件進行打包,其實這一塊要是能夠提早打包好,那後續編譯的時候,就能夠節約這部分的時間了。 vue
解決方案:定義webpack.dll.config.js,使用 DLLPlugin
提早執行打包,而後在webpack.config.{evn}.js經過 DLLReferencePlugin
引入打包好的文件,最後使用AddAssetHtmlPlugin
往html裏注入vendor文件路徑
webpack.dll.config.js示例node
const TerserPlugin = require('terser-webpack-plugin'); const CleanWebpackPlugin = require("clean-webpack-plugin"); const webpack = require('webpack'); const path = require('path'); const dllDist = path.join(__dirname, 'dist'); module.exports = { entry: { vendor: ['react', 'react-dom', 'react-redux', 'react-router-dom', 'react-loadable', 'axios', 'moment'], }, output: { path: const dllDist = path.join(__dirname, 'dist'), filename: '[name]-[hash].js', library: '[name]', }, optimization: { minimizer: [ new TerserPlugin({ terserOptions: { parse: { ecma: 8, }, compress: { ecma: 5, warnings: false, comparisons: false, inline: 2, }, mangle: { safari10: true, }, output: { ecma: 5, comments: false, ascii_only: true, }, }, parallel: true, cache: true, sourceMap: false, }), ], }, plugins: [ new CleanWebpackPlugin(["*.js"], { // 清除以前的dll文件 root: dllDist, }), new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/), new webpack.DllPlugin({ path: path.join(__dirname, 'dll', '[name]-manifest.json'), name: '[name]', }), ] };
webpack.config.prod.js片斷react
const manifest = require('./dll/vendor-manifest.json'); const AddAssetHtmlPlugin = require('add-asset-html-webpack-plugin'); ... plugins: [ // webpack讀取到vendor的manifest文件對於vendor的依賴不會進行編譯打包 new webpack.DllReferencePlugin({ manifest, }), // 往html中注入vendor js new AddAssetHtmlPlugin([{ publicPath: "/view/static/js", // 注入到html中的路徑 outputPath: "../build/static/js", // 最終輸出的目錄 filepath: path.resolve(__dirname, './dist/*.js'), includeSourcemap: false, typeOfAsset: "js" }]), ]
webpack對文件的編譯處理是單進程的,但實際上咱們的編譯機器一般是多核多進程,若是能夠充分利用cpu的運算力,能夠提高很大的編譯速度。 webpack
解決方案:使用happypack進行多進程構建,使用webpack4內置的TerserPlugin並行模式進行js的壓縮。 ios
說明:happypack原理可參考http://taobaofed.org/blog/201...git
webpack.config.prod.js片斷github
const HappyPack = require('happypack'); // 採用多進程,進程數由CPU核數決定 const happThreadPool = HappyPack.ThreadPool({ size: os.cpus().length }); ... optimization: { minimizer: [ new TerserPlugin({ terserOptions: { parse: { ecma: 8, }, compress: { ecma: 5, warnings: false, comparisons: false, inline: 2, }, mangle: { safari10: true, }, output: { ecma: 5, comments: false, ascii_only: true, }, }, parallel: true, cache: true, sourceMap: false, }), ] }, module: { rules: [ { test: /.css$/, oneOf: [ { test: /\.(js|mjs|jsx)$/, include: paths.appSrc, loader: 'happypack/loader', options: { cacheDirectory: true, }, }, ] } ] }, plugins: [ new HappyPack({ threadPool: happThreadPool, loaders: [{ loader: 'babel-loader', }] }), ]
當js頁面特別多的時候,若是都打包成一個文件,那麼很影響訪問頁面訪問的速度。理想的狀況下,是到相應頁面的時候才下載相應頁面的js。
解決方案:使用import('path/to/module') -> Promise。調用 import() 之處,被做爲分離的模塊起點,意思是,被請求的模塊和它引用的全部子模塊,會分離到一個單獨的 chunk 中。
說明: 老版本使用require.ensure(dependencies, callback)進行按需加載,webpack > 2.4 的版本此方法已經被import()取代
按需加載demo,在非本地的環境下開啓監控上報
if (process.env.APP_ENV !== 'local') { import("./utils/emonitor").then(({emonitorReport}) => { emonitorReport(); }); }
react頁面按需加載,可參考http://react.html.cn/docs/cod...,裏面提到的React.lazy,React.Suspense是在react 16.6版本以後纔有的新特性,對於老版本,官方依然推薦使用react-loadable
實現路由懶加載
react-loadable示例
import React, { Component } from 'react'; import { Route, Switch } from 'react-router-dom'; import Loadable from 'react-loadable'; import React, { Component } from 'react'; // 通過包裝的組件會在訪問相應的頁面時才異步地加載相應的js const Home = Loadable({ loader: () => import('./page/Home'), loading: (() => null), delay: 1000, }); import NotFound from '@/components/pages/NotFound'; class CRouter extends Component { render() { return ( <Switch> <Route exact path='/' component={Home}/> {/* 若是沒有匹配到任何一個Route, <NotFound>會被渲染*/} <Route component={NotFound}/> </Switch> ) } } export default CRouter
vue頁面按需加載,可參考https://router.vuejs.org/zh/g...
示例
// 下面2行代碼,沒有指定webpackChunkName,每一個組件打包成一個js文件。 const ImportFuncDemo1 = () => import('../components/ImportFuncDemo1') const ImportFuncDemo2 = () => import('../components/ImportFuncDemo2') // 下面2行代碼,指定了相同的webpackChunkName,會合並打包成一個js文件。 // const ImportFuncDemo = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '../components/ImportFuncDemo') // const ImportFuncDemo2 = () => import(/* webpackChunkName: 'ImportFuncDemo' */ '../components/ImportFuncDemo2') export default new Router({ routes: [ { path: '/importfuncdemo1', name: 'ImportFuncDemo1', component: ImportFuncDemo1 }, { path: '/importfuncdemo2', name: 'ImportFuncDemo2', component: ImportFuncDemo2 } ] })
作完按需加載以後,假如定義的分離點裏包含了css文件,那麼相關css樣式也會被打包進js chunk裏,並經過URL.createObjectURL(blob)的方式加載到頁面中。
假如n個頁面引用了共同的css樣式,無形中也增長n倍的 css in js體積。經過css預加載,把共同css提煉到html link標籤裏,能夠優化這部分的體積。
解決方案:把分離點裏的頁面css引用(包括less和sass)提煉到index.less中,在index.js文件中引用。假如使用到庫的less文件特別多,能夠定義一個cssVendor.js,在index.js中引用,並在webpack config中添加一個entry以配合MiniCssExtractPlugin作css抽離。
P.S. 假如用到antd或其餘第三方UI庫,按需加載的時候記得把css引入選項取消,把 style: true
選項刪掉
示例
cssVendor片斷
// 全局引用的組件的樣式預加載,按需引用,可優化異步加載的chunk js體積 // Row import 'antd/es/row/style/index.js'; // Col import 'antd/es/col/style/index.js'; // Card import 'antd/es/card/style/index.js'; // Icon import 'antd/es/icon/style/index.js'; // Modal import 'antd/es/modal/style/index.js'; // message import 'antd/es/message/style/index.js'; ...
webpack.config.production片斷
entry: { main: [paths.appIndexJs, paths.cssVendorJs] }, plugins: [ new HappyPack({ threadPool: happThreadPool, loaders: [{ loader: 'babel-loader', options: { customize: require.resolve( 'babel-preset-react-app/webpack-overrides' ), plugins: [ [ require.resolve('babel-plugin-named-asset-import'), { loaderMap: { svg: { ReactComponent: '@svgr/webpack?-prettier,-svgo![path]', }, }, }, ], ['import', { libraryName: 'antd', libraryDirectory: 'es' }, ], ], cacheDirectory: true, cacheCompression: true, compact: true, }, }], })]
咱們在項目的開發中常常會引用一些第三方庫,例如antd,lodash。這些庫在咱們的項目中默認是全量引入的,但其實咱們只用到庫裏的某些組件或者是某些函數,那麼按需只打包咱們引用的組件或函數就能夠減小js至關大一部分的體積。
解決方案:使用babel-plugin-import插件來實現按需打包,具體使用方式可參考https://github.com/ant-design...
示例
{ test: /\.(js|jsx)$/, include: paths.appSrc, loader: require.resolve('babel-loader'), exclude: /node_modules/, options: { plugins: [ ['import', [ { libraryName: 'lodash', libraryDirectory: '', "camel2DashComponentName": false, }, { libraryName: 'antd', style: true } ] ], ], compact: true, }, }
有些包含多語言的庫會將全部本地化內容和核心功能一塊兒打包,因而打包出來的js裏會包含不少多語言的配置文件,這些配置文件若是不打包進來,也能夠減小js的體積。
解決方案:使用IgnorePlugin插件忽略指定資源路徑的打包
示例
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/)
壓縮是一道常規的生產工序,前端項目編譯出來的文件通過壓縮混淆,能夠把體積進一步縮小。
解決方案:使用TerserPlugin插件進行js壓縮,使用OptimizeCSSAssetsPlugin插件css壓縮
說明:webpack4以前js壓縮推薦使用ParalleUglifyPlugin插件,它在UglifyJsPlugin的基礎上作了多進程並行處理的優化,速度更快;css壓縮推薦使用cssnano,它基於PostCSS。由於css-loader已經將其內置了,要開啓cssnano去壓縮代碼只須要開啓css-loader的minimize選項。
示例
minimizer: [ new TerserPlugin({ terserOptions: { parse: { ecma: 8, }, compress: { ecma: 5, warnings: false, comparisons: false, inline: 2, }, mangle: { safari10: true, }, output: { ecma: 5, comments: false, ascii_only: true, }, }, parallel: true, cache: true, sourceMap: shouldUseSourceMap, }), new OptimizeCSSAssetsPlugin({ cssProcessorOptions: { parser: safePostCssParser, map: shouldUseSourceMap ? { inline: false, annotation: true, } : false, }, }), ]
在不少chunks裏,有相同的依賴,把這些依賴抽離爲一個公共的文件,則能夠有效地減小資源的體積,並能夠充分利用瀏覽器緩存。
解決方案:使用SplitChunksPlugin抽離共同文件
P.S. webpack4使用SplitChunksPlugin代替了CommonsChunkPlugin
示例
optimization: { splitChunks: { chunks: 'all', name: false } }
SplitChunksPlugin的具體配置可參考 https://juejin.im/post/5af15e...
Scope Hoisting 是webpack3中推出的新功能,能夠把依賴的代碼直接注入到入口文件裏,減小了函數做用域的聲明,也減小了js體積和內存開銷
舉個栗子
假如如今有兩個文件分別是 util.js:
export default 'Hello,Webpack';
和入口文件 main.js:
import str from './util.js'; console.log(str);
以上源碼用 Webpack 打包後輸出中的部分代碼以下:
[ (function (module, __webpack_exports__, __webpack_require__) { var __WEBPACK_IMPORTED_MODULE_0__util_js__ = __webpack_require__(1); console.log(__WEBPACK_IMPORTED_MODULE_0__util_js__["a"]); }), (function (module, __webpack_exports__, __webpack_require__) { __webpack_exports__["a"] = ('Hello,Webpack'); }) ]
在開啓 Scope Hoisting 後,一樣的源碼輸出的部分代碼以下:
[ (function (module, __webpack_exports__, __webpack_require__) { var util = ('Hello,Webpack'); console.log(util); }) ]
從中能夠看出開啓 Scope Hoisting 後,函數申明由兩個變成了一個,util.js 中定義的內容被直接注入到了 main.js 對應的模塊中。
解決方案:webpack4 production mode會自動開啓ModuleConcatenationPlugin,實現做用域提高。
tree shaking 是一個術語,一般用於描述移除 JavaScript 上下文中的未引用代碼(dead-code)
有的時候,代碼裏或者引用的模塊裏包含裏一些沒被使用的代碼塊,打包的時候也被打包到最終的文件裏,增長了體積。這種時候,咱們可使用tree shaking技術來安全地刪除文件中未使用的部分。
使用方法:
在體積優化的路上,咱們可使用工具來分析咱們打包出的體積最終優化成怎樣的效果。
經常使用的工具備兩個:
使用須要在webpack.config.js中配置
plugins: [ new BundleAnalyzerPlugin() ]
執行完build後會打開網頁,效果圖以下:
source-map-explorer是根據source-map去分析出打包文件的體積大小,在本地調試是時設置 devtool: true
,而後執行source-map-explorer build/static/js/main.js
則能夠去分析指定js的體積。效果圖以下: