React中型專案的優化實踐

mykurisu發表於2019-03-02

寫在前頭--在公司搬磚也差不多一年了,眼看著專案越來越大,優化問題亟待解決。優化是一件很矛盾的事情,但是為了詩和遠方,我們還是得走一趟坑坑窪窪的優化之路。

本文可能涉及的內容--

React中型專案的優化實踐

專案介紹

整個專案大概有60+個頁面,用到的元件大概150+,package裡面的依賴大概有70+個,應該勉強算得上是一箇中型的React的專案了。

下面給大家看看我們現在build一次專案的結果--

React中型專案的優化實踐

打包時間約150s,打包完之後的資源gzip之後約1.2m,儘管之前分離了一些公用依賴,但是index包的體積達到了600+還是令人難以接受的。

需要解決的問題 && 思考過的方案

開始優化之前,最重要的就是搞清楚我們到底要優化什麼。確定了優化的目標才能著手思考優化方案,進而實施優化方案。結合對專案的bundle分析以及自身對專案的瞭解,我們初步可以定出以下幾點優化方向--

① 體積瘦身

首先我們需要足夠了解我們的專案,才能著手進行瘦身。在這裡有一個很給力的工具可以推薦給大家webpack-bundle-analyzer

盜用一下github上的圖

React中型專案的優化實踐

如圖所示,我們可以很清晰的看到每個js檔案裡的module組成,還可以看到每個module的大小以及module的組成成分,這對我們分析程式碼冗餘以及優化方向都能夠提供很大的幫助。

具體食用方式也很簡單--

React中型專案的優化實踐

這樣一來我們就對專案有了一個比較具體的認識,大到專案的依賴一覽,小到某個頁面的元件引用都能在分析報告中找到。接下來就可以開始我們的瘦身之旅了。

打團先找大哥

當我們第一次看到bundle的分析報告時,總能找到一些出乎意料的“大個子”,如果是必不可缺的依賴則沒辦法,但如果是一些可以被取代的依賴就有別的說法了。這裡剛好可以看看之前我對create-react-app中moment.js依賴的處理,如果處理順利的話可以很直觀的看到bundle大小的變化。

總結來說,如果有小的並且滿足需求的依賴可以替換,請不要遲疑;但如果沒有可滿足的依賴,可以嘗試自己造一個輪子,當然後者需要結合自身狀況考慮。

分散站位

相信大家在開發網站的時候都用到了不少依賴,但是這些依賴在輸出之後是和業務程式碼打包在一起的,這個明顯不符合我們的預期。面對這些基本不會變更的依賴,我們更傾向它們能夠主動抱團並且遠離我們的業務程式碼。

這時候,就該使用webpack的CommonsChunkPlugin(貌似在wp4中已經被別的外掛替代了,在此我們先不討論wp4),它可以幫助我們將一些指定的module打包進指定的bundle裡。具體使用方案可以參考wp官網中的相關介紹,有一個坑點就是--倘若希望負責集合依賴bundle的檔名在打包時不變,則需要生成manifest。

不要將頁面都放到一個籃子裡(一)-- 頁面分離

我們可以將一些低頻頁面徹底拉出專案,拿我們的專案來說,一共60+個頁面,使用者大多都是隻會訪問其中的幾個或十幾個頁面,不可能將所有的頁面都訪問一遍。這樣一來就必然會有一些頁面的訪問頻率相比之下會十分低下,該怎麼處理這些頁面呢?這裡有幾種方案:

  • 脫離框架重寫相關頁面並重新部署
  • Copy專案程式碼,在其他地方重新跑一次並部署,原專案就可以刪除不需要的頁面
  • 上一方案的簡化版,復刻專案環境,跑一個新的純淨專案並部署,將原專案低頻頁面“剪下”到新的專案中

以上三種方案個人覺得各有優劣,第一個方案很簡單,適用場景就是類似Q&A的靜態頁,可以將其脫離專案,寫成靜態頁部署在其他的地方,但是不好的地方就是可能沒法複用原專案元件。

方案二呢則是時間至上的方案,可以做到快速遷移,但是不好的地方在於遷移出去的頁面其實還是塞在一個籃子裡,只不過換了個新的籃子罷了。

方案三則是質量至上的方案,以時間作為成本,換來一個新的“低頻頁面專案”。具體要使用哪種方案,我覺得也是根據當前專案狀況而定,不追求最完美,追求最合適。

② 首屏載入

首屏載入,大概是優化的永恆話題,所有的優化都避不開這一個話題,因為只有它能最直觀的讓“大家”都感受到我們這次優化的成果。對於使用者來說,認為網頁首屏很快的標準其實很單一,就是一開啟頁面,看了多久的白屏。所以我們需要做的就是弱化使用者對白屏的感知,圍繞這一點,個人認為,首屏載入這一優化可以有兩個方向:一個是速度,另一個是體驗。

引入載入佔位

其實這個就是前段時間很火的“骨架屏”,我們可以在頁面真正被渲染出來之前,先給使用者看到一個“假的”頁面,等到某個時間節點(例如資料已經準備完畢...)就將真正的內容替換上去。這裡有一個我寫的很不走心的例子:)

