衆所周知,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/2016/12/08/happypack-source-code-analysis/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/code-splitting.html,裏面提到的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/guide/advanced/lazy-loading.html
示例
// 下面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/babel-plugin-import
示例
{
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的具體配置可參考 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技術來安全地刪除文件中未使用的部分。
使用方法:
在體積優化的路上,咱們可使用工具來分析咱們打包出的體積最終優化成怎樣的效果。 經常使用的工具備兩個:
plugins: [
new BundleAnalyzerPlugin()
]
複製代碼
執行完build後會打開網頁,效果圖以下:
devtool: true
,而後執行source-map-explorer build/static/js/main.js
則能夠去分析指定js的體積。效果圖以下: