決戰性能之巔 - Taro H5 轉換與優化升級

前言

做爲一個多端開發框架,Taro 從項目發起時就已經支持編譯到 H5 端。隨着 Taro 多端能力的不斷成熟,咱們對 Taro H5 端應用的要求也不斷提高。咱們已經再也不知足於「能跑」,更但願 Taro 能跑得快。javascript

咱們常常收到用戶反饋:爲何使用 Taro 腳手架建立的空項目,打包後代碼體積卻有 400KB+;也有用戶在 Issue 中提到,Taro 的部分 Api 佔用空間巨大,事實上功能卻並不完美,等等。做爲一個開源項目,咱們很是重視社區開發者們的意見。因此在最新版本中,咱們對 Taro H5 端的性能表現進行了優化。java

做爲運行時的基礎,每個 Taro 的 H5 端應用都須要引入 @tarojs/components 和 @tarojs/taro-h5 等基礎依賴包。在編譯、打包以後,這些依賴包大約會在首屏佔用 400KB 以上的空間。若是開發者還使用了 UI 庫,例如 Taro-UI,基礎體積還會更大,這嚴重限制了 Taro H5 端應用的性能優化空間。webpack

事實上,咱們在 H5 端應用中並不會使用到所有的 Taro 組件和 Api。將這些依賴包所有打包進應用是沒有必要,也是不合理的。進行死碼刪除(Dead code elimination),進一步縮減代碼體積,就是咱們的優化方向之一。git

效果

在介紹具體細節以前,咱們先看一看優化的效果,由於這可能會讓你更有興趣瞭解後面的內容。用一句話來講,效果很是顯著。github

咱們創建了一個空項目,在項目配置中加入了webpack-bundle-analyzer插件以查看編譯分析。下圖是優化前的打包文件分析結果:web

before

而在優化後,對比很是明顯:npm

after

優化前生成的代碼總大小爲 455KB,而在優化後僅剩約 96KB,僅是原來的 1/5 左右。編程

你須要作什麼

很簡單,做爲使用者,你不須要作任何代碼上的改動,只須要將 Taro 更新到最新版本便可。但在看不見的地方,Taro 卻默默地作了許多工做。下面會從原理出發,詳細介紹 Taro 的工做。json

原理

死碼刪除(Dead code elimination)是一種代碼優化技術,能夠刪除對應用程序執行結果沒有影響的代碼。Web Fundamentals 的一篇文章有提到,treeshaking 是由 Rollup 提出的一種死碼刪除的形式。api

Tree shaking is a form of dead code elimination. The term was popularized by Rollup, but the concept of dead code elimination has existed for some time.

-- Reduce JavaScript Payloads with Tree Shaking, Jeremy Wagner

經過在構建時進行靜態分析,編譯工具能夠分析出咱們代碼中真正的依賴關係。treeshaking 把咱們的代碼想象成一棵樹,代碼的每一個依賴項看做樹上的節點。將未使用過的依賴項從構建結果中移除,這就是 treeshaking 的基本思想。

那麼,假設咱們手頭有一段代碼,咱們要怎樣辨別其中能夠刪除的部分呢?答案是,經過分析反作用:

// add.js
export default function adda, b{ return a + b; }

// add2.js
console.log('這是一個log')
export default function add2a, b{ return a + b; }

// index.js
import add from './add.js' // 沒有反作用,能夠刪除
import add2 from './add2.js' // 有反作用,不能直接刪除

// EOF
複製代碼

反作用這個名詞對於瞭解函數式編程的同窗確定不陌生。修改外部狀態,或者是產生輸出等等,都是反作用;而存在反作用的代碼,是不能被直接移除的。相似上面這個代碼示意,add2 模塊就是存在反作用的。

站在巨人的肩膀上

除了 Rollup 以外,支持 treeshaking 的工具/插件還有不少,好比 babel-plugin-transform-dead-code-elimination、uglify、terser等。 webpack 在 v2 以後就內置了對 treeshaking 的支持,並在 webpack@4 中對 treeshaking 功能進行了擴展。

