React+Webpack性能優化

本文主要講下React配合Webpack的一些優化,原項目在這裏,有空會持續更新,歡迎關注和start,另外還有個沒法使用HtmlWebpackPlugin插入chunks的issues請求哪位大佬幫忙解決下,謝謝~css

構建優化

loaders

  • 儘可能少使用不一樣的loaders/plugins
  • 使用 include 字段指明要轉換的目錄,使用exclude排除目錄:
module.exports = {
  ...
  module: {
    rules: [
      {
        test: /\.js$/,
        include: path.resolve(__dirname, 'src'),
        exclude: path.resolve(__dirname, 'node_modules'),
        loader: 'babel-loader'
      }
    ]
  }
};
複製代碼

resolve

  • 儘可能減小resolve.modules, resolve.extensions, resolve.mainFiles, resolve.descriptionFiles的值的數量html

  • resolve.modules:

    使用resolve.modules指定模塊目錄的路徑:node

    module.exports = {
        ...
        resolve: {
          modules: [path.resolve(__dirname, 'node_modules')]
        }
    };
    複製代碼
  • resolve.alias:

    resolve.alias使Webpack直接使用庫的壓縮版本,再也不對庫進行解析,還可使用別名方便引用文件:react

    module.exports = {
        ...
        resolve: {
          alias: {
            Components: path.resolve(__dirname, 'src/components/'),
            Utils: path.resolve(__dirname, 'src/utils/'),
            react: patch.resolve(__dirname, './node_modules/react/dist/react.min.js')
          }
        }
    };
    複製代碼

    例如這樣就能夠直接使用React的壓縮版本,每次構建時沒必要再次解析。還能夠經過別名引用文件,而沒必要再打長長的引用路徑:webpack

    import ReactComponent from 'Components/ReactComponent';
    複製代碼

    但這樣的缺點是會沒法使用Tree-Shaking,因此通常對React這種總體性比較強的庫使用比較好,而像lodash這樣的工具庫仍是使用Tree-Shaking去除多餘代碼。git

  • resolve.extensions:

    設置要解析文件後綴,默認值爲:github

    module.exports = {
        ...
        resolve: {
          extensions: ['.wasm', '.mjs', '.js', '.json']
        }
    };
    複製代碼

    能夠設置爲本身要解析的文件類型,加快尋找速度:web

    module.exports = {
        ...
        resolve: {
          extensions: ['.js', '.json', 'jsx']
        }
    };
    複製代碼

externals

使用externals能夠防止某些庫被打包,而經過其餘方式引用庫(如CDN),這樣作的好處是當更新代碼時不會影響庫代碼的緩存,用戶只需下載新的代碼便可。固然咱們也可使用chunk來把不常更新的庫打包在另外一個文件,咱們下面再講。json

例如,從CDN引入React:瀏覽器

<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js" defer></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js" defer></script>
<script src="./dist/index.js" defer></script>
複製代碼
module.exports = {
    ...
    externals: {
      react: 'React',
      'react-dom': 'ReactDOM'
    },
}
複製代碼

devtool

使用devtool是很耗性能的,若是不須要用到它的話就不要設置它,若是須要用到且質量要很好可設爲source-map,不過這是很是耗時的,若是能夠接受質量比較差的話,可以使用cheap-source-map,官方推薦使用的是性能比較好質量比較差的cheap-module-eval-source-map

splitChunks

Webpack 4以後把公共代碼提取工具從CommonChunksPlugin換成更好的SplitChunksPlugin。下面這個例子不使用externals,而是把React和ReactDOM提取到公共模塊代碼。

module.exports = {
  ...
  // externals: {
  // react: 'React',
  // 'react-dom': 'ReactDOM'
  // },
  optimization: {
    ...
    splitChunks: {
      chunks: 'all',
      name: true,
      automaticNameDelimiter: '-',  // 模塊間的鏈接符,默認爲"~"
      cacheGroups: {
        vendors: {
          test: /[\\/]node_modules[\\/]/,
          priority: -10  // 優先級,越小優先級越高
        },
        default: {  // 默認設置,可被重寫
          minChunks: 2,
          priority: -20,
          reuseExistingChunk: true  // 若是原本已經把代碼提取出來,則重用存在的而不是從新產生
        }
      }
    }
  },
}
複製代碼

mode

mode可取值有:

  • production:構建模式,會自動啓用一些構建相關的插件,如壓縮代碼。
module.exports = {
+  mode: 'production',
-  plugins: [
-    new UglifyJsPlugin(/* ... */),
-    new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("production") }),
-    new webpack.optimize.ModuleConcatenationPlugin(),
-    new webpack.NoEmitOnErrorsPlugin()
-  ]
}
複製代碼
  • development:開發模式,會啓動一些開發相關的優化插件。
module.exports = {
+ mode: 'development'
- devtool: 'eval',
- plugins: [
-   new webpack.NamedModulesPlugin(),
-   new webpack.NamedChunksPlugin(),
-   new webpack.DefinePlugin({ "process.env.NODE_ENV": JSON.stringify("development") }),
- ]
}
複製代碼
  • node

babel、Tree-Shaking

這裏使用的版本爲babel 7。由於如今大多數瀏覽器都已經支持ES6的語法,因此若是全部代碼都轉爲ES5的話可能會產生大量的多餘代碼,因此這裏只轉換部分代碼,那要兼容低版本的瀏覽器怎麼辦呢,別急,下面會講到一些解決辦法,咱們先來看下babel配置:

