webpack2終極優(yōu)化
webpack是當(dāng)下***的js打包工具,這得益于網(wǎng)頁應(yīng)用日益復(fù)雜和js模塊化的流行。webpack2增加了一些新特性也正式發(fā)布了一段時(shí)間,是時(shí)候告訴大家如何用webpack2優(yōu)化你的構(gòu)建讓它構(gòu)建出更小的文件尺寸和更好的開發(fā)體驗(yàn)。
優(yōu)化輸出
打包結(jié)果更小可以讓網(wǎng)頁打開速度更快以及簡約寬帶??梢酝ㄟ^這以下幾點(diǎn)做到
壓縮css
css-loader 在webpack2里默認(rèn)是沒有開啟壓縮的,***生成的css文件里有很多空格和tab,通過配置
css-loader?minimize參數(shù)可以開啟壓縮輸出最小的css。css的壓縮實(shí)際是是通過cssnano實(shí)現(xiàn)的。
tree-shaking
tree-shaking 是指借助es6 import export 語法靜態(tài)性的特點(diǎn)來刪掉export但是沒有import過的東西。要讓tree-shaking工作需要注意以下幾點(diǎn):
- 配置babel讓它在編譯轉(zhuǎn)化es6代碼時(shí)不把import export轉(zhuǎn)換為cmd的module.export,配置如下:
- "presets": [
- [
- "es2015",
- {
- "modules": false
- }
- ]
- ]
- 大多數(shù)分布到npm的庫里的代碼都是es5的,但是也有部分庫(redux,react-router等等)開始支持tree-shaking。這些庫發(fā)布到npm里的代碼即包含es5的又包含全采用了es6 import export 語法的代碼。
拿redux庫來說,npm下載到的目錄結(jié)構(gòu)如下:
- ├── es
- │ └── utils
- ├── lib
- │ └── utils
其中l(wèi)ib目錄里是編譯出的es5代碼,es目錄里是編譯出的采用import export 語法的es5代碼,在redux的package.json文件里有這兩個(gè)配置:
- main": "lib/index.js",
- "jsnext:main": "es/index.js",
這是指這個(gè)庫的入口文件的位置,所以要讓webpack去讀取es目錄下的代碼需要使用jsnext:main字段配置的入口,要做到這點(diǎn)webpack需要這樣配置:
- module.exports = {
- resolve: {
- mainFields: ['jsnext:main','main'],
- }
- };
這會(huì)讓webpack先使用jsnext:main字段,在沒有時(shí)使用main字段。這樣就可以優(yōu)化支持tree-shaking的庫。
優(yōu)化 UglifyJsPlugin
webpack --optimize-minimize 選項(xiàng)會(huì)開啟 UglifyJsPlugin來壓縮輸出的js,但是默認(rèn)的UglifyJsPlugin配置并沒有把代碼壓縮到最小輸出的js里還是有注釋和空格,需要覆蓋默認(rèn)的配置:
- new UglifyJsPlugin({
- // 最緊湊的輸出
- beautify: false,
- // 刪除所有的注釋
- comments: false,
- compress: {
- // 在UglifyJs刪除沒有用到的代碼時(shí)不輸出警告
- warnings: false,
- // 刪除所有的 `console` 語句
- // 還可以兼容ie瀏覽器
- drop_console: true,
- // 內(nèi)嵌定義了但是只用到一次的變量
- collapse_vars: true,
- // 提取出出現(xiàn)多次但是沒有定義成變量去引用的靜態(tài)值
- reduce_vars: true,
- }
- })
定義環(huán)境變量 NODE_ENV=production
很多庫里(比如react)有部分代碼是這樣的:
- if(process.env.NODE_ENV !== 'production'){
- // 不是生產(chǎn)環(huán)境才需要用到的代碼,比如控制臺(tái)里看到的警告
- }
在環(huán)境變量 NODE_ENV 等于 production 的時(shí)候UglifyJs會(huì)認(rèn)為if語句里的是死代碼在壓縮代碼時(shí)刪掉。
使用 CommonsChunkPlugin 抽取公共代碼
CommonsChunkPlugin可以提取出多個(gè)代碼塊都依賴的模塊形成一個(gè)單獨(dú)的模塊。要發(fā)揮CommonsChunkPlugin的作用還需要瀏覽器緩存機(jī)制的配合。在應(yīng)用有多個(gè)頁面的場(chǎng)景下提取出所有頁面公共的代碼減少單個(gè)頁面的代碼,在不同頁面之間切換時(shí)所有頁面公共的代碼之前被加載過而不必重新加載。這個(gè)方法可以非常有效的提升應(yīng)用性能。
在生產(chǎn)環(huán)境按照文件內(nèi)容md5打hash
webpack編譯在生產(chǎn)環(huán)境出來的js、css、圖片、字體這些文件應(yīng)該放到CDN上,再根據(jù)文件內(nèi)容的md5命名文件,利用緩存機(jī)制用戶只需要加載一次,第二次加載時(shí)就直接訪問緩存。如果你之后有修改就會(huì)為對(duì)應(yīng)的文件生產(chǎn)新的md5值。做到以上你需要這樣配置:
- {
- output: {
- publicPath: CND_URL,
- filename: '[name]_[chunkhash].js',
- },
- }
知道以上原理后我們還可以進(jìn)一步優(yōu)化:利用CommonsChunkPlugin提取出使用頁面都依賴的基礎(chǔ)運(yùn)行環(huán)境。比如對(duì)于最常見的react體系你可以抽出基礎(chǔ)庫react react-dom redux react-redux到一個(gè)單獨(dú)的文件而不是和其它文件放在一起打包為一個(gè)文件,這樣做的好處是只要你不升級(jí)他們的版本這個(gè)文件永遠(yuǎn)不會(huì)被刷新。如果你把這些基礎(chǔ)庫和業(yè)務(wù)代碼打包在一個(gè)文件里每次改動(dòng)業(yè)務(wù)代碼都會(huì)導(dǎo)致瀏覽器重復(fù)下載這些包含基礎(chǔ)庫的代碼。以上的配置為:
- // vender.js 文件抽離基礎(chǔ)庫到單獨(dú)的一個(gè)文件里防止跟隨業(yè)務(wù)代碼被刷新
- // 所有頁面都依賴的第三方庫
- // react基礎(chǔ)
- import 'react';
- import 'react-dom';
- import 'react-redux';
- // redux基礎(chǔ)
- import 'redux';
- import 'redux-thunk';
- // webpack配置
- {
- entry: {
- vendor: './path/to/vendor.js',
- },
- }
DedupePlugin 和 OccurrenceOrderPlugin
在webpack1里經(jīng)常會(huì)使用 DedupePlugin 插件來消除重復(fù)的模塊以及使用 OccurrenceOrderPlugin 插件讓被依賴次數(shù)更高的模塊靠前分到更小的id 來達(dá)到輸出更少的代碼,在webpack2里這些已經(jīng)這兩個(gè)插件已經(jīng)被移除了因?yàn)檫@些功能已經(jīng)被內(nèi)置了。
除了壓縮文本代碼外還可以:
- 用imagemin-webpack-plugin 壓縮圖片
- 用webpack-spritesmith 合并雪碧圖
- 對(duì)于支持es6的js運(yùn)行環(huán)境使用babili
以上優(yōu)化點(diǎn)只需要在構(gòu)建用于生產(chǎn)環(huán)境代碼的時(shí)候才使用,在開發(fā)環(huán)境時(shí)***關(guān)閉因?yàn)樗鼈兒芎臅r(shí)。
優(yōu)化開發(fā)體驗(yàn)
優(yōu)化開發(fā)體驗(yàn)主要從更快的構(gòu)建和更方便的功能入手。
更快的構(gòu)建
縮小文件搜索范圍
webpack的resolve.modules配置模塊庫(通常是指node_modules)所在的位置,在js里出現(xiàn)import 'redux'這樣不是相對(duì)也不是絕對(duì)路徑的寫法時(shí)會(huì)去node_modules目錄下找。但是默認(rèn)的配置會(huì)采用向上遞歸搜索的方式去尋找node_modules,但通常項(xiàng)目目錄里只有一個(gè)node_modules在項(xiàng)目根目錄,為了減少搜索我們直接寫明node_modules的全路徑:
- module.exports = {
- resolve: {
- modules: [path.resolve(__dirname, 'node_modules')]
- }
- };
除此之外webpack配置loader時(shí)也可以縮小文件搜索范圍。
- loader的test正則表達(dá)式也應(yīng)該盡可能的簡單,比如在你的項(xiàng)目里只有.js文件時(shí)就不要把test寫成/\.jsx?$/
- loader使用include***只需要處理的文件,比如babel-loader的這兩個(gè)配置:
只對(duì)項(xiàng)目目錄下src目錄里的代碼進(jìn)行babel編譯
- {
- test: /\.js$/,
- loader: 'babel-loader',
- include: path.resolve(__dirname, 'src')
- }
項(xiàng)目目錄下的所有js都會(huì)進(jìn)行babel編譯,包括龐大的node_modules下的js
- {
- test: /\.js$/,
- loader: 'babel-loader'
- }
開啟 babel-loader 緩存
babel編譯過程很耗時(shí),好在babel-loader提供緩存編譯結(jié)果選項(xiàng),在重啟webpack時(shí)不需要?jiǎng)?chuàng)新編譯而是復(fù)用緩存結(jié)果減少編譯流程。babel-loader緩存機(jī)制默認(rèn)是關(guān)閉的,打開的配置如下:
- module.exports = {
- module: {
- loaders: [{
- test: /\.js$/,
- loader: 'babel-loader?cacheDirectory',
- }]
- }
- };
使用 alias
resolve.alias 配置路徑映射。
發(fā)布到npm的庫大多數(shù)都包含兩個(gè)目錄,一個(gè)是放著cmd模塊化的lib目錄,一個(gè)是把所有文件合成一個(gè)文件的dist目錄,多數(shù)的入口文件是指向lib里面下的。
默認(rèn)情況下webpack會(huì)去讀lib目錄下的入口文件再去遞歸加載其它依賴的文件這個(gè)過程很耗時(shí),alias配置可以讓webpack直接使用dist目錄的整體文件減少文件遞歸解析。配置如下:
- module.exports = {
- resolve: {
- alias: {
- 'moment': 'moment/min/moment.min.js',
- 'react': 'react/dist/react.js',
- 'react-dom': 'react-dom/dist/react-dom.js'
- }
- }
- };
使用 noParse
module.noParse 配置哪些文件可以脫離webpack的解析。
有些庫是自成一體不依賴其他庫的沒有使用模塊化的,比如jquey、momentjs、chart.js,要使用它們必須整體全部引入。
webpack是模塊化打包工具完全沒有必要去解析這些文件的依賴,因?yàn)樗鼈兌疾灰蕾嚻渌募w積也很龐大,要忽略它們配置如下:
- module.exports = {
- module: {
- noParse: /node_modules\/(jquey|moment|chart\.js)/
- }
- };
除此以外還有很多可以加速的方法:
- 使用happypack多進(jìn)程并行構(gòu)建
- 使用DllPlugin復(fù)用模塊
更方便的功能
模塊熱替換
模塊熱替換是指在開發(fā)的過程中修改代碼后不用刷新頁面直接把變化的模塊替換到老模塊讓頁面呈現(xiàn)出***的效果。
webpack-dev-server內(nèi)置模塊熱替換,配置起來也很方便,下面以react應(yīng)用為例,步驟如下:
- 在啟動(dòng)webpack-dev-server的時(shí)候帶上--hot參數(shù)開啟模塊熱替換,在開啟--hot后針對(duì)css的變化是會(huì)自動(dòng)熱替換的,但是js涉及到復(fù)雜的邏輯還需要進(jìn)一步配置。
- 配置頁面入口文件
- import App from './app';
- function run(){
- render(<App/>,document.getElementById('app'));
- }
- run();
- // 只在開發(fā)模式下配置模塊熱替換
- if (process.env.NODE_ENV !== 'production') {
- module.hot.accept('./app', run);
- }
當(dāng)./app發(fā)生變化或者當(dāng)./app依賴的文件發(fā)生變化時(shí)會(huì)把./app編譯成一個(gè)模塊去替換老的,替換完畢后重新執(zhí)行run函數(shù)渲染出***的效果。
自動(dòng)生成html
webpack只做了資源打包的工作還缺少把這些加載到html里運(yùn)行的功能,在龐大的app里手寫html去加載這些資源是很繁瑣易錯(cuò)的,我們需要自動(dòng)正確的加載打包出的資源。
webpack原生不支持這個(gè)功能于是我做了一個(gè)插件 web-webpack-plugin
具體使用點(diǎn)開鏈接看詳細(xì)文檔,使用大概如下:
demo(https://github.com/gwuhaolin/web-webpack-plugin/tree/master/demo/out-html)
webpack配置
- module.exports = {
- entry: {
- A: './a',
- B: './b',
- },
- plugins: [
- new WebPlugin({
- // 輸出的html文件名稱,必填,注意不要重名,重名會(huì)覆蓋相互文件。
- filename: 'index.html',
- // 該html文件依賴的entry,必須是一個(gè)數(shù)組。依賴的資源的注入順序按照數(shù)組的順序。
- requires: ['A', 'B'],
- }),
- ]
- };
將會(huì)輸出一個(gè)index.html文件,這個(gè)文件將會(huì)自動(dòng)引入 entry A 和 B 生成的js文件,
輸出的html:
- <html>
- <head>
- <meta charset="UTF-8">
- </head>
- </body>
- <script src="A.js"></script>
- <script src="B.js"></script>
- </body>
- </html>
輸出的目錄結(jié)構(gòu)
- ├── A.js
- ├── B.js
- └── index.html
管理多頁面
雖然webpack適用于單頁應(yīng)用,但復(fù)雜的系統(tǒng)經(jīng)常是由多個(gè)單頁應(yīng)用組成,每個(gè)頁面一個(gè)功能模塊。webpack給出了js打包方案但缺少管理多個(gè)頁面的功能。 web-webpack-plugin的AutoWebPlugin會(huì)自動(dòng)的為你的系統(tǒng)里每個(gè)單頁應(yīng)用生成一個(gè)html入口頁,這個(gè)入口會(huì)自動(dòng)的注入當(dāng)前單頁應(yīng)用依賴的資源,使用它你只需如下幾行代碼:
- plugins: [
- // ./src/pages/ 代表存放所有頁面的根目錄,這個(gè)目錄下的每一個(gè)目錄被看著是一個(gè)單頁應(yīng)用
- // 會(huì)為里面的每一個(gè)目錄生成一個(gè)html入口
- new AutoWebPlugin('./src/pages/', {
- //使用單頁應(yīng)用的html模版文件,這里你可以自定義配置
- template: './src/assets/template.html',
- }),
- ],
查看web-webpack-plugin的文檔了解更多
分析輸出結(jié)果
如果你對(duì)當(dāng)前的配置輸出或者構(gòu)建速度不滿意,webpack有一個(gè)工具叫做webpack analyze 以可視化的方式直觀的分析構(gòu)建,來進(jìn)一步優(yōu)化構(gòu)建結(jié)果和速度。要使用它你需要在執(zhí)行webpack的時(shí)候帶上--json --profile2個(gè)參數(shù),這代表讓webpack把構(gòu)建結(jié)果以json輸出并帶上構(gòu)建性能信息,使用如下:
- webpack --json --profile > stats.json
會(huì)生產(chǎn)一個(gè)stats.json文件,再打開webpack analyze 上傳這個(gè)文件開始分析。
***附上這篇文章所講到的webpack整體的配置,分為開發(fā)環(huán)境的webpack.config.js和生產(chǎn)環(huán)境的webpack-dist.config.js