老專案軟體升級評測:痛苦的過程變成寶貴的經驗全記錄

Next.js v14 推出,是否該升級!?
Next.js v14 推出,是否該升級!?
此篇分享一個舊專案在框架升級的過程中經歷的困難和痛點,透過工具分析來看升級效能的差異,並在最後做了關鍵性的決策後成為了效能提升的關鍵。

隨著框架 NextJS 舉辦了發表會,讓我突然間有興致的逛了一下 GitHub 陳年老舊的專案列表,發現有個 NextJS 的專案才兩年不見就差了四個版本,你可知道在軟體界四個版本號已經是非常遙遠。最新版本一定比較快或比較優嗎?為了效率而更新框架是值得的投資嗎?為了想要印證這件事,我投入了一整天的時間 get my hands dirty,實際的走過一遍,確認專案更新後網頁效能是否有如官方所說,快上許多!可惜軟體專案升級的過程雷點多,不像手機或電腦按個按鈕這麼簡單而已。

這個專案是 甜約翰樂團的官方入口連結網站,原始碼我會放在頁面的結尾。當初的概念是想要做一個 link tree 復刻,網頁是自己的,資料也是自己的,未來有客製化需求都能自行更動,想不到演出一忙就是兩年後再見。其實程式碼對於工程師來說,兩週不見就差不多忘光了,遑論兩年。

 

升級前的樣貌

在專案升級前,我會先評估目前專案的健康狀況,從套件清單、程式碼架構、渲染方式、打包分析,以及網站的跑分測試。

  • 審視套件清單,查看專案的  package.json
"dependencies": {
   "@chakra-ui/react": "^1.6.1",
   "@emotion/react": "^11",
   "@emotion/styled": "^11",
   "framer-motion": "^4",
   "next": "10.2.0",
   "next-seo": "^4.24.0",
   "react": "17.0.2",
   "react-dom": "17.0.2",
   "react-icons": "^4.2.0"
 }

除了原本的主角群 next, react, react-dom ,其他的配角都算是擴充套件,這對於一個專案而言並不意外,畢竟一個系統是由多個小程式所組成,而升級困難的點,就是有太多軟體要一起升級才行。

  • 程式碼架構:使用 Next 傳統既有的 Page Router , 也就是裡面有包含實作 SSRgetServerSideProps 函式。

關於 SSR (Sider-side Rendering) 的網頁渲染技巧,可以參考我之前寫的文章:《一次看懂現代網站的渲染招式以及名詞解釋》

Bundle Analyzer 分析結果

bundle analyzer 成份比例

此專案確實使用了 SSR 的渲染方式。成分表中佔用最大空間的套件,除了基本必須的 React DOM 以及 next 以外,還有動畫工具 Framer motion,而這些都是未來可以優化瘦身的地方。

PageSpeed Insights-手機跑分測試.jpg

PageSpeedInsides 電腦跑分測試.jpg

PageSpeed Insights 它背後使用了一個獨立運作的 Lighthouse 做網頁載入的跑分測試,我在《我的第一篇文章!公開此網站建置的所有組合技》這篇文章裡有提到,它是網站評估載入效能的好選擇。

成績單公布,手機版網頁居然只有 70 分!那我們就著手開始升級吧。

 

升級開始

Next.js 目前專案的版本是 v10.2.0, 而這次 Next.js Conf 最新發表會的版本是 v14, 差了四個大的版本號,我預想升級之路會有不少痛苦的過程,就來吧!畢竟工程師是很耐痛的。

我升級的第一步,一定是拜讀框架的 升級文件,看了看會發現只有 v13 到 v14,沒有 v10 直接到 v14。我從 v10 升級文件一路看上來,瞭解大致上 Next.js 的升級方式,只需要處理 next, react, react-dom, 以及開發用的相依套件 eslint:

npm install next@latest react@latest react-dom@latest
npm install -D eslint-config-next@latest

套件更新大致上是這個樣子的:

next_upgrade_diff_low.jpeg