{
    "presets":  [
        [
            "@babel/react",
            {
                "modules": false  // 關閉babel的模塊轉換,才能使用Webpack的Tree-Shaking功能
            }
        ]
    ],
    "plugins": [
        "@babel/plugin-proposal-class-properties",  // class,這個要放在前面,不然可能會報錯
        "@babel/plugin-transform-classes",  // class
        "@babel/plugin-transform-arrow-functions",  // 箭頭函數
        "@babel/plugin-transform-template-literals"  // 字符串模板
    ]
}
複製代碼

當一些庫的package.jsonsideEffects有設置時,就能夠很好地支持Tree-Shaking,如lodash:

{
  "name": "lodash",
  "sideEffects": false
}
複製代碼

happypack

使用happypack可開啓多線程來加速處理loader:

var HappyPack = require('happypack');

module.exports = {
    ...
    rules: [
      {
        test: /\.(js|jsx)$/,
        include: path.resolve(__dirname, 'src'),
        exclude: path.resolve(__dirname, 'node_modules'),
        use: 'happypack/loader?id=babel'
      },
    ],
    plugins: [
      new HappyPack({
        id: 'babel',
        loaders:['babel-loader?cacheDirectory']
      }),
    ],
}
複製代碼

其餘

把代碼構建到ES6+

上面說到轉換代碼到ES5的話會很耗時且可能有不少多餘代碼,由於如今大多數瀏覽器都已經支持ES6語法,如今咱們來看看如何兼容較低版本的瀏覽器。

  1. modulenomodule:

可使用<script type="module" src="index.js"></script>來加載ES6+的代碼,由於支持這個屬性的瀏覽器一定會支持async/awaitPromiseclass這些屬性,而不支持的瀏覽器則會選擇忽略它,不進行加載。

因此也還須要一份ES5的腳原本兼容低版本的瀏覽器,使用<script nomodule src="index.es5.js"></script>來加載ES5代碼,能夠識別nomodule的瀏覽器會忽略它,而不能識別它的低版本瀏覽器則會加載它。這樣就能夠作到兼容到低版本的瀏覽器而較新的瀏覽器使用代碼量少不少的ES6+代碼。

可是這個方法也有缺點:當使用splitChunks把代碼分爲較多的模塊時,須要產生大量兩個版本的代碼。

  1. 動態polyfill
<script src="https://cdn.polyfill.io/v2/polyfill.min.js"></script>
複製代碼

它會經過分析請求頭信息中的 UserAgent 實現自動加載瀏覽器所需的 polyfills。若是你使用較新的版本訪問上面的鏈接會發現沒有多少代碼,而用IE則會產生不少。這樣咱們就可使用ES6+的代碼和動態polyfill來兼容低版本瀏覽器,可是動態polyfill不能支持class和箭頭函數等等這些特性,因此就須要按上面那樣配置babel來把這些轉換成ES5的。想知道更多動態polyfill能夠點這裏

開發優化

避免使用構建時才使用到的工具

有一些工具在開發時是不須要用到的,若是用了可能會大大減慢生成代碼的速度,如UglifyJsPlugin,在開發時不須要將代碼進行壓縮,還有如下工具也避免在開發時用到:

  • UglifyJsPlugin
  • ExtractTextPlugin
  • [hash]/[chunkhash]
  • AggressiveSplittingPlugin
  • AggressiveMergingPlugin
  • ModuleConcatenationPlugin

不要輸出路徑信息

module.exports = {
  // ...
  output: {
    pathinfo: false
  }
};
複製代碼

關閉部分構建優化

module.exports = {
  ...
  optimization: {
    removeAvailableModules: false,
    removeEmptyChunks: false,
    splitChunks: false,
  }
};
複製代碼

React優化

由於React的HTML元素都是寫在JS文件中,因此通常致使構建出的JS文件很是大,而在加載和執行JS的漫長過程當中,用戶的瀏覽器一直顯示的都是白屏狀態,首屏渲染的時間變得很是的長,不使用服務端渲染的話能夠按如下方法進行一些改善。

添加首屏loading

可經過使用HtmlWebpackPlugin插件來爲html文件添加loading,而不至於白屏。

var loading = {
  ejs: fs.readFileSync(path.resolve(__dirname, 'template/loading.ejs')),
  css: fs.readFileSync(path.resolve(__dirname, 'template/loading.css')),
};

module.exports = {
    ...
    plugins: [
        new HtmlWebpackPlugin({
          template: path.resolve(__dirname, 'template/index.ejs'),
          hash: true,
          loading: loading,  // 在React渲染完前添加loading
        }),
        new ScriptExtHtmlWebpackPlugin({  // 給script標籤加上defer
          defaultAttribute: 'defer'
        }), 
    ]
}
複製代碼

具體的模板代碼看這裏

prerender-spa-plugin

prerender-spa-plugin能夠生成單頁面應用的首屏到HTML,原理是經過puppeteer訪問相應路徑抓取相應的內容,這裏由於我一直裝不上puppeteer,因此就不深刻講了。

module.exports = {
    ...
    new PrerenderSpaPlugin(
      // Absolute path to compiled SPA
      path.resolve(__dirname, '../dist'),
      // List of routes to prerender
      ['/']
    )
}
複製代碼

React Loadable

可使用它來動態import React的組件,能夠把一些不是那麼重要的組件先分離到chunks,而後再動態引入,能夠提高渲染首屏的速度:

import Loading from './src/components/Loading';
import ReactDOM from 'react-dom';
import Loadable from 'react-loadable';

const LoadableApp = Loadable({
  loader: () => import('./src/App'),
  loading: Loading,
});

ReactDOM.render(LoadableApp, document.querySelector('#root'));

複製代碼

暫時就寫這麼多優化的地方,之後有空會持續更新,有什麼問題歡迎一塊兒討論~

相關文章
相關標籤/搜索