寫在前頭–在公司搬磚也差不多一年了,眼看著專案越來越大,優化問題亟待解決。優化是一件很矛盾的事情,但是為了詩和遠方,我們還是得走一趟坑坑窪窪的優化之路。
本文可能涉及的內容–
專案介紹
整個專案大概有60+個頁面,用到的元件大概150+,package裡面的依賴大概有70+個,應該勉強算得上是一箇中型的React的專案了。
下面給大家看看我們現在build一次專案的結果–
打包時間約150s,打包完之後的資源gzip之後約1.2m,儘管之前分離了一些公用依賴,但是index包的體積達到了600+還是令人難以接受的。
需要解決的問題 && 思考過的方案
開始優化之前,最重要的就是搞清楚我們到底要優化什麼。確定了優化的目標才能著手思考優化方案,進而實施優化方案。結合對專案的bundle分析以及自身對專案的瞭解,我們初步可以定出以下幾點優化方向–
① 體積瘦身
首先我們需要足夠了解我們的專案,才能著手進行瘦身。在這裡有一個很給力的工具可以推薦給大家webpack-bundle-analyzer
盜用一下github上的圖
如圖所示,我們可以很清晰的看到每個js檔案裡的module組成,還可以看到每個module的大小以及module的組成成分,這對我們分析程式碼冗餘以及優化方向都能夠提供很大的幫助。
具體食用方式也很簡單–
這樣一來我們就對專案有了一個比較具體的認識,大到專案的依賴一覽,小到某個頁面的元件引用都能在分析報告中找到。接下來就可以開始我們的瘦身之旅了。
打團先找大哥
當我們第一次看到bundle的分析報告時,總能找到一些出乎意料的“大個子”,如果是必不可缺的依賴則沒辦法,但如果是一些可以被取代的依賴就有別的說法了。這裡剛好可以看看之前我對create-react-app中moment.js依賴的處理,如果處理順利的話可以很直觀的看到bundle大小的變化。
總結來說,如果有小的並且滿足需求的依賴可以替換,請不要遲疑;但如果沒有可滿足的依賴,可以嘗試自己造一個輪子,當然後者需要結合自身狀況考慮。
分散站位
相信大家在開發網站的時候都用到了不少依賴,但是這些依賴在輸出之後是和業務程式碼打包在一起的,這個明顯不符合我們的預期。面對這些基本不會變更的依賴,我們更傾向它們能夠主動抱團並且遠離我們的業務程式碼。
這時候,就該使用webpack的CommonsChunkPlugin(貌似在wp4中已經被別的外掛替代了,在此我們先不討論wp4),它可以幫助我們將一些指定的module打包進指定的bundle裡。具體使用方案可以參考wp官網中的相關介紹,有一個坑點就是–倘若希望負責集合依賴bundle的檔名在打包時不變,則需要生成manifest。
不要將頁面都放到一個籃子裡(一)– 頁面分離
我們可以將一些低頻頁面徹底拉出專案,拿我們的專案來說,一共60+個頁面,使用者大多都是隻會訪問其中的幾個或十幾個頁面,不可能將所有的頁面都訪問一遍。這樣一來就必然會有一些頁面的訪問頻率相比之下會十分低下,該怎麼處理這些頁面呢?這裡有幾種方案:
- 脫離框架重寫相關頁面並重新部署
- Copy專案程式碼,在其他地方重新跑一次並部署,原專案就可以刪除不需要的頁面
- 上一方案的簡化版,復刻專案環境,跑一個新的純淨專案並部署,將原專案低頻頁面“剪下”到新的專案中
以上三種方案個人覺得各有優劣,第一個方案很簡單
,適用場景就是類似Q&A的靜態頁,可以將其脫離專案,寫成靜態頁部署在其他的地方,但是不好的地方就是可能沒法複用原專案元件。
方案二呢則是時間至上
的方案,可以做到快速遷移,但是不好的地方在於遷移出去的頁面其實還是塞在一個籃子裡,只不過換了個新的籃子罷了。
方案三則是質量至上
的方案,以時間作為成本,換來一個新的“低頻頁面專案”。具體要使用哪種方案,我覺得也是根據當前專案狀況而定,不追求最完美,追求最合適。
② 首屏載入
首屏載入,大概是優化的永恆話題,所有的優化都避不開這一個話題,因為只有它能最直觀的讓“大家”都感受到我們這次優化的成果。對於使用者來說,認為網頁首屏很快的標準其實很單一,就是一開啟頁面,看了多久的白屏。所以我們需要做的就是弱化使用者對白屏的感知
,圍繞這一點,個人認為,首屏載入這一優化可以有兩個方向:一個是速度,另一個是體驗。
引入載入佔位
其實這個就是前段時間很火的“骨架屏”,我們可以在頁面真正被渲染出來之前,先給使用者看到一個“假的”頁面,等到某個時間節點(例如資料已經準備完畢…)就將真正的內容替換上去。這裡有一個我寫的很不走心的例子:)
在這個優惠券列表頁面我的處理方案是,初始化頁面的時候就渲染3個列表項骨架,等待介面資料返回就將真實內容替換上去。
在我們的首屏其實也是類似的,我們可以根據首屏的展示結構,做一個匹配的骨架元件,然後按需求進行展示即可,這樣可以有效減少使用者看到白屏的時間。下面是我這個骨架的程式碼,優化的空間很大,不過由於優先順序不是很高,所以就沒有進行迭代了。
大概結構就是這樣,樣式方面很粗暴,因為每一項都是獨立的一個元件,直接可以用absolute定位堆砌一個簡潔的佔位列表項。裡面那個類似進度條的效果則是通過css3的animation實現的,我們可以將每個block的背景色變成漸變的,然後通過background-positon的變化來達到圖中的效果。
圖片懶載入
這應該是個老生常談的優化方向了,原理大概是將檢視之外的圖片都用同一個佔點陣圖進行佔位,將其真正的圖連結存在data-*中,通過監聽滾動來判斷圖片是否進入檢視中,來控制img標籤src的值。具體的實現很多地方都能搜到,大家可以根據自身情況,按需選擇。
不要將頁面都放到一個籃子裡(二)– 懶載入
其實在整個優化過程中我的重心是放在這個地方的,其他的都是半路上想到的…
讓我們回想一下,上面我們講過將低頻頁面分離,那麼,必然就有會那麼幾個訪問量十分高的頁面,那麼對於這幾個頁面應該怎麼辦呢?
因為訪問頻率高,所以我們可以認為這些頁面與我們的核心業務是強相關的,所以將其分離就顯得不那麼划算了(很可能會出現維護多套程式碼的窘況)。
但是這樣高頻頁面才是優化的重點區域呀,應該怎麼辦呢?面對這樣頁面我們還是可以使用懶載入大法(頁面懶載入 || 元件懶載入 || 依賴懶載入)。
想要在js層面實現各類懶載入,我們都需要藉助webpack中的特性Code Splitting,它可以將我們本來打包在一起的js分解成一塊一塊,並能達到按需載入並使用的效果。
- 頁面懶載入
因為我們使用了react-router,所以我們可以使用react-router的getComponent輕鬆達到頁面懶載入這一需求。如下圖所示,將mainpage這樣引入route的話,在打包的時候會將其分離成一個獨立的js。
- 元件懶載入 && 依賴懶載入
元件和依賴的懶載入也是十分簡單的,如下圖這樣寫就能達到懶載入的效果,但如果我們使用了babel則需要修改一下babel的配置,讓它能夠順利解析動態import()的語法。
③ 打包提速
我們通常的優化都是為了使用者而優化,但其實為了我們自身能夠良好的開發體驗,也應該為開發人員優化優化開發體驗,打包優化則成了不二之選。
使用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的配置有基本的認知,緊接下來就是建目錄、裝依賴巴拉巴拉。最終我們會得到一個這樣的目錄結構–
- 寫個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裡)
- 仔細研讀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打包的),效果可能沒有想象中那麼理想吧,但也算往前踏出很不容易的一步了:)
其實,能夠優化的還有很多很多,請求方面、業務方面甚至是程式碼寫法…都是可以優化的,但是這些怎麼能一蹴而就呢?還是得走一步,看一步,選擇最適合自家專案的優化方案才是最佳方案~