Webpack是當下最熱門的前端資源模塊強大打包工具。它能夠將許多鬆散的模塊按照依賴和規則打包成符合生成環境部署的前端資源;還能夠將按需加載的模塊進行代碼分割,等到實際須要再異步加載。在使用webpack時,若是不注意性能優化,很是大的可能產生性能問題。性能問題主要分爲開發時構建速度慢、開發調試時的重複工做、輸出打包文件過大等,所以優化啊方案也主要針對這些方面來分析得出。css
webpack打包,首先根據entry配置的入口出發,遞歸遍歷解析所依賴的文件。這個過程分爲搜索文件和匹配文件進行分析、轉化的2個過程,所以能夠從這兩個角度來進行優化配置。html
減少文件搜索的優化配置以下:前端
modules: [path.resolve(__dirname, "src"), "node_modules"]
複製代碼
設置resolve.mainFields:['main'],設置儘可能少的值,能夠減小入口文件的搜索解析node
webpack打包Node應用程序默認會從module開始解析,resolve.mainFields默認值爲:react
mainFields: ["module", "main"]
複製代碼
第三方模塊爲了適應不一樣的使用環境,會定義多個入口文件。咱們能夠設置mainFields統一第三方模塊的入口文件main,減小搜索解析。(大多數第三方模塊都使用main字段描述入口文件的位置)jquery
resolve.alias:{
'react':patch.resolve(__dirname, './node_modules/react/dist/react.min.js')
}
複製代碼
這樣會影響Tree-Shaking,適合對總體性比較強的庫使用,若是是像lodash這類工具類的比較分散的庫,比較適合Tree-Shaking,避免使用這種方式。webpack
默認值:resolve.extensions: ['.js', '.json'],當引入語句沒帶文件後綴時,webpack會根據extensions定義的後綴列表進行查找,因此: - 列表值儘可能少 - 頻率高的文件後綴寫在前面 - 代碼中引入語句儘量地帶上文件後綴,好比require('./data')改寫成require('./data.json')。git
好比 JQuery、React,另外若是使用resolve.alias配置了react.min.js,則應該排除解析,由於react.min.js通過構建,已是能夠直接運行在瀏覽器的、非模塊化的文件。github
module: {
noParse: [/jquery|lodash, /react\.min\.js$/]
}
複製代碼
有殊途同歸的效果,就是使用externals外部擴展,剝離第三方依賴模塊(如jquery、react、echarts等),不打包到bundle.js。web
module: {
loaders: [{
test: /\.js$/,
loader: 'babel-loader',
include: [
path.resolve(__dirname, "app/src"),
path.resolve(__dirname, "app/test")
],
exclude: /node_modules/
}]
}
複製代碼
使用DllPlugin動態連接庫插件,大量複用的模塊只用編譯一次,其原理是把網頁依賴的基礎模塊抽離出來打包到dll文件中,當須要導入模塊存在於某個dll中,這個模塊再也不打包,直接從dll中獲取。我認爲使用DllPlugin連接第三方模塊,和配置resolve.alias和module.noParse的效果有殊途同歸之處。
使用方法:
1)使用DllPlugin插件,配置webpack_dll.config.js來構建dll文件:
const path = require('path');
const DllPlugin = require('webpack/lib/DllPlugin');
module.exports = {
entry: {
// 把 React 相關模塊的放到一個單獨的動態連接庫
react: ['react', 'react-dom'],
// 把項目須要全部的 polyfill 放到一個單獨的動態連接庫
polyfill: ['core-js/fn/promise', 'whatwg-fetch']
},
output: {
// 輸出的動態連接庫的文件名稱
filename: '[name].dll.js',
// 輸出的文件都放到 dist 目錄下
path: path.resolve(__dirname, 'dist'),
// 存放動態連接庫的全局變量名稱,例如對應 react 來講就是 _dll_react
// 之因此在前面加上 _dll_ 是爲了防止全局變量衝突
library: '_dll_[name]'
},
plugins: [
new DllPlugin({
// 動態連接庫的全局變量名稱
name: '_dll_[name]',
// 描述動態連接庫的 manifest.json 文件輸出時的文件名稱
path: path.join(__dirname, 'dist', '[name].manifest.json')
})
]
}
複製代碼
構建輸出的如下這四個文件:
├── polyfill.dll.js
├── polyfill.manifest.json
├── react.dll.js
└── react.manifest.json
2)在主config文件,使用DllReferencePlugin插件引入xx.manifest.json動態連接庫文件:
const path = require('path');
const DllReferencePlugin = require('webpack/lib/DllReferencePlugin');
module.exports = {
entry: {
main: './main.js'
},
// ... 省略output和loader配置
plugins: [
new DllReferencePlugin({
// 描述 react 動態連接庫的文件內容
manifest: require('./dist/react.manifest.json'),
}),
new DllReferencePlugin({
// 描述 polyfill 動態連接庫的文件內容
manifest: require('./dist/polyfill.manifest.json'),
})
]
}
複製代碼
ParallelUglifyPlugin插件能夠開啓多個子進程,每一個子進程使用uglifyJsPlugin壓縮代碼,能夠並行執行,能顯著縮短壓縮代碼時間。
使用方法:
1)安裝 webpack-parallel-uglify-plugin 插件:
npm install -D webpack-parallel-uglify-plugin
複製代碼
2)而後在webpack.config.js 配置代碼以下:
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
module.exports = {
plugins: [
new ParallelUglifyPlugin({
uglifyJS: {
// 這裏放uglifyJs的參數
}
})
]
}
複製代碼
html-webpack-plugin插件,給html文件載入時添加loading圖。使用方法以下:
1)安裝 html-webpack-plugin 插件:
npm install -D html-webpack-plugin
複製代碼
2)webpack.config.js配置以下:
const HtmlWebpackPlugin = require('html-webpack-plugin');
const loading = require('./render-loading');// 事先設計好的loading圖
module.exports = {
plugins: [
new HtmlWebpackPlugin({
template: './src/index.html',
loading: loading
})
]
}
複製代碼
prerender-spa-plugin插件,預渲染極大地提升了首屏加載速度。其原理是此插件在本地模擬瀏覽器環境,預先執行咱們打包的文件,返回預先解析的首屏html。使用方法入以下:
1)安裝 prerender-spa-plugin 插件:
npm install -D prerender-spa-plugin
複製代碼
2)webpack.config.js配置以下:
const PrerenderSPAPlugin = require('prerender-spa-plugin');
module.exports = {
plugins: [
new PrerenderSPAPlugin({
// 生成文件的路徑,也能夠與webpakc打包的一致。
staticDir: path.join(__dirname, '../dist'),
// 要預渲染的路由
route: [ '/', '/team', '/analyst','/voter','/sponsor'],
// 這個很重要,若是沒有配置這段,也不會進行預編譯
renderer: new Renderer({
headless: false,
renderAfterDocumentEvent: 'render-active',
// renderAfterTime: 5000
})
})
]
}
複製代碼
3)項目入口文件main.js啓動預渲染:
/* eslint-disable no-new */
new Vue({
el: '#app',
router,
store,
i18n,
components: { App },
template: '<App/>',
render: h => h(App),
/* 這句很是重要,不然預渲染將不會啓動 */
mounted () {
document.dispatchEvent(new Event('render-active'))
}
})
複製代碼
會分析JS代碼語法樹,理解代碼的含義,從而去掉無效代碼、日誌輸出代碼,縮短變量名,進行壓縮等優化。使用UglifyJSPlug配置webpack.config.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, //刪除全部註釋
}
})
]
複製代碼
現在愈來愈多的瀏覽器支持直接執行ES6代碼了,這樣比起轉換後的ES5代碼量更少,且性能更好。直接運行的ES6代碼,也是須要代碼壓縮,第三方uglify-webpack-plugin提供了壓縮ES6代碼的功能,使用方法以下:
a、安裝uglify-webpack-plugin插件:
uglify-webpack-plugin
複製代碼
b、webpack.config.js配置以下:
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
}
}
})
]
複製代碼
另外要防止babel-loader轉換ES6代碼,要在.babelrc中去掉babel-preset-env,由於正是babel-preset-env負責把ES6轉換爲ES5。
將js裏面分離出來的多個css合併成一個,而後進行壓縮、去重等處理。
a、安裝引入mini-css-extract-plugin、optimize-css-assets-webpack-plugin插件
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
const OptimizeCSSAssetsPlugin = require('optimize-css-assets-webpack-plugin')
複製代碼
b、配置loader
module: {
rules: [
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: {
plugins: [
require('postcss-import')(),
require('autoprefixer')({
browsers: ['last 30 versions', "> 2%", "Firefox >= 10", "ie 6-11"]
})
]
}
}
]
}
]
}
複製代碼
c、將多個css文件合併成單一css文件
主要是針對多入口,會產生多分樣式文件,合併成一個樣式文件,減小加載次數 配置以下
optimization:{
splitChunks: {
chunks: 'all',
minSize: 30000,
minChunks: 1,
maxAsyncRequests: 5,
maxInitialRequests: 3,
name: true,
cacheGroups: {
styles: {
name: 'style',
test: /\.css$/,
chunks: 'all',
enforce: true
}
}
}
}
複製代碼
- filename 與output中的filename 命名方式同樣
- 這裏是將多個css合併成單一css文件, 因此chunkFilename 不用處理
- 最後產生的樣式文件名大概張這樣 style.550f4.css ;style 是 splitChunks-> cacheGroups-> name
new MiniCssExtractPlugin({
filename: 'assets/css/[name].[hash:5].css'
})
複製代碼
d、優化css文件,去重壓縮等處理
- 主要使用 optimize-css-assets-webpack-plugin 插件和 cssnano 優化器
- cssnano 優化器具體作了哪些優化,可參考 官網
配置方式有兩種,效果等同。
方式一:
module.exports = {
optimization:{
minimizer: [
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
// cssProcessorOptions: cssnanoOptions,
cssProcessorPluginOptions: {
preset: ['default', {
// 對註釋的處理
discardComments: {
removeAll: true,
},
// 建議設置爲false,不然在使用 unicode-range 的時候會產生亂碼
normalizeUnicode: false
}]
},
// 是否打印處理過程當中的日誌
canPrint: true
})
]
}
}
複製代碼
方式二:
module.exports = {
plugins:[
new OptimizeCSSAssetsPlugin({
assetNameRegExp: /\.css$/g,
cssProcessor: require('cssnano'),
// cssProcessorOptions: cssnanoOptions,
cssProcessorPluginOptions: {
preset: ['default', {
discardComments: {
removeAll: true,
},
normalizeUnicode: false
}]
},
canPrint: true
})
]
}
複製代碼
Tree Shaking能夠剔除用不上的死代碼,它依賴ES6的import、export的模塊化語法,最早在Rollup中出現,Webpack 2.0將其引入。適合用於Lodash、utils.js等工具類較分散的文件。它正常工做的前提是代碼必須採用ES6的模塊化語法,由於ES6模塊化語法是靜態的(在導入、導出語句中的路徑必須是靜態字符串,且不能放入其餘代碼塊中)。若是採用了ES5中的模塊化,例如:module.export = {...}、require( x+y )、if (x) { require( './util' ) },則Webpack沒法分析出能夠剔除哪些代碼。
如何啓用Tree Shaking:
{
"presets": [
[
"env",
{ "module": false }, //關閉Babel的模塊轉換功能,保留ES6模塊化語法
]
]
}
複製代碼
prepack-webpack-plugin插件能提早計算,代碼運行時直接獲取結果,提高代碼運行速度。其原理是,編譯代碼時提早將計算結果放到編譯後的代碼中,而不是運行時纔去求值計算,運行代碼時直接將運算結果輸出以提高性能。prepack的使用方法:
1)安裝prepack-webpack-plugin插件:
npm install -D prepack-webpack-plugin
複製代碼
2)webpack.config.js配置以下:
const PrepackWebpackPlugin = require('prepack-webpack-plugin').default;
const configuration = {};
module.exports = {
// ...
plugins: [
new PrepackWebpackPlugin(configuration)
]
};
複製代碼
Scope Hoisting是Webpack3.x內置的功能,它分析模塊間的依賴關係,儘量將被打散的模塊合併到一個函數中,但不能形成代碼冗餘,因此只有被引用一次的模塊才能被合併。因爲須要分析模塊間的依賴關係,因此項目代碼中需使用ES6模塊化,不然Webpack會降級處理不採用Scope Hoisting。Scope Hoisting的使用配置以下:
const ModuleConcatenationPlugin = require('webpack/lib/optimize/ModuleConcatenationPlugin');
module.exports = {
// ...
plugins: [
new ModuleConcatenationPlugin()
]
}
複製代碼
CDN經過將資源部署到世界各地,使得用戶能夠就近訪問資源,加快訪問速度。要接入CDN,須要把網頁的靜態資源上傳到CDN服務上,在訪問這些資源時,使用CDN服務提供的URL。
因爲CDN會爲資源開啓長時間的緩存,例如用戶從CDN獲取index.html,即便以後替換了index.html,用戶那邊仍會在使用以前的版本直到緩存時間過時。業界的作法:
dist
|-- app_9d89c964.js
|-- app_a6976b6d.css
|-- arch_ae805d49.png
|-- index.html
複製代碼
另外,HTTP1.x版本的協議下,瀏覽器會對於同一個域名並行發起的請求限制在4-8個。那麼把全部靜態資源放在同一域名下的CDN服務上就會遇到限制,因此能夠把靜態資源分散在不一樣的CDN服務上,例如JS文件放在js.cdn.com域名下,CSS文件放在css.cdn.com域名,圖片文件放在img.cdn.com域名下。使用了多個域名後又會帶來一個新問題:增長域名解析時間。是否採用多域名分散資源須要根據本身的需求去衡量得失。 固然你能夠經過在HTML HEAD標籤中加入<link rel="dns-prefetch" href="//js.cdn.com">
去預解析域名,以下降域名解析帶來的延遲。
Webpack接入CDN主要的配置以下:
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const {WebPlugin} = require('web-webpack-plugin');
module.exports = {
// 省略 entry 配置...
output: {
// 給輸出的 JavaScript 文件名稱加上 Hash 值
filename: '[name]_[chunkhash:8].js',
path: path.resolve(__dirname, './dist'),
// 指定存放 JavaScript 文件的 CDN 目錄 URL
publicPath: '//js.cdn.com/id/',
},
module: {
rules: [
{
// 增長對 CSS 文件的支持
test: /\.css$/,
// 提取出 Chunk 中的 CSS 代碼到單獨的文件中
use: ExtractTextPlugin.extract({
// 壓縮 CSS 代碼
use: ['css-loader?minimize'],
// 指定存放 CSS 中導入的資源(例如圖片)的 CDN 目錄 URL
publicPath: '//img.cdn.com/id/'
}),
},
{
// 增長對 PNG 文件的支持
test: /\.png$/,
// 給輸出的 PNG 文件名稱加上 Hash 值
use: ['file-loader?name=[name]_[hash:8].[ext]'],
},
// 省略其它 Loader 配置...
]
},
plugins: [
// 使用 WebPlugin 自動生成 HTML
new WebPlugin({
// HTML 模版文件所在的文件路徑
template: './template.html',
// 輸出的 HTML 的文件名稱
filename: 'index.html',
// 指定存放 CSS 文件的 CDN 目錄 URL
stylePublicPath: '//css.cdn.com/id/',
}),
new ExtractTextPlugin({
// 給輸出的 CSS 文件名稱加上 Hash 值
filename: `[name]_[contenthash:8].css`,
}),
// 省略代碼壓縮插件配置...
],
};
複製代碼
大型網站一般是由多個頁面組成,確定會依賴一樣的樣式文件、腳本文件等。若是不把這些公共文件提取出來,那麼每一個單頁打包出來的chunck中都會包含公共代碼,至關於要傳輸n份重複代碼。若是把公共代碼提取成一個文件,那麼當用戶訪問了一個網頁加載了這個公共文件,再訪問其餘依賴公共文件的網頁,就直接使用文件在瀏覽器的緩存,不用重複加載請求。
a、把多個頁面依賴的公共代碼提取到common.js
const CommonsPlugin = require('webpack/lib/optimize/CommonsChunkPlugin');
module.exports = {
plugins:[
new CommonsChunkPlugin({
chunks:['a','b'], //從哪些chunk中提取
name:'common', // 提取出的公共部分造成一個新的chunk
})
]
}
複製代碼
b、找出依賴的基礎庫,寫一個base.js文件,再與common.js提取公共代碼到base中,common.js就剔除了基礎庫代碼,而base.js保持不變。
//base.js
import 'react';
import 'react-dom';
import './base.css';
//webpack.config.json
module.exports = {
entry:{
base: './base.js'
},
plugins:[
new CommonsChunkPlugin({
chunks:['base','common'],
name:'base',
//minChunks:2, 表示文件要被提取出來須要在指定的chunks中出現的最小次數,防止common.js中沒有代碼的狀況
})
]
}
複製代碼
c、獲得基礎庫代碼base.js,不含基礎庫的公共代碼common.js,和頁面各自的代碼文件xx.js。
頁面引用順序以下:base.js--> common.js--> xx.js
在webpack編譯完以後,你可能會注意到有一些很小的 chunk - 這產生了大量 HTTP 請求開銷。幸運的是使用LimitChunkCountPlugin插件能夠經過合併的方式,處理 chunk,以減小http請求數。
const LimitChunkCountPlugin = require('webpack/lib/optimize/LimitChunkCountPlugin');
module.exports = {
// ...
plugins: [
new LimitChunkCountPlugin({
// 限制 chunk 的最大數量,必須大於或等於1的值
maxChunks: 10,
// 設置 chunk 的最小大小
minChunkSize: 2000
})
]
}
複製代碼
監聽文件有兩種方式:
方式一:
在配置文件 webpack.config.js 中設置 watch: true。
// 從配置的 Entry 文件出發,遞歸解析出 Entry 文件所依賴的文件,
// 把這些依賴的文件加入到監聽列表
// 而不是直接監聽項目目錄下的全部文件
module.export = {
// 只有在開啓監聽模式時,watchOptions 纔有意義
// 默認爲 false,也就是不開啓
watch: true,
// 監聽模式運行時的參數
// 在開啓監聽模式時,纔有意義
watchOptions: {
// 不監聽的文件或文件夾,支持正則匹配
// 默認爲空
ignored: /node_modules/,
// 在 Webpack 中監聽一個文件發生變化的原理是定時的不停的去獲取文件的最後編輯時間,
// 每次都存下最新的最後編輯時間,若是發現當前獲取的和最後一次保存的最後編輯時間不一致,
// 就認爲該文件發生了變化。
// poll 就是用於控制定時檢查的週期,具體含義是每隔多少毫秒檢查一次
// 默認每隔1000毫秒詢問一次
poll: 1000,
// 監聽到文件發生變化時,webpack 並不會馬上告訴監聽者,
// 而是先緩存起來,收集一段時間的變化後,再一次性告訴監聽者
// aggregateTimeout 就是用於配置這個等待時間,
// 目的是防止文件更新太快致使從新編譯頻率過高,讓程序構建卡死
// 默認爲 300ms
aggregateTimeout: 300,
// 不監聽的 node_modules 目錄下的文件
ignored: /node_modules/,
}
}
複製代碼
方式二:
在執行啓動 Webpack 命令時,帶上 --watch 參數,完整命令是 webpack --watch。
方式一:webpack-dev-server
在使用 webpack-dev-server 模塊去啓動 webpack 模塊時,webpack 模塊的監聽模式默認會被開啓。 webpack 模塊會在文件發生變化時告訴 webpack-dev-server 模塊。
方式二:koa + webpack-dev-middleware + webpack-hot-middleware先後端同構
模塊熱替換(HMR - Hot Module Replacement)功能會在應用程序運行過程當中替換、添加或刪除模塊,而無需從新加載整個頁面,因此預覽反應更快,等待時間更少。原理是向每一個chunk中注入代理客戶端來鏈接DevServer和網頁。開啓方式:
const HotModuleReplacementPlugin = require('webpack/lib/HotModuleReplacementPlugin');
module.exports = {
plugins: [
new HotModuleReplacementPlugin()
]
}
複製代碼
開啓後,若是修改子模塊就能夠實現局部刷新,但若是修改的是根JS文件,會整個頁面刷新。緣由在於,子模塊更新時,事件一層層向上傳遞,直到某個層的文件接收了當前變化的模塊,而後執行回調函數。若是一層層向外拋到最外層都沒有文件接收,就會刷新整頁。
永遠不要在生產環境(production)下啓用 HMR。
在使用webpack構建前端項目中,逐漸暴露出一些性能問題,其主要有以下幾個方面:
針對如上問題,上述webpack優化方案便派上用場了。做爲開發工程師,咱們要不斷追求項目工程高性能,秉持「什麼方案解決什麼問題」的準則,針對實際開發項目,持續改進優化項目性能,不斷提高開發效率、下降資源成本。