如今的 web 應用,內容通常都很豐富,站點須要加載的資源也特別多,尤爲要加載不少 js 文件。js 文件從服務端獲取,體積大小決定了傳輸的快慢;瀏覽器端拿到 js 文件以後,還須要通過解壓縮、解析、編譯、執行操做,因此,控制 js 代碼的體積以及按需加載對前端性能以及用戶體驗是十分的重要。javascript
本文從 Tree Shaking
和 代碼分割
兩部分介紹 js 打包優化,有興趣的能夠跟着一塊兒實踐。 clone 如下項目 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 的效果了,能夠參考一下 developers.google.com/web/fundame… 或者其餘文章,這裏不作詳細解釋了。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()]
複製代碼
完整的優化代碼見 github.com/jasonintju/…
有用的文章: webpack分離第三方庫及公用文件
developers.google.com/web/fundame…