React Stack 開發體驗與優化策略
Landing Page for MCS Lite
傳統上,Landing Page 用純 HTML、CSS 即可解決。然而,當其它專案都陸續使用 React 完成時,考慮到維護整體專案的一致性,架構簡單的頁面也可以用 React 進行開發,本篇文章即是針對此方向分成兩個部分來討論介紹:
- PART I. React Stack 開發體驗與推薦工具
- PART II. 效能評估與優化策略
PART I.
在開始之前,先簡單將一般開發 React 的幾種 Stacks 作分類,以下依目前在 MediaTek 專案開發經驗中分為三類。
Stack A— Dashboard Console
使用者需先註冊並登入才能進行操作,這種類型包含了後台管理的 Admin 頁面、CMS (Content Management System)、數據呈現 Dashboard 等等的頁面,通常這種網站不需要直接被 Crawler 給爬取,沒有 SEO 的需求,畢竟怎麼爬都只能爬到登入頁,何況如果是內部系統比較沒什麼意義。所以一般做 CSR (Client Side Render) 的 SPA (Single Page Application) 就好。實際案例:
- MCS 物聯網裝置的 Dashboard Console:使用 Angular 開發。
- MediaTek Labs:使用 Laravel Blade 與 React 開發。
- MCS Lite Mobile Web:使用 CRA (Create-React-App) 開發。
Stack B — Dynamic Routing with Public Data
會依據 URL 動態的產生頁面 Content,並且有 SEO 需求。例如你想要讓平台上的資訊能很好的被搜尋且分享,一般是要做 SSR (Server Side Render) 的,使用 React renderToString 先將內容產生出來,這樣對於不同的搜尋引擎才能比較友善的被爬取。實際案例:
- MCS Public Page 分享裝置的公開頁面:React Isomorphic (架構參考)。
- 下個專案可能考慮的選擇:Next.js
可以的話應盡量避免 Stack B,畢竟多了一台 Server 需要管理,會在 Server 上發出 Pre-fetch HTTP Request,Debug 追蹤什麼的比較困難,而且如果只要考慮 Google 搜尋引擎,Stack A 事實上是能夠被 Render 解析。
Stack C — Landing Page
產品介紹頁面,通常是只有幾個 Static Page,SEO 非常的重要,但是相較於 Stack B 是有限的 Routing。實際案例:
- MCS Lite Landing Page:使用 CRA 加上後處理。
- 下個專案可能考慮的選擇:Gatsby.js
Stack C 其實就只是拿 Stack A 來用,而本篇主要在分享 MCS Lite Landing Page 的實作面。
Pre-Render
最簡單判斷網站 Render 的方法,是透過檢視 HTML 檔案網頁原始碼。SSR 或是 Pre-render 會將 Content 直接塞在 Body 裡面;而 SPA CSR 的 Body 則會是如下所示,只有一個進入點,也就是剩下的 Content 會在 Browser 上執行 JavaScript 後產生出來:
<body>
<div id="root">
<!-- 未使用 Pre-render -->
</div>
</body>
CRA 是建立 React 專案的利器,除了好維護外,也有大量的社群給予各種 Solution Best Practice 建議,其文檔本身就是一個很好的學習管道。其中有個段落提到 Pre-render 的做法,也就是可以採用 CRA 預設 CSR 的架構再簡單補上兩行程式碼,就能提前將 Content 產生在 HTML 上,對於 SEO 與效能上都會有所提升:
"build": "react-scripts build && react-snapshot"
剛好 MediaTek 有四種不同 Render 策略的實作,雖然不是所有專案都是相同的性質,網路狀況的基準也不一,還是可以多少看一下彼此的差異。下圖中,以會打 API 的 Homepage 的最下兩排作為對照組,供大家簡單參考看看。
Netlify Continuous Deployment
作為 Static Page 的專案很適合 Host 在 Netlify Service 上,比起 GitHub Pages 多了很多功能,例如每個 Commit/PR 都能 Build 出獨立的 URL 來做測試,甚至是能使用新的 Split Testing 功能,把同一個 URL 分流到不同 Branch 去來達成 A/B Testing 的設定;也提供免費的 HTTPS for Custom Domains(這一點 GitHub 似乎還不支援);我在 Build A Web App in MediaTek 提到 MCS Lite 是一個 Monorepo 類型的專案,也就是 Repository 可能會包含數個 Projects,如此可透過 Netlify 將環境個別且獨立的進行部署設定。
Netlify 同樣支援 SPA Client Side Routing,可以搭配 React-Intl 與 React-Router 來做多國語言的切換,Router 的設計如下:
<Route path="/" component={IntlProvider}>
<IndexRedirect to={browserLocale() || DEFAULT_LOCALE} />
<Route path=":locale" component={App} />
</Route>
至於多國語言翻譯與詳細流程設計,可以參考 I18n Workflow for React Project。
提供一個服務資訊:能留言請 Netlify 幫你開啟把 HTTP 導到 HTTPS 的功能(Free Subdomain)喔!一直覺得他們客服做得很好,私心推一下。
Google Analytics Autotrack
一般加載 GA Tracking Snippet 只會幫你打 Pageview,如果要觀察更多的 Event,需要自己將 Script 塞在 JS Code 中。而 Autotrack 是半官方提供的套件來簡化你的這些設定,其中涵蓋大部分開發者常用的觀察項目,可以使用 Async 的方式分別加載:
<script async src="https://www.google-analytics.com/analytics.js"></script>
<script async src="https://unpkg.com/autotrack/autotrack.js"></script>
例如外部連結事件、SPA 網址變動時自動打 Pageview、紀錄目前頁面的 Breakpoint、使用者最遠 Scroll 到頁面的比例、使用 Declarative 方式來埋 Event (下左圖示意)等等。例如下右圖為模擬當外部點擊事件發生時,統計當下螢幕的尺寸。
Status Monitor
對於線上狀態監控,最簡單可以採用 Postman 的 Monitor 功能,設定每隔幾小時或每天就打一次 API Test,可以寫簡單的 GET 測試,HTML 頁面是否如預期的返回 Content。
左圖紅色警訊即順便檢測不同語言的頁面是否 Pre-render 成功,寫在 Postman 語法大致上像是:
tests["description is correct"] = responseBody.includes("MCS Lite 是為了需要...");
Google Webmaster (Search Console)
完成部署與監測,接下來終於要開發新的用戶了,這時別忘了要去 Search Console 提交 Sitemap,來加速搜尋引擎處理。像是 Landing Page 類型網站,不會經常更動 Sitemap,首次產生可以藉由一些 Tools 快速的產生出來,例如可以使用 Sitemap-Generator-Cli 來做到。
另外可以透過提交 Google 模擬器,驗證機器人看到的頁面是否跟你看到的一樣,特別要留意 CSR 是否能夠正常的執行 JS Script,有些情況可能會需要加載額外的 Polyfill 才能成功 Render 頁面,甚至更保險應使用前面段落提到的 Pre-render 策略,可以參考下圖為 MCS Lite Landing Page 轉譯狀況:
PART II
檢測指標
Google 有提供兩個常用的檢測工具,一般網頁可以使用 PageSpeed,另外則是利用 Lighthouse 來做 PWA(Progressive Web Apps)的相關項目檢測。雖然 Lighthouse 的分數不見得具有絕對性,因為其會依據網路與瀏覽器環境而異,就算評估出來的分數差,也不見得在使用者電腦就會表現不好,每次跑都會有不同的結果。但是畢竟皆為 Google 所推廣,網站速度有一定程度會影響到 PageRank 的搜尋排行。
更客觀地應使用 WebPagetest 選擇地點實際地進行評估,甚至可以試試看 Pwa-Directory 算是另一個第三方工具,幫你監控三個指標的變換,並幫助開發者對這些指標進行長期的監控,其得出的平均結果即可當做適當的參考。
CRA 在 1.0 版之後,預設加上了 Service Worker 的功能,其主要功能為支援的瀏覽器能做到 Pre-catch 以及離線瀏覽,因此在未進行任何調整前就已經表現得很不錯囉,不過依然有些建議項目可以著手進行,在下面幾個段落分別提及在 MCS Lite Landing Page 採取的優化策略。
Progressive Image Loading
圖檔肯定是網站效能瓶頸之一,基本上設定有損壓縮比率能達到一定效果,或是準備數張不同解析度的圖檔。不過最好的調教方式就是不要用圖檔,以 Data URIs Base64 格式 Inline 在頁面中就不需要額外的下載,例如小張且需要做動畫的圖片就很適合此方法。
在大圖的處理上,可以折中採用漸進式載入的策略,看到這篇文章的你應該已經很習慣了,Medium 的圖片載入方式就是其中一種 Progressive Image Loading 的實作:在大張圖檔載入之前預先放一張小張的圖片,如此就能很快地讓使用者知道這邊有張圖片,雖然一開始不是很清楚,但是很快就會變清楚了。
以左下圖為例,在背景圖片完整的載入之前可以先放一張較小的圖片(右圖標示 1. Inline Placeholder),然後加上一些模糊的效果,接著等到真正圖檔下載完畢(右圖標示 2. Image Download),在漸進式地替換掉:
我在 Reproducing Medium Style Progressive Image Loading for React 曾經介紹過如何使用 React 來簡單地實作,用起來的感覺會是:
<ProgressiveImage
src={largeImage}
placeholder={smallImage} // Base64 inline
/>
或是可以直接參考我發佈的套件來使用:
SVG Component
在頁面中會有動畫設計的部分,這時候是要將 SVG 圖片拆成多個圖層來處理,雖然會發出過多瑣碎圖檔的 HTTP Requests,但是可以透過 Webpack Svg-react-loader 設定,將 SVG 轉成 React Component 來 Render 進行優化,數個小元件即會被塞進一包 JS Bundle 裡。
轉換上,亦可手動將 SVG 原圖做 Svgo 壓縮後複製 SVG Path 到你的 React Component 內,甚至是利用類似 Svg-to-react-cli 工具來轉換,最後記得檢查 Attribute 是否支援以及正確轉換成駝峰式。不過這樣會導致設計師後續更新不易,因此需要做好事前的溝通與確認。如此一來就可以使用 JS 來寫些動畫了,尤其是與 Scroll 互動的部分,例如下圖動畫為當使用者滑進視窗高度的 10% 時執行:
On Demand LazyLoadable
LazyLoadable = Lazy Load + React-Loadable Code Split
由上兩組在線專案對照所示,左側為純 CSR 策略的專案,當 Main JS 下載完後才能顯示頁面資訊;而右側 Pre-render 的目的在於降低 First Meaningful Paint 所需的時間,加速顯示較重要的資訊,並且可透過 React-Waypoint 來達到緩載入 Offscreen Images。當使用者將進入預覽的段落時,才會開始下載所需的片斷,建議可以先看我另一篇文章 Component-based Code Splitting,React 程式碼寫起來像是:
const LoadableComponent = Loadable({
loader: () => import('./Component'),
loading: () => <IconLoading />,
});const LazyLoadableComponent = props =>
<LazyLoad>
<LoadableComponent {...props} />
</LazyLoad>;
昨天剛好看到 React-Loadable-Visibility 用 IntersectionObserver 做類似的實作,之後可能評估看看直接抽換掉或是考慮 Refactor 這一個 Component,有興趣的話可以 Follow 我的 Medium 或是 Repo 喔!
以下圖為例,在 Landing Page 第一個段落中會使用圖表來模擬實際 MCS Console 提供圖表的功能,因此在頁面剛進入時,會先以圖片方式呈現,而後續才開始載入圖表所需的 JS Chunk,接者 Render 出來:
另一個案例在處理 Responsive 的變換,當頁面需要才下載。例如當下為 Mobile Device(在這邊設定 Screen < 768 px)時,才會下載 Mobile 專屬的 Navigation Burger Menu 選單元件。下圖操作為將 Screen Size 從 768 px 切為 767 px,才會發出 HTTP Request 請求下載 Header 所需的 JS Chunk:
React Performance
在 React v15.4 版後的一個功能,開發時在 URL 後加上 ?react_perf
,再打開 Chrome Performance 就會有個小驚喜,可以看到 React 的相關紀錄,很適合拿來追蹤 Lifecycle 的問題。
在 Landing Page 頁面上方與結尾(Section1 及 Section5)有個 Download Button,在頁面載入時會發一個 API Request 到 GitHub 取得最新的 Release Tag,這時候當 React 更新 State 時,將會做下圖的更新程序,左圖為調整前,其中頁面中段的 Section2、Section3、Section4 三個毫無相關的 Components 也被徹底檢查了一番,這時候能在 shouldComponentUpdate 動些手腳來省去這些工,可以改成 React.PureComponent 或是更方便的使用 Recompose 提供的 pure HOC。調整後為右圖,只要三行省去了整整三個樹狀結構的檢查:
- export default Section; // Before
+ export default pure(Section); // After
60fps Event Listener
在一般用 CSS 處理動畫效果比較不會發生,而如果是用 JS 來計算處理,雖然 React 本身就已經很優秀了,仍然很難完全避免。如上圖紀錄所圈選的紅色小三角形即為 Jank 發生處。在事件綁定實作上,EventListener 常常忽略了效能的問題,這邊模擬快速 Scroll 或 Resize 的事件,就會瘋狂的 Trigger Handler,因此造成了畫面來不及在下一次繪製時準備好,導致 Frame Rate 降低。為了解決這個問題,可以考慮加入 Throttle 的機制,甚至是直接使用 Raf (Request Animation Frame) 來優化,例如可以簡單導入 Wu, Ching Ting 的 Raf-Throttle 套件來把 Function 封裝起來,類似這樣:
componentDidMount = () => {
window.addEventListener('scroll', this.onScroll);
this.onScroll();
};componentWillUnmount = () => {
window.removeEventListener('scroll', this.onScroll);
this.onScroll.cancel();
};onScroll = rafThrottle(() => {
this.setState({ isTop: getScrollTop() < this.props.offset });
});
後記
架構選擇常常在效能、維護性、開發速度取捨,我認為更重要的是工程師是否有一致性的開發體驗,產品過程中是不是能夠有些收穫。雖然不是任何頁面都需要使用 PWA 優化,但是提供一個任何平台都能友善獲取資訊的產品,是前端工程師該努力的方向。
下一步,React 本身真的有一點肥了,如果可以應該會考慮使用 Preact,看起來會瘦身不少,但是礙於使用到的套件是否都有支援,就會是另一個問題了。
References
- Twitter Lite and High Performance React Progressive Web Apps at Scale
- Progressive Web Apps with React.js: Part 2 — Page Load Performance
- Connect: behind the front-end experience
*完整 Source Code 放在 MCS-Lite/mcs-lite,如果你喜歡這系列文章,關於 Michael 在 OSS 的專案開發心得,別忘了可以點個 ❤️ 讓我知道喔!