乾貨 | 減少50%空間,攜程機票React Native Bundle 分析與優化

{"type":"doc","content":[{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"一、前言"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在業務迭代上線的過程中,往往會出現一些代碼冗餘,導致最終打包出來的 bundle size 不盡如人意。同時,業務包占用的尺寸過大,對應用的性能以及用戶體驗都會造成一定程度的影響。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":"br"}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文將從 JavaScript 層面對 React Native 的業務包進行分析與優化,在這個過程中會運用 CRN (Ctrip React Native)bundle 分析平臺等工具,在項目開發的中後期對業務包的尺寸進行裁剪優化。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"二、現狀"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"目前針對 React Native 的性能調優可以使用的工具少之又少,下面將介紹 React Native 中可以對 bundle 進行可視化的本地工具,以及我們爲什麼需要一個在線平臺去構建 bundle 分析結果。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.1 使用 bundle-analyzer 進行包模塊內容的實時查看"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 react-native 中可以使用 "},{"type":"link","attrs":{"href":"https:\/\/www.npmjs.com\/package\/react-native-bundle-visualizer","title":null,"type":null},"content":[{"type":"text","text":"react-native-bundle-visualizer"}]},{"type":"text","text":" 進行 bundle 的查看。它的原理是使用了 source-map-explorer 進行了 Metro bundler 的可視化輸出。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Metro 是 React Native 官方的打包程序,會生成對應的 bundle 文件。在 react 中或者是使用 webpack 等工具打包出來的內容,都可以使用與 source-map-explorer 相關的一些打包分析工具進行可視化內容查看。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/10\/1027ba888767c1111a90a470bb0766c7.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用 bundle 分析工具,可以比較明顯地辨識出哪些業務文件大小比較異常、需要進行優化,或者是引用了哪些 Javascript 庫,導致 bundle 膨脹。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":"br"}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"執行如下命令進行安裝並啓動:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"cmd yarn add --dev react-native-bundle-visualizer && yarn run react-native-bundle-visualizer"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果沒有跑出具體內容,則需要手動添加入口文件。例如,--entry-file .\/index.android.js。執行結果會把 node_modules 和源文件中打包出來的代碼尺寸都包含在內,可以清晰地看出哪些文件佔用的空間比較大。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"2.2 爲什麼要開發 CRN bundle 分析平臺"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Web 端針對 React 的分析優化工具很多,包括 webpack 官方也有提供打包分析,但這些針對 React Native 都不能使用。在上一小節中提到的工具,也只能在本地運行,每次改動後需要生成新的 treemap 進行圖片之間的對比查看,不直觀並且不方便對比。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"React Native 開發的模塊最後都會打包到 APP 中,如果能在平時的開發階段,就注重保持 Bundle SIZE 的簡潔,注意觀察業務包 SIZE 的限制大小,那麼不需要後期進行排查裁剪。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CRN bundle 分析平臺不需要使用者手動運行,只需要使用者選擇自己的業務包名稱,即可進行在線的分析,並且可生成過去7天 bundle size 色階圖,可以讓使用者對過去一段時間內的開發打包結果進行及時排查,也就是說可以對包內尺寸的膨脹進行告警。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"現有的 React Native Bundle 分析工具,除了只能本地進行運行以外,還存在的缺點就是它是針對 React Native 官方的打包工具的運行結果進行的分析,對於 Ctrip React Native 或者是其他基於 React Native 優化的跨平臺開發框架,是會有一定缺陷的,例如無法找到正確的入口文件、無法找到對應的依賴關係等等。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"針對 React Native 進行 bundle 分析的在線平臺,相較於現有的工具,具有以下優點:"}]},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"便於 React Native 性能調優"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"便於減少APP SIZE,提升應用整體性能"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在線分析展示"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"包內 SIZE 膨脹告警"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Ctrip React Native Bundler 打包結果定製化分析"}]}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"定製化的 RN bundle 分析平臺,可以隨時拉取當前業務包的歷史打包結果,並且進行在線分析與告警,還可以讓使用者得到一個關於本次優化內容的文件差異對比內容,在分析優化後,可以快速看到優化效果,簡單高效。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"三、CRN bundle 分析平臺"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.1 功能介紹"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CRN bundle 分析平臺,可以對 React Native 打包後的內容進行在線二次分析。它具有項目內部模塊依賴分析、文件尺寸樹狀結構矩形圖等圖表展示功能。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.1.1 bundle 概要"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/9a\/9aec5c85d2edc971faa6a7b180bca541.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如上所示中,展示了業務模塊的整體大小以及壓縮後的尺寸,並且進行了圖形化的佔比展示。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在條形圖中,從打包的模塊內容角度,顯示了當前業務包中佔比最大的五項內容,包括 build 後生成的內容,以及 node_modules 中的模塊大小佔比。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"底部的佔比圖中,從文件類型的角度,顯示了當前業務包中的 JavaScript、Font、Image 等文件類型的大小佔比。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 bundle 概要的頁面,顯示了當前業務包的源代碼大小以及打包後的壓縮大小。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.1.2 SIZE 詳情"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/42\/428c398e5b017d3bc11b6d8d2161ce04.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"bundle Size 詳情頁面,使用樹形結構圖,直觀地展示了當前業務包中各個模塊的尺寸大小以及佔比。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"可對相應的模塊文件進行搜索查看,同時會高亮展示在樹形結構圖區域,以便排查和優化打包結果。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用詳情頁面,可以對優化前後的結果進行圖形化的尺寸對比。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.1.3 模塊依賴分析"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/6b\/6b2ff1a1f563e16257fe658317bbde71.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"模塊依賴分析頁面,會根據模塊的依賴關係生成 dependency graph,便於排查模塊之間的深層依賴。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.1.4 文件差異比較"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/a1\/a19123fcec556c9913adfe4b9d468736.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於任意兩個發佈單,可以根據 JobId 進行包內各個文件的大小 diff 對比,並且會鏈接到 gitlab 對應的 changes 內容,可以看到代碼優化部分的相關 commmits 。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用該功能,可以直觀地進行某次裁剪前後的尺寸大小對比,快速驗證優化效果。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3.1.5 CRN 模塊可用包大小統計"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/52\/5216f7f8b6e96d0ef1a2e5c396b69621.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對於每個業務包可以給出一個可用包尺寸大小,並且根據每日打包結果,生成對應的過去時間段中的打包尺寸大小色階圖,使用色階可以預警過去的時間段中是否出現超限的業務包打包結果,及時對打包內容進行排查。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"3.2 實現原理"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"CRN bundle 分析平臺主要依賴三個部分進行實現,分別是處理 JOB 數據、使用後臺 API 分析打包後的業務包文件,最後在前端進行各種圖表化的展示。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/e2\/e24528aff7fdee8bd0560c4d5d251e4d.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"打開平臺頁面後,使用者選擇要分析的業務包名稱,後臺API根據參數調用相關接口,得到要分析的業務包的下載地址和對應的內容映射文件,並且將數據添加到隊列中,等待後續分析處理。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"循環調用後臺 API 去獲取要分析的 JOB 進行數據處理。在這個過程中,調用 Nodejs 對當前選擇的業務包進行基礎分析,並與 map 文件相結合,得到關鍵依賴數據與代碼詳情內容,生成最基礎也是最重要的數據包,這個數據包使用 JobId 作爲文件名稱,得到一個 JSON 格式的數據內容,後續的處理都在這個 JSON 文件的基礎上進行。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"獲得一個基礎數據文件後,使用前端把數據處理爲需要的格式進行展示。在前端交互邏輯上使用了 Vue.js 與 element-ui 進行基礎頁面構建,使用 d3.js 進行了數據的可視化展示,在這裏用到了樹形矩陣圖、色階圖、條形圖、依賴關係圖等等圖表進行內容展示。在 DIFF 頁面中,同時分析了兩個指定的 JobId 下的業務包內容,並且按照差異內容進行了詳細的 SIZE 增減對比。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"四、分析包模塊"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在進行包裁剪之前,我們需要先分析業務包內各模塊的佔比大小,以便對具體的模塊進行修改。工欲善其事,必先利其器。在有了分析工具後,可以對業務包模塊進行詳細的分析與優化。在這裏,使用本地安裝的 bundle 分析工具進行普適的分析。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個截圖中,可以很清楚地看到,除了公共引用庫以外的內容中,有幾個比較明顯的膨脹模塊,分別是 lodash、moment,以及一個工具類庫下的業務邏輯文件。接下來我們針對這幾處明顯的問題進行優化。"}]},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/56\/56f058baaf340c6de45500d9f5281891.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"五、解決方案"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.1 常用類庫優化方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Momentjs 和 Lodashjs 是前端常用類庫,但這兩個都有很明顯的問題,所佔據的文件空間略大,而且大多數時候我們只需要用到其中小部分的功能。在如下類庫替換過程中用到的方法,可以運用到所有常用類庫的優化使用中。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"5.1.1 選擇滿足需求的最小類庫"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"moment 是一個常用的JavaScript日期處理類庫,它支持多語言的日期格式。moment 的核心代碼只有52kb,但是包含了全世界語言的本地化文件,也就是說當你使用其中的功能時,也包含了很多你用不到的特性。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"對應的解決方案是你可以通過 npm 安裝"},{"type":"link","attrs":{"href":"https:\/\/github.com\/ksloan\/moment-mini","title":null,"type":null},"content":[{"type":"text","text":"moment-mini"}]},{"type":"text","text":",該庫非官方維護,但暴露了官方的 moment-min.js 作爲 npm 模塊開源使用。或者你可以直接使用一些更爲簡潔的 JavaScript 日期格式化類庫。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"作爲 momentjs 的替代方案,可以使用 luxon、date-fns、dayjs,或者直接使用 JavaScript 的原生 API 來做日期國際化(JavaScript Internationalization API)。如果不需要引入日期國際化,dayjs 核心代碼只有7.1k,可以作爲 momentjs 的替代。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/93\/9344c34ba1371b6f627a7d31c44ca25e.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"5.1.2 不必要時避免引入整個類庫"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"lodash 是一個實用性非常高的 JavaScript 工具庫,可以對 array、object、string 等值進行操作和檢測等等,還具有一些非常實用的函數。但lodash類庫所佔用的空間達到了71K,而且也存在很多你用不上的方法。實際上,我們在使用中或許只會用到非常少的幾個函數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"官方雖然也提供了 lodash-cli 這樣的工具,讓使用方可以針對具體的某些函數進行打包,但官方是不推薦這種用法的,並且在新的版本中也取消了這樣的部分模塊打包方式。官方推薦的方式是,在引用時指定對應的函數,這樣最終打包時只會打包對應的函數。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如下所示,如果直接引用 lodash,大小時71K。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\njavascript import get from 'lodash' \/\/ 71K (gzipped: 24.7K)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果引用對應的函數,那麼所需要的空間會大大減少。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":"br"}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"javascript import get from 'lodash\/get' \/\/ 8.2K (gzipped: 2.5K)"}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"通過 Babel 插件配置"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"link","attrs":{"href":"https:\/\/www.npmjs.com\/package\/babel-plugin-transform-imports","title":null,"type":null},"content":[{"type":"text","text":"babel-plugin-transform-imports"}]}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這個插件可以把全局 import 替換爲具體模塊的單獨引入。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"配置如下:"}]},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n# .babelrc\n\"plugins\": [\n [\"transform-imports\", {\n \"lodash\": {\n \"transform\": \"lodash\/${member}\",\n \"preventFullImport\": true\n }\n }]\n]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具有如下效果:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nimport { map, some } from 'lodash'\n\/\/ 被替換爲\nimport map from 'lodash\/map'\nimport some from 'lodash\/some'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"注意這個選項 preventFullImport 在引入整個庫的時候會讓插件拋出異常。"}]},{"type":"heading","attrs":{"align":null,"level":5},"content":[{"type":"text","text":"通過 ESLint 規則配置"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在ESLint中配置 "},{"type":"link","attrs":{"href":"https:\/\/eslint.org\/docs\/rules\/no-restricted-imports","title":null,"type":null},"content":[{"type":"text","text":"no-restricted-imports"}]},{"type":"text","text":" 規則,也可以在全局引入時拋出異常。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n# .eslintrc\n\"no-restricted-imports\": [\n \"error\",\n {\n \"paths\": [\n \"lodash\"\n ]\n }\n]"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在如下引入方式時會拋出異常:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":"br"}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"import { map } from 'lodash'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但按照這樣編寫則不會報錯:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":null},"content":[{"type":"text","text":"import map from 'lodash\/map'"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"具體使用方法可查看該規則說明,可以對引入模塊的代碼風格進行控制。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"5.1.3 刪除可替代的類庫,重寫方法實現"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"使用功能齊全的工具性函數是非常誘人的,可以快速交付,或者是能夠對未來的功能進行快速實現。但是過度實現增加了目前不需要的代碼,其造成的複雜性,會對 bundle 的大小產生一定的影響。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在我們的項目中使用到的是 Lodash,官方雖然指出只引入對應模塊就會便捷很多。但 Lodash 依然有很多存在依賴關係的內部函數需要一起打包進去。如果你僅僅是使用到這個實用庫類的部分工具函數,那麼可以用一些體積更小的工具包進行優化,或者直接使用對應的原生實現方式進行替換。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"如果我們的對於項目代碼中的依賴關係,只引入了一小部分相關內容,並且可以在合理的時間內對其進行重寫。那麼我們應該重寫這部分代碼,以達到優化冗餘代碼的目的。把項目中涉及到的工具庫類函數直接用原生代碼替換,不失爲一個很好的解決方案。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"以下是原生 JavaScript 實現 Lodash 的 debounce 函數:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nfunction debounce(func, wait, immediate) {\n var timeout;\n return function() {\n var context = this, args = arguments;\n clearTimeout(timeout);\n timeout = setTimeout(function() {\n timeout = null;\n if (!immediate) func.apply(context, args);\n }, wait);\n if (immediate && !timeout) func.apply(context, args);\n };\n}\n\/\/ Avoid costly calculations while the window size is in flux.\njQuery(window).on('resize', debounce(calculateLayout, 150));"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.2 替換package中不必要的模塊"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這裏的替換掉不必要的組件\/模塊,更多地是從業務邏輯方面來說的。如果已經引用的庫裏面存在某些業務邏輯功能,或者有公用的組件已經實現了對應的功能,那麼我們應該進行替換,刪除掉多餘的業務內代碼。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"同樣的,檢查下 package.json 文件中也許會存在未使用的包,或者是重複功能。在開發階段,也許會存在引用了某些庫類,隨着業務變化,又在具體邏輯中刪除了引用,但未清除徹底,導致 package 中還有殘餘,卻給 bundle size 帶來了一定的負擔。也或者是同上面 lodash 和 moment 庫,可以通過用一些更簡單的庫,或者自己實現幾個常用功能來進行整個模塊的替換。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這裏對於我們的業務包來說,包內存在以下這些問題:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)把業務內不必要的組件替換爲公用組件"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)刪除不必要的 node_modules 模塊,或者用其他模塊替代"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在這個層面上來說,是細粒度上面的代碼冗餘的清理,包括下線實驗代碼的處理工作等等。"}]},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.3 代碼拆分"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"除了頁面展示需要的代碼模塊以外,不應該加載多餘的代碼邏輯。對於不同的業務固然有不同的方法,但核心的兩個主要方法是:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"bulletedlist","content":[{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於路由的代碼拆分"}]}]},{"type":"listitem","attrs":{"listStyle":null},"content":[{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"基於功能\/組件的代碼拆分"}]}]}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"1) 使用 Ctrip React Native 的 lazyRequire 方案"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"React Native 官方提供的 require 目前並不支持動態加載,所以 CRN 框架提供了 lazyRequire來支持懶加載方案。App組件將在跳轉頁面的時候再加載該模塊。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nlet page = lazyRequire('.\/src\/Page.js'); \nconst pages = [\n {\n component: page,\n name:'page',\n isInitialPage: true\n }\n];"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"2) 使用 require 延遲加載"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"我們可以通過內聯引用的方式,延遲模塊或文件的加載,直到實際需要該文件。但如上所說,目前 React Native並不支持動態加載,所以需要 state 屬性去控制是否引入對應模塊。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nimport React, { Component } from 'react';\nimport { TouchableOpacity, View, Text } from 'react-native';\n\nlet VeryExpensive = null;\n\nexport default class Optimized extends Component {\n state = { needsExpensive: false };\n\n didPress = () => {\n if (VeryExpensive == null) {\n VeryExpensive = require('.\/VeryExpensive').default;\n }\n\n this.setState(() => ({\n needsExpensive: true,\n }));\n };\n\n render() {\n return (\n \n \n Load\n \n {this.state.needsExpensive ? : null}\n \n );\n }\n}"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"heading","attrs":{"align":null,"level":3},"content":[{"type":"text","text":"5.4 靜態資源優化、靜態代碼檢查"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"5.4.1 圖片資源優化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 bundle 分析截圖中,到目前爲止雖然已經解決了一些庫類引用不合理的問題,但還存在邏輯文件過長的問題,分析後發現是裏面包含了超大的base64圖片。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"base64 更適合出現在一些重複使用的背景圖片,或者尺寸極小的 ICON 的情形,而一些較大的圖片則適合使用 PNG 或者 JPEG 。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"PNG 是無損的,JPEG 是有損的。如果不需要背景透明,那麼把 PNG 轉換爲 JPEG 會更節省空間。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)剪裁圖片大小:設計師給出的圖片一般會比較大,而實際應用中不需要這麼大的圖片,可以適當地進行圖片大小的裁剪。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)壓縮圖片質量:對圖片進行無損壓縮後,再進行 base64 使用。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"經過以上兩個步驟以後,base64 的圖片字節數會明顯減少很多。如果字節數還是很大,那麼應該考慮是否不適合使用 base64 進行展示。"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"5.4.2 ESLint 檢測 React Native 的 CSS 冗餘"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在 React Native 的 ESLint 規則中配置 react-native\/no-unused-styles ,會檢測在 React 組件中存在的未使用 CSS 。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在長期對組件進行開發的過程中,隨着 UX\/UI 的更改,會存在一些冗餘的樣式散落在文件中。這樣的一個配置可以很好地顯示出冗餘的部分。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\n# .eslintrc\n \"rules\": {\n \"react-native\/no-unused-styles\": 2\n }"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但它存在很明顯的缺陷,就是在 css-in-js 的寫法中可以檢測到當前文件中的 css 對象引用,也就是對於 styles.xxx 可以很好地檢測到。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":"br"}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"但是對於以下幾種,目前這個規則都無法檢測:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)Import 進來的 CSS 文件無法識別;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)使用 StyleSheet.flatten 等方法操作過的 style 無法識別;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nconst styles = StyleSheet.flatten([style1, style2])\n\/\/ 無法檢測到該對象中存在的樣式"}]},{"type":"heading","attrs":{"align":null,"level":4},"content":[{"type":"text","text":"3)CSS 對象初始化與使用名稱不同時,無法識別。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"codeblock","attrs":{"lang":"text"},"content":[{"type":"text","text":"\nconst A = StyleSheet.create({\n header: {\n color: \"#61dafb\",\n fontSize: 30,\n marginBottom: 36\n }\n})\n\nconst styles = IS_BOOL ? A : B\n\/\/ 無法檢測到 A\/B 中存在的樣式"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"這些問題官方目前都未修復。這個規則只能檢測最簡單形式的 CSS 內容中的冗餘,如果希望一直能使用該規則,則在代碼規範上需要保持簡潔的 CSS 引用形式。"}]},{"type":"heading","attrs":{"align":null,"level":2},"content":[{"type":"text","text":"六、結束語"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在經過一系列的分析與細節的優化操作過後,成果是壓縮後的 Bundle Size 減少了約 50% 的空間佔比。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"image","attrs":{"src":"https:\/\/static001.geekbang.org\/infoq\/ab\/ab6296771a3ed46be2b5b4f99e157c1d.png","alt":"圖片","title":null,"style":[{"key":"width","value":"75%"},{"key":"bordertype","value":"none"}],"href":null,"fromPaste":true,"pastePass":true}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"進行 bundle 分析後,可以明顯地找出尺寸異常的文件或者模塊,進行對應的優化,從大的層面上進行分析與尺寸優化:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)根據 bundle 分析裁剪具體模塊,分析模塊引用代價;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)替換不合理的庫類引用"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"粗粒度的優化後,剩下的有關邏輯的代碼優化,就跟平時的編寫有關。從小的層面上進行優化需要:"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"1)從邏輯上分析不必要存在的庫類\/模塊引用;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"2)編寫邏輯代碼時,需要更加註重保持代碼行數的簡潔;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"3)提取常用功能爲公用組件進行使用;"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"4)靜態資源使用優化"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"在代碼編寫階段保持最佳實踐是最好的,但在中後期我們也能通過一些分析工具進行代碼包的裁剪。項目結構與模塊的依賴關係更加複雜的時候,運用以上方案進行 bundle 裁剪會更加有效。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"保持Bundle尺寸精簡是一個長期的任務,就像保持代碼簡潔一樣,甚至可以說這項任務與項目的生命週期是緊密相連的。有了良好的工具可以更加方便開發者進行分析,實時查看目前的代碼簡潔程度。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","marks":[{"type":"strong"}],"text":"作者簡介"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"Sheila,攜程資深前端開發工程師,關注前端性能優化;xqin,攜程前端開發專家,CRN bundle分析平臺開發者。"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null}},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"本文轉載自:攜程技術(ID:ctriptech)"}]},{"type":"paragraph","attrs":{"indent":0,"number":0,"align":null,"origin":null},"content":[{"type":"text","text":"原文鏈接:"},{"type":"link","attrs":{"href":"https:\/\/mp.weixin.qq.com\/s\/aajdqmpCLKvGaokL4Qp1tg","title":"xxx","type":null},"content":[{"type":"text","text":"乾貨 | 減少50%空間,攜程機票React Native Bundle 分析與優化"}]}]}]}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章