React Stack 開發體驗與優化策略

Landing Page for MCS Lite

Michael Hsu
17 min readJul 10, 2017

傳統上,Landing Page 用純 HTML、CSS 即可解決。然而,當其它專案都陸續使用 React 完成時,考慮到維護整體專案的一致性,架構簡單的頁面也可以用 React 進行開發,本篇文章即是針對此方向分成兩個部分來討論介紹:

  • PART I. React Stack 開發體驗與推薦工具
  • PART II. 效能評估與優化策略

相關連結: Website | GitHub

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 的最下兩排作為對照組,供大家簡單參考看看。

由上到下前四種 Landing Page 的比較,後二為對照組 (Via WebPagetest 僅供參考)

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 (下左圖示意)等等。例如下右圖為模擬當外部點擊事件發生時,統計當下螢幕的尺寸。

Autotrack with custom Breakpoint dimension

Status Monitor

Postman Daily 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 轉譯狀況:

Google Search Console 模擬頁面 Render 狀況

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),在漸進式地替換掉:

背景圖片漸進呈現 (模擬 Regular 3G 網路)

我在 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% 時執行:

SVG React Component

On Demand LazyLoadable

LazyLoadable = Lazy Load + React-Loadable Code Split

Pre-render & LazyLoadable 前後對照組

由上兩組在線專案對照所示,左側為純 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:

Mobile 元件依需緩載

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
React Performance 檢查工具

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

  1. Twitter Lite and High Performance React Progressive Web Apps at Scale
  2. Progressive Web Apps with React.js: Part 2 — Page Load Performance
  3. Connect: behind the front-end experience

*完整 Source Code 放在 MCS-Lite/mcs-lite,如果你喜歡這系列文章,關於 Michael 在 OSS 的專案開發心得,別忘了可以點個 ❤️ 讓我知道喔!

--

--