Taro H5 端在構建過程當中,使用 webpack 做爲構建的核心。在 webpack 中使用 treeshaking 功能有幾個須要注意的地方:

  1. 若是是 npm 模塊,須要package.json中存在sideEffects字段,而且準確配置了存在反作用的源代碼。
  2. 必須使用 ES6 模塊語法。因爲諸如babel-preset-env之類的 babel 預配置包默認會對代碼的模塊機制進行改寫,還須要將modules設置爲false,將模塊解析的工做直接交給 webpack。
  3. 須要工做在 webpack 的production模式下。

webpack 的 treeshaking 工做主要分爲兩步。第一步是在模塊級別移除未使用且無反作用的模塊,這一步由 webpack 的內置插件完成;第二步是在文件級別移除未使用的代碼,這一步由代碼壓縮工具 Terser 完成的。

移除未使用的模塊

前面咱們提到,須要在package.json中配置sideEffects字段。

webpack 文檔 中有提到,這一舉動正是爲了讓 webpack 正確地識別到沒有反作用的代碼模塊。

在 webpack 中,模塊依賴分析是由內置插件 SideEffectsFlagPlugin 進行的。

image-20190225220418363

通過 SideEffectsFlagPlugin處理後,沒有使用過而且沒有反作用的模塊都會被打上sideEffectFree標記。

ModuleConcatenationPlugin 中,帶着sideEffectFree標記的模塊將不會被打包:

image-20190222111301698

來到這裏,webpack 完成了在模塊級別對未使用模塊的排除。接下來,依靠 Terser,webpack 能夠在文件級別,對未使用、無反作用的代碼進行移除。

移除未使用的代碼

在 CommonJS 規範中,咱們經過require函數來引入模塊,經過module.exports進行導出。這意味着咱們能夠在代碼中的任何地方用任何方式引入和導出模塊:能夠是在某個須要等待用戶輸入的回調函數中,或者是在符合某個條件才進行引入等等。因此在使用 ES6 的模塊系統以前,對 Javascript 作編譯時的依賴關係分析是近乎不可能的(並非徹底不可能。prepack 經過實現一個 JS 解釋器,甚至能夠在編譯時提早進行靜態計算)。

// utils.js
module.exports.add = function (a, b) { return a + b };
module.exports.minus = function (a, b) { return a - b };

// index.js;
var utils = require('./utils.js');

utils.add(1, 2);
複製代碼

像上面這段代碼,雖然咱們最終只使用了add函數,但minus函數也會在最終的打包代碼中出現,由於在編譯時沒法快速得知是否使用了minus函數。

在 ES6 的模塊系統中,咱們使用import/export語法來進行模塊的引入和導出。與 CommonJS 規範不一樣的是,這套新的模塊系統存在一些限制:import/export行爲只能在代碼的頂層、默認使用嚴格模式等等。這些限制使代碼模塊的導入與導出變得靜態化,模塊間的依賴關係在開發時已經肯定,編譯器也更容易解析咱們的代碼。

// utils.js
export function add (a, b) { return a + b };
export function minus (a, b) { return a - b };

// index.js;
import { add } from './utils.js';
add(1, 2);
複製代碼

在使用 ES6 模塊系統改造後,咱們能夠清楚地看到,minus函數確實沒有被使用過,因此能夠安全地將其從最終打包代碼中移除。

固然,具體的分析過程很是複雜。變量提高、object 取值操做、for(var i in list) 語句、自執行函數、函數傳參(onClick(function a () {…}))等等,都有可能致使意料以外的狀況,從而致使 treeshaking 失效。若是想了解 Terser 的具體處理過程,百度/Google 會是最好的老師。

Taro 作了什麼

Taro 須要對依賴包作一些修改。

組件的 ES 模塊化

在進行組件庫的 ES 模塊化改造以前,若是要發佈 @tarojs/components 包,Taro 會執行命令 yarn build,使用 webpack 對源代碼進行打包,輸出爲dist/index.js文件。因爲 webpack 並不支持輸出爲 ES 模塊,因此這是個 UMD 模塊。

image-20190225154632128

這個文件佔據了 462KB 的空間,而且因爲模塊規範等問題,沒法進行 treeshaking。因此就算開發者在 Taro 的項目中只引入了兩個組件,最終的打包結果也會包含全部的內置組件。

