1. 程式人生 > >webpack打包經驗——處理打包文件體積過大的問題

webpack打包經驗——處理打包文件體積過大的問題

樣式 class a turn dynamic 所有 動態加載 17. ide split

前言

最近對一個比較老的公司項目做了一次優化,處理的主要是webpack打包文件體積過大的問題。

這裏就寫一下對於webpack打包優化的一些經驗。

主要分為以下幾個方面:

  1. 去掉開發環境下的配置
  2. ExtractTextPlugin:提取樣式到css文件
  3. webpack-bundle-analyzer:webpack打包文件體積和依賴關系的可視化
  4. CommonsChunkPlugin:提取通用模塊文件
  5. 提取manifest:讓提取的公共js的hash值不要改變
  6. 壓縮js,css,圖片
  7. react-router 4 之前的按需加載
  8. react-router 4 的按需加載

本篇博客用到的webpack插件如何配置都可以去查看我寫的這篇博客:

【Webpack的使用指南 02】Webpack的常用解決方案

這裏就不細講這些配置了。

去掉開發環境下的配置

比如webpack中的devtool改為false,不需要熱加載這類只用於開發環境的東西。

這些不算是優化,而算是錯誤了。

對於在開發環境下才有用的東西在打包到生產環境時通通去掉。

ExtractTextPlugin:提取樣式到css文件

將樣式提取到單獨的css文件,而不是內嵌到打包的js文件中。

這樣帶來的好處時分離出來的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'],
        }),
      },
    ],
  },
}

打包後生成文件如下:

技術分享圖片

webpack-bundle-analyzer:webpack打包文件體積和依賴關系的可視化

這個東西不算是優化,而是讓我們可以清晰得看到各個包的輸出文件體積與交互關系。

安裝:

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打包分析圖。

CommonsChunkPlugin:提取通用模塊文件

所謂通用模塊,就是如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中。

提取manifest:讓提取的公共js的hash值不要改變

當我們了解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,圖片

這個其實不準備記錄進來,因為這些一般項目應該都具備了,不過這裏還是順帶提一句吧。

壓縮js和css一步即可:

webpack -p

圖片的壓縮:

image-webpack-loader

具體的使用請查看 Webpack的常用解決方案 的第16點。

react-router 4 之前的按需加載

如果使用過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 的按需加載

上面那種方法應用到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上面找了一下,發現它提供的按組件命名的方法需要用到服務端渲染,然後就沒有繼續下去了。

反正這個東西也不是很重要,所以就沒有進一步深究,如果有園友對這個問題有好的辦法,也希望能在評論裏說明。

總結

總的來講,通過以上步驟應該是可以解決絕大多數打包文件體積過大的問題。

當然,因為文中webpack版本和插件版本的差異,在配置和玩法上會有一些不同,但是上面描述的這些方向都是沒有問題的,並且相信在各個版本下都可以找到相應的解決方案。

文中如有疑誤,請不吝賜教。

webpack打包經驗——處理打包文件體積過大的問題