執行開發模式,跑跑看結果如何:

npm run dev

執行失敗,果然沒這麼幸運:

Request-is-not-defined.jpeg

升級後會遇到錯誤,想必是理所當然。第一個會踩到的雷點,就是 API 改變的問題。

 

API 不相容所造成的系統錯誤

消化一下錯誤訊息,可以發現這是由 NextRequest 所衍生出來的問題,經由網路搜索,找到 Next.js 在升級後使用了更新版本的 Node.js API Request已不相容我手上的 v16,因為他最低要求是 v18 以上。

<解法> 切換 Node.js 的版本

於是乎,我用 Node.js 的版本切換工具 n 來速速 從 v16 到 v18,然後再跑跑看:

node-v18-upgrade.jpg

居然成功了!原來升級跳了四個版本號居然如此無痛,順便偉哉 NextJS 團隊。

💡 使用 n 能快速且無負擔的切換 Node.js 版本,有時候對於解決 Build Time 所產生的錯誤,不失為一個好方法。

 

我開開心心的打開瀏覽器,然後下巴掉在地上:

nextjs-upgrade-error.jpg

 

修了一個 BUG ,但是卻多了 10 個

這是軟體業界很常見的事,需要你夠耐痛,都可以撐過去。

靜靜剖析畫面中的錯誤訊息,可以發現兩個問題:

  1. ReactDOM.render is not supported in React 18.
  2. Expected server HTML to contain a matching <a> in <a>.

第一個問題,是由於所使用的擴充套件  React UI  函式庫 Chakra 版本過舊,內部的 React 版本沒有一起升級所導致,而這個很明顯是 API 更新造成不相容的系統錯誤。ReactDOM 在 v18 後從原本的 ReactDOM.render 變成了 ReactDOM.createRoot,而解決辦法不是升級 Chakra,就是用別的相容套件來取代。

第二個問題,錯誤原因是 Improper HTML Structure 所導致的 Hydration Error,也就是 Server side 在 SSR 過程中比對了 Client-side 的 HTML 後發現不一致所造成的錯誤,這個在 React 官方 Repository 的 ISSUE #24519 底下有非常好的討論串和詳細的解釋。這種 Hydration 錯誤常見的型態,大部分是 <div> 被包在 <p> 底下,或是 <a> 被包在 <a> 底下。至於為何這邊有 <a> 包在 <a> 底下的狀況,仔細檢查程式碼後,發現來自 next/link 的連結組件 <Link> 其 Component API 更新了。

< 解決問題 1.> 升級 React Component UI

根據 Chakra 官方文件表示,升級必須同步伴隨相依套件,也就是所謂的 peer dependency,而除了 React v18 是為必須,在本專案中還有樣式工具 emotion 以及動畫工具 framer motion,我們就一行指令一次升級吧:

npm install @chakra-ui/react@latest @emotion/react@latest @emotion/styled@latest framer-motion@latest

chakra_ui_upgrade.jpeg

< 解決問題 2.> 升級 Next 的 Link Component API

<Link> 以往都需要包在 <a> 標籤的父層,但是自從 Next v11 開始,他自己就變成了 <a>,不需要在包覆任何的 <a> ,而在當時提供了一個 legacyBehavior 的 props 當作維持原狀的過渡期,否則你就必須要把底下的 <a> 元件給砍掉。

upgrade_link_component.jpeg

官方有提供 Codemods 的懶人程式,使用這個工具可以快速幫你改 Code 需要升級的部分。

在同步升級了兩個專案相依的組件以及 API 寫法之後,我們再度執行看看:

final_result_after_upgrade.jpeg

看起來是順利的升級成功了。

 

升級後變得如何

在升級了 Next.js v14、React v18 以及其他相依模組之後,我們按照慣例使用手上的工具進行評分。

  • PageSpeed Insights 跑分測試:

PageSpeed Insides Mobile 升級後跑分.jpg