事實上,@tarojs/components 的源碼自己是使用 ESM 規範的:

image-20190225160508956

因此只要讓 webpack 直接解析組件庫的源碼,咱們當即就能夠享受到 webpack 自帶 treeshaking 帶來的好處了。

image-20190225162018328

同時,咱們也在sideEffects屬性中對樣式文件作了標記,幫助 webpack 對樣式代碼的反作用進行識別,在項目編譯的代碼中保留樣式代碼。

Api 的 ES 模塊化

一樣,之前在發佈 @tarojs/taro-h5 以前,Taro 也須要執行命令 yarn build,使用 Rollup 對源代碼進行打包,輸出爲dist/index.js文件:

image-20190225162654885

這個文件佔據了 262KB 的空間。一樣,只要是個 Taro 的 H5 端應用,生成的代碼都會全量引入這個文件。

咱們對 @tarojs/taro-h5 進行模塊化改造的思路與 @tarojs/components 相同。咱們但願 @tarojs/taro-h5 模塊自己遵照 ESM 模塊規範,那就只須要標記一下sideEffects,再修改一下模塊入口就好。

image-20190225165957461

粗略一看,@tarojs/taro-h5 還挺 「ESM」 的,但這還不夠。咱們還須要將這些 Api 以 namedExports 的形式導出,開發者使用import { XXX } from '@tarojs/taro-h5'導入 Api 便可。

image-20190225172609369

那麼問題來了。在 Taro 項目中,咱們一直使用的是 defaultImport,並不會使用 Api 的 namedExports 形式:

import Taro from '@tarojs/taro-h5'

Taro.navigateTo()
Taro.getSystemInfo()
// Taro.xxx ...
複製代碼

只要 Api 是經過對Taro變量取屬性獲取,Taro變量就須要具有全部的 Api,treeshaking 也就無從談起。

有沒有辦法把 defaultImport 修改成 namedImports 呢?答案是確定的。咱們寫了一個 babel 插件 babel-plugin-transform-taroapi,將指定的 Api 調用替換爲 namedImports,未指定的變量則保留屬性取值的形式。具體源碼能夠在__這裏__查看。

// const apis = new Set(['navigateTo', 'navigateBack', ...])
{
  babel: {
    preset: ['babel-preset-env'],
    plugins: [
      // ...,
      ['babel-plugin-transform-taroapi', {
        packageName: '@tarojs/taro-h5',
        apis
      }]
    ]
  }
}
複製代碼

這個插件接受一個對象做爲配置參數:packageName屬性指定須要進行替換的模塊名,apis接受一個 Set 對象,也就是全部 Api 的列表。

爲了不後期手動維護 Api 列表的狀況,咱們給 @tarojs/taro-h5 模塊加了一個編譯任務,經過一個簡單的Rollup 插件,在執行yarn build命令時生成一份 Api 列表:

image-20190225210238592

下面是編譯先後的代碼對比。能夠看到,在編譯後setStoragegetStorage的調用都被替換爲 namedImports。

// 編譯前
import Taro from '@tarojs/taro-h5';
Taro.initPxTransform({});
Taro.setStorage()
Taro['getStorage']()

// 編譯後
import Taro, { setStorage as _setStorage, getStorage as _getStorage } from '@tarojs/taro-h5';
Taro.initPxTransform({});
_setStorage();
_getStorage();
複製代碼

到這裏,雖然過程比較艱辛,但咱們對 @tarojs/taro-h5 的模塊化改造終於完成了。

最後

截至目前,Taro 在 H5 端的完成度已經很高,可是並不完美。將來,在對已有問題進行修復的同時,咱們還將繼續爲 Taro H5 端帶來更多新的特性,好比對社區中呼聲至關高的switchTab、頁面滾動監聽onPageScroll、下拉刷新onPullDownRefresh等 Api 的支持,更加統一的頁面切換動畫,以及更加穩定的多頁面模式等等。

Taro 發展到如今,離不開社區的支持。很是感謝在 github、微信羣中踊躍反饋的開發者們。若是你對Taro有什麼想法或建議,Taro 很是歡迎你來吐槽或觀光:

https://github.com/NervJS/taro

相關文章
相關標籤/搜索