最近對一個比較老的公司項目作了一次優化,處理的主要是webpack打包文件體積過大的問題。css
這裏就寫一下對於webpack打包優化的一些經驗。html
主要分爲如下幾個方面:node
本篇博客用到的webpack插件如何配置均可以去查看我寫的這篇博客:react
【Webpack的使用指南 02】Webpack的經常使用解決方案webpack
這裏就不細講這些配置了。ios
好比webpack中的devtool改成false,不須要熱加載這類只用於開發環境的東西。git
這些不算是優化,而算是錯誤了。github
對於在開發環境下才有用的東西在打包到生產環境時統統去掉。web
將樣式提取到單獨的css文件,而不是內嵌到打包的js文件中。npm
這樣帶來的好處時分離出來的css和js是能夠並行下載的,這樣能夠更快地加載樣式和腳本。
解決方案:
安裝ExtractTextPlugin
npm i --save-dev extract-text-webpack-plugin
而後修改webpack.config.js爲:
const ExtractTextPlugin = require('extract-text-webpack-plugin'); module.exports = { // ... plugins: [ // ... new ExtractTextPlugin({ filename: '[name].[contenthash].css', allChunks: false }), ], module: { rules: [ { test: /\.css$/, exclude: /node_modules/, use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: ['css-loader?modules', 'postcss-loader'], }), }, { test: /\.css$/, include: /node_modules/, use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: ['css-loader', 'postcss-loader'], }), }, { test: /\.less$/, use: ExtractTextPlugin.extract({ fallback: 'style-loader', use: ['css-loader?modules', 'less-loader', 'postcss-loader'], }), }, ], }, }
打包後生成文件以下:
這個東西不算是優化,而是讓咱們能夠清晰得看到各個包的輸出文件體積與交互關係。
安裝:
npm install --save-dev webpack-bundle-analyzer
而後修改webpack.config.js:
const { BundleAnalyzerPlugin } = require('webpack-bundle-analyzer'); module.exports = merge(common, { // ... plugins: [ new BundleAnalyzerPlugin({ analyzerPort: 8919 }) ], });
打包後會自動出現一個端口爲8919的站點,站點內容以下:
能夠看到咱們打包後的main.js中的代碼一部分來自node_modules文件夾中的模塊,一部分來自本身寫的代碼,也就是src文件夾中的代碼。
爲了以後描述方便,這個圖咱們直接翻譯過來就叫webpack打包分析圖。
所謂通用模塊,就是如react,react-dom,redux,axios幾乎每一個頁面都會應用到的js模塊。
將這些js模塊提取出來放到一個文件中,不只能夠縮小主文件的大小,在第一次下載的時候能並行下載,提升加載效率,更重要的是這些文件的代碼幾乎不會變更,那麼每次打包發佈後,仍然會沿用緩存,從而提升了加載效率。
而對於那些多文件入口的應用更是有效,由於在加載不一樣的頁面時,這部分代碼是公共的,直接能夠從緩存中應用。
這個東西不須要安裝,直接修改webpack的配置文件便可:
const webpack = require('webpack'); module.exports = { entry: { main: ['babel-polyfill', './src/app.js'], vendor: [ 'react', 'react-dom', 'redux', 'react-router-dom', 'react-redux', 'redux-actions', 'axios' ] }, plugins: [ new webpack.optimize.CommonsChunkPlugin({ names: ['vendor'], minChunks: Infinity, filename: 'common.bundle.[chunkhash].js', }) ] }
打包後的webpack打包分析圖爲:
能夠很明顯看到react這些模塊都被打包進了common.js中。
當咱們瞭解webpack中的hash值時,通常都會看到[hash]和[chunkhash]兩種hash值的配置。
其中hash根據每次編譯的內容計算獲得,因此每編譯一次全部文件都會生成一個新的hash,也就徹底沒法利用緩存。
因此咱們這裏用了[chunkhash],chunkhash是根據內容來生成的,因此若是內容不改變,那麼生成的hash值就不會改變。
chunkhash適用於通常的狀況,可是,對於咱們以上的狀況是不適用的。
我去改變主文件代碼,而後生成的兩個公共js代碼的chunkhash值卻改變了,它們並無使用到主文件。
因而我用文本對比工具,對比了它們的代碼,發現只有一行代碼是有差異的:
這是由於webpack在執行時會有一個帶有模塊標識的運行時代碼。
當咱們不提取vendor包的時候這段代碼會被打包到main.js文件中。
當咱們提取vendor到common.js時,這段腳本會被注入到common.js裏面,而main.js中沒有這段腳本了了.
當咱們將庫文件分爲兩個包提取出來,分別爲common1.js和common2.js,發現這段腳本只出如今一個common1.js中,而且
那段標識代碼變成了:
u.src=t.p+""+e+"."+{0:"9237ad6420af10443d7f",1:"be5ff93ec752c5169d4c"}
而後發現其餘包的首部都會有個這樣的代碼:
webpackJsonp([1],{2:functio
這個運行時腳本的代碼正好和其餘包開始的那段代碼中的數字相對應。
咱們能夠將這部分代碼提取到一個單獨的js中,這樣打包的公共js就不會受到影響。
咱們能夠進行以下配置:
plugins: [ new webpack.optimize.CommonsChunkPlugin({ names: ['vendor'], minChunks: Infinity, filename: 'common.bundle.[chunkhash].js', }), new webpack.optimize.CommonsChunkPlugin({ names: ['manifest'], filename: 'manifest.bundle.[chunkhash].js', }), new webpack.HashedModuleIdsPlugin() ]
對於names而言,若是chunk已經在entry中定義了,那麼就會根據entry中的入口提取chunk文件。若是沒有定義,好比mainifest,那麼就會生成一個空的chunk文件,來提取其餘全部chunk的公共代碼。
而咱們這段代碼的意思就是將webpack注入到包中的那段公共代碼提取出來。
打包後的文件:
webpack打包分析圖:
看到圖中綠色的那個塊了嗎?
那個東西就是打包後的manifest文件。
這樣處理後,當咱們再修改主文件中的代碼時,生成的公共js的chunkhash是不會改變的,改變的是那個單獨提取出來的manifest.bundle.[chunkhash].js的chunkhash。
這個其實不許備記錄進來,由於這些通常項目應該都具有了,不過這裏仍是順帶提一句吧。
壓縮js和css一步便可:
webpack -p
圖片的壓縮:
image-webpack-loader
具體的使用請查看 Webpack的經常使用解決方案 的第16點。
若是使用過Ant Design 通常都知道有一個配置按需加載的功能,就是在最後打包的時候只把用到的組件代碼打包。
而對於通常的react組件其實也有一個使用react-router實現按需加載的玩法。
對於每個路由而言,其餘路由的代碼實際上並非必須的,因此當切換到某一個路由後,若是隻加載這個路由的代碼,那麼首屏加載的速度將大大提高。
首先在webpack的output中配置
output: { // ... chunkFilename: '[name].[chunkhash:5].chunk.js', },
而後須要將react-router的加載改成按需加載,例如對於下面這樣的代碼:
import React from 'react'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; import ReactDOM from 'react-dom'; import { HashRouter as Router, Route } from 'react-router-dom'; import PageMain from './components/pageMain'; import PageSearch from './components/pageSearch'; import PageReader from './components/pageReader'; import reducer from './reducers'; const store = createStore(reducer); const App = () => ( <Provider store={store}> <Router> <div> <Route exact path="/" component={PageMain} /> <Route path="/search" component={PageSearch} /> <Route path="/reader/:bookid/:link" component={PageReader} /> </div> </Router> </Provider> );
應該改成:
import React from 'react'; import { createStore } from 'redux'; import { Provider } from 'react-redux'; import ReactDOM from 'react-dom'; import { HashRouter as Router, Route } from 'react-router-dom'; import reducer from './reducers'; const store = createStore(reducer); const PageMain = (location, callback) => { require.ensure([], require => { callback(null, require('./components/pageMain').default); }, 'PageMain'); }; const PageSearch = (location, callback) => { require.ensure([], require => { callback(null, require('./components/pageSearch').default); }, 'PageSearch'); }; const PageReader = (location, callback) => { require.ensure([], require => { callback(null, require('./components/pageReader').default); }, 'PageReader'); }; const App = () => ( <Provider store={store}> <Router> <div> <Route exact path="/" getComponent={PageMain} /> <Route path="/search" getComponent={PageSearch} /> <Route path="/reader/:bookid/:link" getComponent={PageReader} /> </div> </Router> </Provider> );
上面那種方法應用到react-router 4上是行不通的,由於getComponent方法已經被移除了。
而後我參考了官方教程的方法
在這裏咱們須要用到webpack, babel-plugin-syntax-dynamic-import和 react-loadable。
webpack內建了動態加載,可是咱們由於用到了babel,因此須要去用babel-plugin-syntax-dynamic-import避免作一些額外的轉換。
因此首先須要
npm i babel-plugin-syntax-dynamic-import --save-dev
而後在.babelrc加入配置:
"plugins": [ "syntax-dynamic-import" ]
接下來咱們須要用到react-loadable,它是一個用於動態加載組件的高階組件。
這是官網上的一個例子
import Loadable from 'react-loadable'; import Loading from './my-loading-component'; const LoadableComponent = Loadable({ loader: () => import('./my-component'), loading: Loading, }); export default class App extends React.Component { render() { return <LoadableComponent/>; } }
使用起來並不難,Loadable函數會傳入一個參數對象,返回一個渲染到界面上的組件。
這個參數對象的loader屬性就是須要動態加載的組件,而loading這個屬性傳入的是一個展現加載狀態的組件,當尚未加載出動態組件時,展現在界面上的就是這個loading組件。
使用這種方法相對於原來的方式優點很明顯,咱們不僅是在路由上能夠進行動態加載了,咱們動態加載的組件粒度能夠更細,好比一個時鐘組件,而不是像以前那樣每每是一個頁面。
經過靈活去使用動態加載能夠完美控制加載的js的大小,從而使首屏加載時間和其餘頁面加載時間控制到一個相對平衡的度。
這裏有個點須要注意,就是一般咱們在使用loading組件時常常會出現的問題:閃爍現象。
這種現象的緣由是,在加載真正的組件前,會出現loading頁面,可是組件加載很快,就會致使loading頁面出現的時間很短,從而形成閃爍。
解決的方法就是加個屬性delay
const LoadableComponent = Loadable({ loader: () => import('./my-component'), loading: Loading, delay: 200 });
只有當加載時間大於200ms時loading組件纔會出現。
還有更多的關於react-loadable的玩法:https://www.npmjs.com/package/react-loadable
那麼如今看下咱們的打包文件:
webpack打包分析圖:
注意看看上面的打包文件名字,發現經過這種方法進行按需加載的幾個文件都是按照數字命名,而沒有按照咱們指望的組件名命名。
我在這個項目的github上面找了一下,發現它提供的按組件命名的方法須要用到服務端渲染,而後就沒有繼續下去了。
反正這個東西也不是很重要,因此就沒有進一步深究,若是有園友對這個問題有好的辦法,也但願能在評論裏說明。
React這個版本新加了lazy和Suspense這兩個功能。
對於上面的按需加載,能夠修改代碼爲:
import React, { Suspense } from 'react'; import Loading from './my-loading-component'; const LoadableComponent = React.lazy(() => import('./my-component')); export default class App extends React.Component { render() { return ( <Suspense fallback={<Loading />}> <LoadableComponent/>; </Suspense> ) } }
臨時更新,寫得簡單點,見諒!
總的來說,經過以上步驟應該是能夠解決絕大多數打包文件體積過大的問題。
固然,由於文中webpack版本和插件版本的差別,在配置和玩法上會有一些不一樣,可是上面描述的這些方向都是沒有問題的,而且相信在各個版本下均可以找到相應的解決方案。
文中若有疑誤,請不吝賜教。