PageSpeed Insides Desktop 升級後跑分.jpg

  • Bundle Analyzer 分析打包成份:

Bundle Analyzer 升級後結果.jpg

比較升級前後,手機版效能從 70 分進步到 76 分,桌機版從 98 分變成了 100 分!網站的打包成果從 141kB 變成了 135kB,成功瘦身 6kB。

 

軟體升級的過程,我學到了什麼?

這次框架大版號的升級,所投資的時間成本除了框架本身,其餘連帶的相依套件都必須評估進去。升級的成效,從效能的角度來看,提升了 8.5%,而整體的載入大小瘦身了 4.2%,綜觀來說只能算是微小幅度的一次升級。儘管如此, 如果團隊因此而不作為,只會放任專案舊下去,舊到天荒地老,成為同事眼中的 Legacy ,新人們口耳相傳的 Legendary ,最終只會加速面臨刪除重做的命運。其實在升級之後,你獲得除了所謂的效能上的提升,其實還有更好的資安防護、更多的功能、更新的版本號、更快的開發速度和順暢的體驗,背後當然也修正了許多的 BUG。升級是讓你體驗擁有更多「選擇權」所帶來自由的價值,熟悉的版本號也可以給新進同事更快上手的程式碼,大大降低專案維護和軟體開發的時間成本。

 

策略比新功能更重要

Next.js 除了其知名的 SSR 渲染方式,其實還有 SSG (Static Site Generation)。在當時的時空背景下所追求的新潮流 SSR 於效能表現來說,其實不如老舊的 SSG,因為兩者應用場景不同。以此網站來說,資料都是經常不變的固定資料,其實像這樣的使用場景會更適合 SSG ,而不是需要應付資料多變的 SSR

在我靈機一動之下,嘗試了把網頁的 SSR 改成了 SSG

SSR-to-SSG-codemods

第一頁的 HTML 會在 Build Time 的過程被完美生成為靜態網站,而我再次透過 Bundle Analyzer 的分析結果如下:

Route (pages)                              Size     First Load JS
┌ ● / (387 ms)                             51.8 kB         135 kB
├   /_app                                  0 B            83.2 kB
└ ○ /404                                   182 B          83.4 kB
+ First Load JS shared by all              85.4 kB
  ├ chunks/framework-6ab2e0a3e5a0915a.js   45.5 kB
  ├ chunks/main-276604b6d612fdd7.js        35.3 kB
  ├ chunks/pages/_app-3e769653da6b08ca.js  1.6 kB
  ├ chunks/webpack-efdab446b2c4fcf1.js     776 B
  └ css/91ce690993d85f60.css               2.2 kB

○  (Static)  prerendered as static HTML
●  (SSG)     prerendered as static HTML (uses getStaticProps)

現在果然沒有任何頁面是 SSR,且讓我帶著全靜態網站的方式看看 PageSpeed Insights 的跑分結果:

PageSpeed Insights-SSG.jpg

手機版的效能從 76 分變成了 95 分,跟升級相比提升了 35% 。因此,在追求升級的道路上,在獲得更多選擇的情況下,是否可以走上適合專案的使用場景,讓利益最大化才是。

 

未來效能優化之路

在專案升級的過程中,有很多的節骨眼都有想過,走向不同的方式是否更好,例如:

  1. 使用了 Chakra UI  帶來了更多的 peer dependencies,是否換上更輕量的 TailwindCSS 就好?
  2. Next.js v13 的 React Server Component 是否能幫助 Client side 的打包成品瘦身?瘦身的程度是多少?載入速度能有多少的提升?

以上都是需要工程師親自實作驗證,而過程中必定歷經不少痛苦的折磨;能享受這個痛苦過程的工程師,通常都是活得不錯。

如果你對甜約翰連結入口網站的原始碼有興趣,可以到以下連結自行欣賞:

brandimage
關於作者 Jay Chung
一位熱愛新技術的鼓手。最新資訊發佈在 Twitter ,演出資訊在 Instgram ,而文章都保留在這裡。