React中型專案的優化實踐

在這個優惠券列表頁面我的處理方案是,初始化頁面的時候就渲染3個列表項骨架,等待介面資料返回就將真實內容替換上去。

在我們的首屏其實也是類似的,我們可以根據首屏的展示結構,做一個匹配的骨架元件,然後按需求進行展示即可,這樣可以有效減少使用者看到白屏的時間。下面是我這個骨架的程式碼,優化的空間很大,不過由於優先順序不是很高,所以就沒有進行迭代了。

React中型專案的優化實踐

大概結構就是這樣,樣式方面很粗暴,因為每一項都是獨立的一個元件,直接可以用absolute定位堆砌一個簡潔的佔位列表項。裡面那個類似進度條的效果則是通過css3的animation實現的,我們可以將每個block的背景色變成漸變的,然後通過background-positon的變化來達到圖中的效果。

圖片懶載入

這應該是個老生常談的優化方向了,原理大概是將檢視之外的圖片都用同一個佔點陣圖進行佔位,將其真正的圖連結存在data-*中,通過監聽滾動來判斷圖片是否進入檢視中,來控制img標籤src的值。具體的實現很多地方都能搜到,大家可以根據自身情況,按需選擇。

不要將頁面都放到一個籃子裡(二)-- 懶載入

其實在整個優化過程中我的重心是放在這個地方的,其他的都是半路上想到的...

讓我們回想一下,上面我們講過將低頻頁面分離,那麼,必然就有會那麼幾個訪問量十分高的頁面,那麼對於這幾個頁面應該怎麼辦呢?

因為訪問頻率高,所以我們可以認為這些頁面與我們的核心業務是強相關的,所以將其分離就顯得不那麼划算了(很可能會出現維護多套程式碼的窘況)。

但是這樣高頻頁面才是優化的重點區域呀,應該怎麼辦呢?面對這樣頁面我們還是可以使用懶載入大法(頁面懶載入 || 元件懶載入 || 依賴懶載入)。

想要在js層面實現各類懶載入,我們都需要藉助webpack中的特性Code Splitting,它可以將我們本來打包在一起的js分解成一塊一塊,並能達到按需載入並使用的效果。

  • 頁面懶載入

因為我們使用了react-router,所以我們可以使用react-router的getComponent輕鬆達到頁面懶載入這一需求。如下圖所示,將mainpage這樣引入route的話,在打包的時候會將其分離成一個獨立的js。

React中型專案的優化實踐
  • 元件懶載入 && 依賴懶載入

元件和依賴的懶載入也是十分簡單的,如下圖這樣寫就能達到懶載入的效果,但如果我們使用了babel則需要修改一下babel的配置,讓它能夠順利解析動態import()的語法。

React中型專案的優化實踐

③ 打包提速

我們通常的優化都是為了使用者而優化,但其實為了我們自身能夠良好的開發體驗,也應該為開發人員優化優化開發體驗,打包優化則成了不二之選。

使用DLL為打包保駕護航

由於時間原因,在公司的專案中並沒有嘗試使用DLL,但是看到網上有不少同學都推薦介紹了它,所以我選擇在此提及一下~有關於webpack DLL的文件

將webpack版本從2.0 --> 4.x

由於專案是在差不多一年多以前正式啟動的,所以接手的時候是webpack1.x,在剛接手的時候為了懶載入硬是升級到了2.x。

但是到現在發現,2.x好像也不夠用了,畢竟已經落下了兩個大版本了,更新之後的新特性、新功能或是新優化都應該成為我將專案遷移至新版本的動力。wp4具體的配置細節,在掘金上就見到過挺多同學介紹的,這裡我想介紹一下,我是怎麼將舊專案遷移到wp4的:

在開始進行版本遷移之前,我設想了兩個方案,一個是在原有專案上直接升級並修改配置;第二個方案是新建webpack4專案,搭建好之後將業務程式碼遷移過來。

經過對成功率以及時間成本的評估,我最後選擇的是第二個方案。那麼這個新建的專案應該完善到什麼程度才能進行遷移呢?我個人是經過以下幾個步驟--

  • 搭建專案骨架

這回的專案和之前的都不太一樣了,我們沒有藉助大神們的腳手架來搭建專案骨架了,我們需要自己從零開始一點一點的摸索webpack的用法以及新舊版本的差異。

關於一些基礎的知識以及配置十分推薦查閱webpack官網的文件以及一些之前參考過的文章webpack4-用之初體驗webpack 4.0.0-beta.0 新特性介紹

相信大家看完這些之後都會對wp的配置有基本的認知,緊接下來就是建目錄、裝依賴巴拉巴拉。最終我們會得到一個這樣的目錄結構--

React中型專案的優化實踐
  • 寫個Hello World!

我們應該如何判斷將專案程式碼遷入新專案的時機呢?很簡單,當這個新專案可以正常的除錯或打包一個相應框架的Hello World即可。

