前端性能優化—js代碼打包

如今的 web 應用,內容通常都很豐富,站點須要加載的資源也特別多,尤爲要加載不少 js 文件。js 文件從服務端獲取,體積大小決定了傳輸的快慢;瀏覽器端拿到 js 文件以後,還須要通過解壓縮、解析、編譯、執行操做,因此,控制 js 代碼的體積以及按需加載對前端性能以及用戶體驗是十分的重要。javascript

本文從 Tree Shaking代碼分割 兩部分介紹 js 打包優化,有興趣的能夠跟着一塊兒實踐。
clone 如下項目 https://github.com/jasonintju...,就是個簡單的 React SPA,一看就懂。css

Tree Shaking

Tree Shaking 簡單理解就是:打包時把一些沒有用到的代碼刪除掉,保證打包後的代碼體積最小化。其詳細的介紹能夠參考 Tree-Shaking性能優化實踐 - 原理篇前端

項目 clone、安裝依賴後,先 npm run build 打包初始代碼,大小及分佈以下(其中 src/utils/utils.js 這個文件打包後大小爲11.72Kb):java

clipboard.png

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

clipboard.png

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

clipboard.png

能夠看到,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: ...

clipboard.png

動態導入

使用 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)}/>

此時打包結果:

clipboard.png

能看到,<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();
}

clipboard.png

若是有同窗對Network面板不是很熟悉,能夠看一下 Chrome DevTools — Network

提取複用的業務代碼

第三方庫代碼已經單獨提取出來了,可是業務代碼中也會有一些複用的代碼,典型的好比一些工具函數庫 utils.js。如今,About 組件Docs 組件都引用了 utils.js,webpack 只打包了一份 utils.jsmain.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>
    );
  }
}

此時打包結果:

clipboard.png

可以看到,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: ...

再打包看結果:

clipboard.png

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;

打包結果:

clipboard.png

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 頁面的按需加載,達到了理想的加載效果。

clipboard.png

緩存

項目打包後,資源部署在服務器端,客戶端須要向服務器請求下載這些資源,用戶才能看到內容。使用緩存,客戶端能夠大大減小沒必要要的請求和時間耽擱,只有當資源有更新時,再去下載。區分一個文件是否有更新,使用 文件名 + 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 runtimeChunk code

// webpack.base.conf.js
optimization: {
  runtimeChunk: {
    name: 'manifest'
  },
  splitChunks: {...}
}

此時,能達到:修改某個文件,只有這個文件和 manifest.js 文件的 hash 會發生變化,其餘文件的 hash 不變。
打包前:

clipboard.png

// About.scss
.page-about {
  padding-left: 30px;
  color: #545880; // 修改字體顏色
}

修改後:

clipboard.png

HashedModuleIdsPlugin

增長、刪除一些模塊,可能會致使不相關文件的 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...


有用的文章:
webpack分離第三方庫及公用文件
https://developers.google.com...

相關文章
相關標籤/搜索