如今的 web 應用,內容通常都很豐富,站點須要加載的資源也特別多,尤爲要加載不少 js 文件。js 文件從服務端獲取,體積大小決定了傳輸的快慢;瀏覽器端拿到 js 文件以後,還須要通過解壓縮、解析、編譯、執行操做,因此,控制 js 代碼的體積以及按需加載對前端性能以及用戶體驗是十分的重要。javascript
本文從 Tree Shaking
和 代碼分割
兩部分介紹 js 打包優化,有興趣的能夠跟着一塊兒實踐。
clone 如下項目 https://github.com/jasonintju...,就是個簡單的 React SPA,一看就懂。css
Tree Shaking 簡單理解就是:打包時把一些沒有用到的代碼刪除掉,保證打包後的代碼體積最小化。其詳細的介紹能夠參考 Tree-Shaking性能優化實踐 - 原理篇。前端
項目 clone、安裝依賴後,先 npm run build
打包初始代碼,大小及分佈以下(其中 src/utils/utils.js
這個文件打包後大小爲11.72Kb
):java
src/containers/About/test.js
只引用可是沒有使用到,src/utils/utils.js
這個文件是個工具函數集,有不少不少函數,而咱們只用到了其中的一個。默認狀況下,整個文件都被打包進 main.js
了,顯然,這是很大的冗餘,正好可使用 Tree Shaking
優化。node
.babelrc
{ "presets": [["env", { "modules": false }], "react", "stage-0"] }
package.json
{ "name": "optimizing-js", "version": "1.0.0", "sideEffects": false }
這樣設置以後,表示全部的 module 都是無反作用的,沒有使用到的 module 均可以刪掉,此時打包結果以下:react
import React from 'react'; // 只引入了 arraySum, utils.js 中的其餘方法不會被打包 import { arraySum } from '@utils/utils'; import './test'; // 引用,「未使用」,不會被打包 import './About.scss'; // 引用,「未使用」,不會被打包 class About extends React.Component { render() { const sum = arraySum([12, 3]); return ( <div className="page-about"> <h1>About Page</h1> <div> 12 plus 3 equals {sum}</div> </div> ); } } export default About;
如上面註釋所說,Tree Shaking 認爲這些是沒有被使用的代碼,因此能夠刪掉。但事實上咱們知道不是這樣的,test.js
能夠刪掉,可是 css、scss 是有用的代碼,咱們只需引入便可。所以,須要修改一下 sideEffects
的值:webpack
{ "sideEffects": [ "*.css", "*.scss", "*.sass" ] }
表示,除了[]
中的文件(類型),其餘文件都是無反作用的,能夠放心刪掉。此時打包結果:git
能夠看到,css 等樣式文件如今如期打包進去了。若是有其餘類型的文件有反作用,可是也但願打包進去,在 sideEffects: []
中添加便可,能夠是具體的某個文件或者某種文件類型。github
關於爲何修改這兩個地方就能夠實現 Tree Shaking 的效果了,能夠參考一下https://developers.google.com... 或者其餘文章,這裏不作詳細解釋了。web
單頁應用,若是全部的資源都打包在一個 js 裏面,毫無疑問,體積會很是龐大,首屏加載會有很長時間白屏,用戶體驗極差。因此,要代碼分割,分紅一個一個小的 js,優化加載時間。
第三方庫代碼單獨提取出來,和業務代碼分離,減小 js 文件體積。在 webpack.base.conf.js
中增長:
module: {...}, optimization: { splitChunks: { cacheGroups: { venders: { test: /node_modules/, name: 'vendors', chunks: 'all' } } } }, plugins: ...
使用 ECMAScript 提案 的 dynamic import
語法能夠異步加載業務中的組件。使用方法以下:
// src/containers/App/App.js // 註釋掉此行代碼 // import About from '@containers/About/About'; // 修改模塊爲動態導入形式 <Route path="/about" render={() => import(/* webpackChunkName: "about" */ '@containers/About/About').then(module => module.default)}/>
此時打包結果:
能看到,<About> 組件
已經被 webpack 單獨打包出對應的 js 文件了。同時,結合 react-router
,分離 <About> 組件
的同時也作到了按需加載:當訪問 About 頁面時,about.js
纔會被瀏覽器加載。
注意,咱們如今只是簡單地使用了 dynamic import
,不少邊界狀況沒考慮進去,好比:加載進度、加載失敗、超時等處理。能夠開發一個高階組件,把這些異常處理都包含進去。社區有個很棒的 react-loadable,大樹底下好乘涼~
npm i react-loadable // src/containers/App/App.js import Loadable from 'react-loadable'; // 代碼分割 & 異步加載 const LoadableAbout = Loadable({ loader: () => import(/* webpackChunkName: "about" */ '@containers/About/About'), loading() { return <div>Loading...</div>; } }); class App extends React.Component { render() { return ( <BrowserRouter> <div> <Header /> <Route exact path="/" component={Home} /> <Route path="/docs" component={Docs} /> <Route path="/about" component={LoadableAbout} /> </div> </BrowserRouter> ); } }
react-loadable 還提供了 preload 功能。假若有統計數據顯示,用戶在進入首頁以後大機率會進入 About 頁面,那咱們就在首頁加載完成的時候去加載 about.js
,這樣等用戶跳到 About 頁面的時候,js 資源都已經加載好了,用戶體驗會更好。
// src/containers/App/App.js componentDidMount() { LoadableAbout.preload(); }
若是有同窗對Network面板不是很熟悉,能夠看一下 Chrome DevTools — Network。
第三方庫代碼已經單獨提取出來了,可是業務代碼中也會有一些複用的代碼,典型的好比一些工具函數庫 utils.js
。如今,About 組件
和 Docs 組件
都引用了 utils.js
,webpack 只打包了一份 utils.js
在 main.js
裏面,main.js 在首頁就被加載了,其餘頁面有使用到 utils.js 天然能夠正常引用到,符合咱們的預期。可是目前咱們只是把 About 頁面異步加載了,若是把 Docs 頁面也異步加載了會怎麼樣呢?
// src/containers/App/App.js // 註釋掉此行代碼 // import Docs from '@containers/Docs/Docs'; const LoadableDocs = Loadable({ loader: () => import(/* webpackChunkName: "docs" */ '@containers/Docs/Docs'), loading() { return <div>Loading...</div>; } }); class App extends React.Component { render() { return ( <BrowserRouter> <div> <Header /> <Route exact path="/" component={Home} /> <Route path="/docs" component={LoadableDocs} /> <Route path="/about" component={LoadableAbout} /> </div> </BrowserRouter> ); } }
此時打包結果:
可以看到,about.js 和 docs.js 裏面都打包了 utils.js,重複了!
在 webpack.base.conf.js
中增長:
module: {...}, optimization: { splitChunks: { cacheGroups: { venders: { test: /node_modules/, name: 'vendors', chunks: 'all' }, default: { minSize: 0, minChunks: 2, reuseExistingChunk: true, name: 'utils' } } } }, plugins: ...
再打包看結果:
utils.js 也被單獨打包出來了,達到了預期。
假如,如今 Docs.js 引用了 lodash
這個三方庫:
import React from 'react'; import _ from 'lodash'; import { arraySum } from '@utils/utils'; import './Docs.scss'; class Docs extends React.Component { render() { const sum = arraySum([1, 3]); const b = _.sum([1, 3]); return ( <div className="page-docs"> <h1>Docs Page</h1> <div> 1 plus 3 equals {sum}</div> <br /> <div>use _.sum, 1 plus 3 equals {b} too.</div> </div> ); } } export default Docs;
打包結果:
lodash.js 只在 Docs 頁面使用,並且可能 Docs 頁面訪問量不多,把 lodash.js 打包在首頁就會加載的 venders.js 裏面,實在不是明智之舉。
修改 webpack.base.conf.js
:
... venders: { test: /node_modules\/(?!(lodash)\/)/, // 去除 lodash,剩餘的第三方庫打成一個包,命名爲 vendors-common name: 'vendors-common', chunks: 'all' }, lodash: { test: /node_modules\/lodash\//, // lodash 庫單獨打包,並命名爲 vender-lodash name: 'vender-lodash' }, default: { minSize: 0, minChunks: 2, reuseExistingChunk: true, name: 'utils' } ...
此時把 lodash 單獨打成了一個包,且配合 Docs 頁面的按需加載,達到了理想的加載效果。
項目打包後,資源部署在服務器端,客戶端須要向服務器請求下載這些資源,用戶才能看到內容。使用緩存,客戶端能夠大大減小沒必要要的請求和時間耽擱,只有當資源有更新時,再去下載。區分一個文件是否有更新,使用 文件名 + hash
能夠達到目的。本案例中,已經使用了 '[name].[contenthash:8].js'
。
然而,在打包的時候,webpack的運行時代碼有時候會致使某些狀況出現,如:什麼內容都沒改,兩次 build 代碼的 hash 不同;或者是,修改了 a 文件的代碼,卻致使了某些未修改代碼文件的 hash 也發生了變化。This is caused by the injection of the runtime and manifest which changes every build.
注意:使用的 webpack 版本不一樣,可能會致使打包出的結果不同。較新的版本或許沒有這種 hash 問題,但爲了安全起見,仍是建議按照下面的步驟處理一下。
// webpack.base.conf.js optimization: { runtimeChunk: { name: 'manifest' }, splitChunks: {...} }
此時,能達到:修改某個文件,只有這個文件和 manifest.js 文件的 hash 會發生變化,其餘文件的 hash 不變。
打包前:
// About.scss .page-about { padding-left: 30px; color: #545880; // 修改字體顏色 }
修改後:
增長、刪除一些模塊,可能會致使不相關文件的 hash 發生變化,這是由於 webpack 打包時,按照導入模塊的順序,module.id 自增,會致使某些模塊的 module.id 發生變化,進而致使文件的 hash 變化。
解決方式: 使用 webpack 內置的 HashedModuleIdsPlugin,該插件基於導入模塊的相對路徑生成相應的 module.id,這樣若是內容沒有變化加上 module.id 也沒變化,則生成的 hash 也就不會變化了。
// webpack.prod.conf.js const webpack = require('webpack'); ... plugins: [new webpack.HashedModuleIdsPlugin(), new BundleAnalyzerPlugin()]
完整的優化代碼見 https://github.com/jasonintju...