拿我們的專案來說,搭建完專案目錄以及一些基礎配置之後,接下來就是完全模擬原有專案的技術棧,在新的專案中寫幾個簡單的demo頁。當然這些demo頁並不是隨便寫的,是帶有目的性的,按我這次的經歷來說,我寫了這麼幾個檔案index.js、App.js、Hello.js、Global.scss、router.js。

剝開非核心依賴,我們最核心的依賴其實就是react & react-router & sass,只要webpack能夠正確的解析es6和sass我們就能很大程度的還原舊專案的環境。(babel中與jsx相關的配置在package裡)

React中型專案的優化實踐
  • 仔細研讀package

上面的demo完成之後,我們新專案就初具雛形了,接下來我們就需要將舊專案package.json遷移到新專案中,這裡需要注意的幾點是:

① "scripts"中的指令要注意,我們要看裡面的每條指令分別有什麼作用,然後再思考應該怎麼在新專案中寫一個功能一樣的指令。

② "dependencies" && "devDependencies"舊專案的依賴也應該無縫遷移過來,不過我們可以趁這個機會把沒有用到的依賴剔除出去。

③ "babel" || "autoprefixer"等輔助工具的配置也應該與舊專案保持一致。

  • 遷移專案&修修補補

上述步驟都跑通之後,就能刪掉原有的demo,將舊專案的所有業務程式碼都遷移過來。接下來就看著報錯,一個一個修復即可。這裡遇到這麼幾個坑,困擾了我許久。頁面很順利的遷移過來了,依賴補全之後也順利的跑起來了。

但是在dev環境下切換頁面老是會404。相信大家看到這裡就懂了,我用了history模式的路由,在devServer中應該要加上這樣一行配置

devServer {
    historyApiFallback: true
}
複製程式碼

好啦,404消滅之後又有新的狀況了,靜態資源老是引用不到,這是為啥?其實這是因為我們在output的時候沒有設定publicPath引起的,在dev的webpack.config中我的output是這樣配置的

output: {
    path: path.resolve('dist'),
    publicPath: '/'
},
// ...
devServer = {
    contentBase: './dist',
    port: 9000,
    historyApiFallback: true
}
複製程式碼

這個問題解決之後,我們的開發環境算是還原的差不多了。接下來就該踩踩打包的坑了,我遇到的第一個問題就是,打包完成之後,資料夾裡面只有打包輸出,index.html咋不見了...這說好的不太一樣。後面發現是少了copy-webpack-plugin

const CopyWebpackPlugin = require('copy-webpack-plugin')
const config = {
    // ...
    plugins: [
        new CopyWebpackPlugin([{from: 'public', to: ''}])
    ]
}
複製程式碼

加上這個依賴之後,我們public資料夾的內容就會乖乖的在dist裡面出現。但緊接著又出現新的問題了,第二次打包的時候,怎麼dist沒有被清空呢?和上面一樣,年輕的我少用了一個外掛clean-webpack-plugin

const CleanWebpackPlugin = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

let cleanOptions = {
    root: path.join(__dirname, '..'),
    verbose: true,
    dry: false
}

const config = {
    // ...
    plugins: [
        new CleanWebpackPlugin(['dist'], cleanOptions),
        new CopyWebpackPlugin([{from: 'public', to: ''}])
    ]
}
複製程式碼

完成上述配置後,每次打包webpack都會清空dist資料夾,並且在打包完成之後,將public中的內容複製到dist。好了看來應該可以了,但是在本地開了個伺服器跑頁面的時候發現,各種靜態資源404。這又是什麼玩意?實話說在這裡踩坑時間是最多的,但是解決方案又是令人窒息的簡單...都怪自己沒有好好看文件

const CleanWebpackPlugin = require('clean-webpack-plugin')
const CopyWebpackPlugin = require('copy-webpack-plugin')

let cleanOptions = {
    root: path.join(__dirname, '..'),
    verbose: true,
    dry: false
}

const config = {
    output: {
        filename: 'static/js/[name].[chunkhash:8].js',
        chunkFilename: 'static/js/[name].[chunkhash:8].chunk.js',
        publicPath: 'http://localhost:5000/' // !!!這裡一定要使用絕對路徑,不然就會被坑到
    }
    // ...
    plugins: [
        new CleanWebpackPlugin(['dist'], cleanOptions),
        new CopyWebpackPlugin([{from: 'public', to: ''}])
    ]
}
複製程式碼

總結

好了,不知道有多少同學會看到這裡,先謝謝大家看我在這嘮叨一堆~各類優化的方案在網上看了好多好多,但是好像大家都只講方案沒有涉及實踐,等到自己真正去玩的時候才發現,其實優化沒有想象中那麼簡單,要兼顧原有的,又要儘量使用更新更好的,很多時候都會在夾縫中取捨。

對了,好像忘記放懶載入優化後的圖了(還是wp2打包的),效果可能沒有想象中那麼理想吧,但也算往前踏出很不容易的一步了:)

React中型專案的優化實踐

其實,能夠優化的還有很多很多,請求方面、業務方面甚至是程式碼寫法...都是可以優化的,但是這些怎麼能一蹴而就呢?還是得走一步,看一步,選擇最適合自家專案的優化方案才是最佳方案~

React中型專案的優化實踐

相關文章