新聞動態消息 (News Feed) 是許多熱門社群平台的核心功能。從 Facebook News Feed、X (Twitter) Timeline 到 Instagram Feed,這些不斷更新的內容流透過持續提供來自朋友、家人與追蹤帳號的新動態,讓使用者持續滾動並維持高黏著度。
本質上,News Feed 就是一個持續更新的內容清單,通常包含狀態更新、照片、影片、連結等。雖然各平台包裝方式不同,但核心概念大同小異。絕大多數 News Feed 系統都具備以下關鍵特性:
- 可滾動的內容清單。
- 個別貼文的詳細檢視頁。
- 無限滾動 (Infinite Scrolling) 功能。
- 常見的社群互動:按讚 (like)、留言 (comment)、分享 (share)。
雖然骨架類似,但不同的內容型態與面試官要求會導向不同的技術路線。本章將逐步帶你完成一款 News Feed app 的設計。
Step 1:理解問題並確立設計範圍#
系統設計面試的第一步永遠是收集資訊與釐清需求,確認要支援的功能與整體範圍。以下是候選人與面試官之間典型的對話。
候選人:在深入 mobile app 細節前,我想先了解整個生態系。我們只為行動裝置設計,還是要考量共用同一套後端的網頁或穿戴裝置?
面試官:好問題。今天重點放在 mobile,但要把未來可能出現的其他 client 納入考量。
候選人:我設想要支援:檢視動態、分享與按讚貼文、檢視貼文細節、發布新貼文;動態使用 infinite scrolling,依伺服器回傳順序顯示。這樣對嗎?
面試官:完全正確。
候選人:貼文只支援純文字,還是可以附上圖片、影片?
面試官:讓它更有挑戰性吧。支援文字與媒體附件 (圖片、影片),並支援 rich text editing (標題、粗體、斜體等)。
候選人:那即時功能呢?例如分享數、按讚數的即時更新,或好友發新貼文的推播?
面試官:為了簡化,這次不做即時更新與通知。
候選人:了解。規模大概多大?使用者分布如何?
面試官:目標 5 億 MAU,全球分布。
候選人:這是不小的規模。考量到全球與不穩定網路,是否要支援離線模式檢視已載入內容?要不要加 pre-fetching 加速首次載入?
面試官:離線模式要,pre-fetching 這次先不做。
候選人:我們需要設計身分驗證嗎?還是假設使用者已登入?
面試官:假設已登入。
透過這樣的往返對話,可以逐步勾勒出要設計的系統輪廓。
需求整理#
功能性需求 (Functional Requirements):
- 使用者能瀏覽他人貼文組成的動態。
- 使用者能直接在動態中按讚與分享貼文。
- 使用者能撰寫包含 rich text、圖片、影片的貼文。
- 點擊貼文開啟詳細檢視頁。
- 無限滾動載入更多貼文。
- 離線模式可存取先前已載入的內容。
非功能性需求 (Non-functional Requirements):
- Scalability:支援全球 5 億 MAU,適應不同網路環境並維持穩定表現。
- Performance:滾動流暢、貼文建立高效,對媒體內容處理在各種裝置上都要最佳化。
- Reliability:網路不穩時仍可運作,透過快取提供離線存取。
- Data efficiency:透過智慧快取與選擇性載入降低流量消耗。
- Consistency:離線動作要維持最終一致性 (eventual consistency),恢復連線後可靠同步。
範圍外 (Out of Scope):
- 即時更新與推播通知。
- 開啟 app 前的預先抓取 (pre-fetching)。
- 身分驗證 (假設已登入)。
UI 草圖#
- News/Posts Feed 畫面:app 的核心,顯示無盡的貼文清單。使用者可直接按讚、分享,點擊進入詳細頁;透過 Floating Action Button (FAB) 發布新貼文。
- Create Post 畫面:讓使用者撰寫含 rich text、圖片、影片的新貼文。
- Post Detail 畫面:顯示完整貼文內容以及按讚、分享選項。
有了清楚的問題定義與使用者流程,就可以進入 API 設計。
Step 2:API 設計#
API 設計的目的是在 client 與 backend 間建立清楚的契約,確保候選人與面試官對功能需求的理解一致。本節將討論:
- 通訊協定 (Communication protocol)。
- API 端點 (API endpoints)。
- API 設計的額外考量。
通訊協定#
先盤點 client 與 backend 之間會發生的主要互動:
- 動態擷取:開啟 app 時從 backend 抓取最近的貼文。
- 貼文建立:將使用者建立的新貼文送到 backend。
- 貼文細節:點擊貼文時抓取更多細節。
- 使用者互動:按讚或分享時通知 backend。
上述情境皆由 client 主動發起。考量規模龐大且 client 種類多元,HTTP with REST APIs 是合適的選擇。
| HTTP with REST 優點 | HTTP with REST 缺點 |
|---|---|
| 廣為人知且易於維護;server 為無狀態 (stateless),每個請求自含所需資訊,水平擴展容易;可利用 HTTP caching 降低 server 負載。 | API 版本管理隨演進可能變得複雜;無狀態表示每次請求都要帶上 auth token、session 等資訊,在 mobile 環境會增加額外流量與連線建立延遲;容易發生 over-fetching (回傳過多) 或 under-fetching (需多次請求)。 |
Table 1:HTTP with REST 的優缺點
雖然 GraphQL 可讓 client 明確指定需要的欄位以避免 over-fetching,本章仍選用業界最普遍的 HTTP with REST APIs,如 Meta Graph API、Reddit API、X API 等。
資料編碼方面,可選 JSON、XML 或 Protocol Buffers:
| JSON | XML | Protocol Buffers |
|---|---|---|
| 可讀性高,payload 比 XML 小,幾乎所有語言原生支援,常與 REST API 搭配。 | News feed 不需大量 metadata,JSON 比 XML 更合適。 | 對本 app 來說是可行選項,但設定成本高,本題不以極致效能為目標,因此選 JSON。 |
Table 2:網路資料編碼的權衡
最終採用 JSON:在簡潔、可讀與效率之間取得良好平衡。
決策摘要
- Client 與 Backend 通訊:HTTP with REST APIs。
- 資料格式:JSON。
API 端點#
動態擷取 (Feed retrieval)#
Authentication: Bearer <token>
GET /v1/feed?after=<some-value>&limit=30
Body: empty
Response: 200 OK. Payload of type FeedApiResponse在 endpoint 中加入版本號 (如
/v1/) 可在導入 breaking change 時保留舊版穩定性,便於受控的 API deprecation。更多細節可參考第 10 章 Mobile System Design Building Blocks。
資料模型 (Data models)#
由於是 GET 請求,body 為空;回傳一個 FeedApiResponse 物件。
| Kotlin | Swift |
|---|---|
| data class FeedApiResponse feed: List<PostPreview> paging: PaginationMetadata | struct FeedApiResponse feed: [PostPreview] paging: PaginationMetadata |
| data class PostPreview postId: Long contentSummary: String author: AuthorPreview createdAt: String liked: Boolean likeCount: Int attachmentCount: Int attachmentPreviewImageUrl: String? | struct PostPreview postId: Int64 contentSummary: String author: AuthorPreview createdAt: String liked: Bool likeCount: Int attachmentCount: Int attachmentPreviewImageUrl: String? |
| data class AuthorPreview id: Long name: String profileImageThumbnailUrl: String | struct AuthorPreview id: Int64 name: String profileImageThumbnailUrl: String |
PostPreview 的設計以效率為優先:只帶必要資料就能打造豐富體驗。與其抓所有附件,只回傳數量與單一縮圖 URL,並包含互動資訊如是否已按讚。
平台實作細節
本書的 API response 以 Kotlin、Swift data model 呈現而非 raw JSON。實務上這些 JSON payload 會在 network layer 轉為平台資料結構:
- Android:常用
kotlinx.serialization、Moshi。- iOS:內建的
Codable協定。
分頁 (Pagination)#
Feed endpoint 必須支援分頁以配合無限滾動。第 10 章會討論各類分頁取捨。對頻繁更新的動態而言,offset pagination 在深層滾動會出現查詢效能下降且窗格失準的問題。
Cursor-based pagination 更適合本場景:
- 透過索引欄位處理大資料集,比計算 offset 更高效。
- 以唯一識別碼維持穩定順序,一致性更佳。
- 能與持續變動的內容良好共存。
決策:feed retrieval endpoint 採用 cursor-based pagination。
其他標頭#
出現 Authentication header 表示此 endpoint 需授權。由於統一採 JSON,所有請求預設帶 Accept: application/json。
貼文建立 (Post creation)#
Authentication: Bearer <token>
POST /v1/posts
Body: NewPostRequest
Response: 201 CreatedClient 以 POST 送出 NewPostRequest,server 回 201 但 body 空白。
| Kotlin | Swift |
|---|---|
| data class NewPostRequest requestId: Long content: String attachments: List<NewAttachment> | struct NewPostRequest requestId: Int64 content: String attachments: [NewAttachment] |
| data class NewAttachment type: String contentUrl: String caption: String? | struct NewAttachment type: String contentUrl: String caption: String? |
requestId是 client 產生的去重鍵,與 backend 指派的postId不同。- 關鍵資料 (
postId、created_at) 皆由 backend 產生,以維持 backend 為 source of truth。詳見後文「ID and timestamp generation」。
對附件而言,endpoint 假設媒體已先行上傳,client 只需帶上 contentUrl。這種兩步驟流程 (先上傳附件、再建立貼文) 更利於處理大檔案,細節見 deep dive。
貼文細節 (Post details)#
Authentication: Bearer <token>
GET /v1/posts/{postId}
Body: empty
Response: 200 OK. Payload of type PostDetailApiResponse| Kotlin | Swift |
|---|---|
| data class PostDetailApiResponse post: PostDetail | struct PostDetailApiResponse post: PostDetail |
| data class PostDetail postId: Long content: String author: AuthorPreview createdAt: String liked: Boolean likeCount: Int shareCount: Int attachments: List<Attachment> | struct PostDetail postId: Int64 content: String author: AuthorPreview createdAt: String liked: Bool likeCount: Int shareCount: Int attachments: [Attachment] |
| data class Attachment id: Long type: String contentUrl: String previewImageUrl: String? caption: String? | struct Attachment id: Int64 type: String contentUrl: String previewImageUrl: String? caption: String? |
與 PostPreview 不同,PostDetail 涵蓋完整內容與所有附件。
List-Detail 兩層模型
在具列表-細節結構的 app 中,常見同一內容使用兩種模型:
- 輕量模型 (
PostPreview):列表夠用,使用縮圖或低解析度圖以節省頻寬。- 完整模型 (
PostDetail):使用者點擊才載入,含全部內容與高解析度圖。此兩層策略能平衡回應性與使用者體驗,節省網路、CPU 與記憶體資源,對低階裝置尤為關鍵。
使用者與貼文的互動#
Authentication: Bearer <token>
POST /v1/posts/{postId}/interactions
Body: PostInteractionRequest
Response: 201 Created| Kotlin | Swift |
|---|---|
| data class PostInteractionRequest requestId: Long type: String | struct PostInteractionRequest requestId: Int64 type: String |
type 可為列舉值,如 LIKED、REMOVED_LIKE、SHARED。requestId 通常是 client 生成的 UUID,用於 backend 去重,並非長期保存欄位。
POST 請求中不包含 user id,backend 會從 auth token 取得。此設計不只節省頻寬,更能避免 user id 與 auth token 不一致導致的安全問題。
API 設計的額外考量:ID 與時間戳生成#
建立新貼文時不送 postId 與 created_at 是刻意的設計。將這類關鍵資料集中在 backend 生成具有多項優點:
- 一致性:backend 可強制統一格式與驗證規則。
- 可靠性:client 時鐘可能錯誤或被竄改,造成時間戳不準。
- 唯一性:即使高交易量仍能保證 ID 不衝突。
- 資料庫整合:可善用資料庫 auto-increment 與
CURRENT_TIMESTAMP等機制。
許多公司為此打造專門的 ID 生成系統,如 X (Twitter) 的 Snowflake ID,可在多 server 情境下產生具時間順序且不衝突的分散式 ID。
Step 3:高階 Client 架構#
在定義好協定、endpoint 與資料模型後,接著設計 mobile 系統的高階架構。本節涵蓋:
- 高階 mobile 架構設計。
- 外部 server 端元件。
- UI 層與資料層的核心元件。
- 資料跨層流動。
下圖展示高階架構,注意箭頭代表資料流向而非元件相依。
外部 server 端元件#
Backend#
系統主要外部元件是 Backend:以 HTTPS 與 JSON 為基礎的 RESTful 服務。它肩負兩個主要職責:
- 提供 client 所需資料 (貼文、使用者資訊)。
- 永久儲存使用者的貼文與互動。
多數業務邏輯 (動態生成、貼文建立、互動處理) 皆在 backend。
Content Delivery Network (CDN)#
為了有效分發使用者頭像、媒體附件等靜態內容,加入 CDN:
- 不論使用者位置都能快速穩定載入動態。
- 在流量高峰或重大事件時減輕 server 負載。
- 將內容放在離使用者更近的節點提升整體效能。
以 5 億 MAU 的規模,加入 CDN 是合理選擇。由 backend 負責將內容推送到 CDN 做分發。
Client 架構#
為了打造穩健、高效、可擴展的 app,將架構分為兩個主要層:
- UI Layer:使用者互動的地方,負責顯示貼文、處理互動、準備資料供畫面使用。
- Data Layer:app 的骨幹,處理貼文的 client-side 業務邏輯,管理貼文如何在裝置上建立、修改與擷取。
架構模式的選擇
面試時使用你最熟悉的模式即可——Repository、Clean Architecture、MVVM、MVI、MVP 皆可。重點不是模式名稱,而是你能否清楚解釋資料流與設計取捨。更深入內容可參考第 10 章 Mobile System Design Building Blocks。
為避免錯誤並維持可預測性,導入 Unidirectional Data Flow (UDF) 與 Reactive programming:
- UDF:資料從 data source 經 repository 向上流動到 UI;使用者操作則透過定義明確的管道反向流動。
- Reactive programming:提供處理非同步資料流與變更傳遞的工具。
平台實作細節
- Android:Kotlin Coroutines + Flow。
- iOS:Combine framework。
同時遵守 separation of concerns、single responsibility 等原則,並以 dependency injection 維持元件間鬆耦合、提升可測試性。
架構決策
- 採用 UI + Data 兩層式 (layered) 架構。
- 以 UDF 與 Reactive programming 形塑資料流。
Data Layer#
資料層為每種應用資料設計:
- Repository:該資料型別的主要對外介面,暴露資料並封裝業務邏輯。
- Data Sources:負責具體資料存取,可有零到多個。
資料層是系統設計討論中最該著墨的地方,因為它承載了 client-side 業務邏輯與應用資料,最能展現你對穩健資料管理的理解。
News Feed app 的主要資料是 Posts,對應元件為:
- Posts Repository:中心元件,暴露 feed 與個別貼文、管理核心業務邏輯。
- Posts Remote Data Source:與 backend 通訊收送貼文資料。
- Posts Local Data Source:管理本地儲存、支援離線功能。
並加入 User Repository 處理目前登入者與 auth token 等資訊。
繪製高階架構時不需解釋 UI/Data 層這類基本概念,而是聚焦在如何套用這些概念解決當下問題,並說明你的關鍵決策與取捨。面試官若有興趣會主動追問。
UI Layer#
對應需求階段辨識出的三個畫面:News Feed、Post Detail、Create Post。
Step 3 不必追求完美,只要打好骨架。後續仍有機會精煉設計。務必控管時間,避免陷入細節。
每個畫面對應一個 State Holder (如 Android 的 ViewModel),負責:
- 管理 UI 狀態 (要顯示什麼)。
- 處理畫面邏輯 (處理使用者互動、資料轉換、協調資料層)。
透過 Navigation 元件串聯畫面並傳遞必要參數,如開啟 Post Detail 時傳入 postId。
使用情境:News Feed#
News Feed State Holder 訂閱 Posts Repository,取得貼文資料,並透過可觀察的 NewsFeedUiState 流向 UI 暴露。UI state 除了貼文資料,也可能包含錯誤訊息、載入狀態等。
| Kotlin | Swift |
|---|---|
| data class NewsFeedUiState feed: List<PostPreviewUiModel> isLoading: Boolean errorMessage: String? | struct NewsFeedUiState feed: [PostPreviewUiModel] isLoading: Bool errorMessage: String? |
NewsFeedUiState使用的是PostPreviewUiModel而非PostPreview,這是刻意區隔 domain/data model 與 UI model。UI 往往只需部分資料,甚至需要預先處理過的格式,保持分離讓 UI 精簡、減少記憶體耗用,也允許畫面呈現與後端儲存獨立演進。愈靠近 UI 的模型,愈該反映畫面的需求。
當使用者點擊貼文,News Feed Screen 通知 Navigation 元件切畫面並帶上 postId (Figure 5)。這是事件式 navigation library 的常見做法。
State Holder 也管理按讚、分享等互動:將動作委派給 Posts Repository 處理資料變更;若採 optimistic write,會立刻更新 UI state 讓使用者有即時回饋。
UI 層的共用元件#
加入 Media Loader 元件統一處理貼文媒體附件 (通常以 URL 字串呈現):
- 集中下載與管理靜態影像等媒體 (多由 CDN 提供)。
- 統一快取、載入與錯誤處理。
- 可實作漸進載入與 placeholder 提升體驗。
資料跨層流動#
Figure 6 展示使用者首次登入後的資料流。登入完成後,News Feed State Holder 訂閱 Posts Repository 的資料流;由於本地資料源為空,Repository 透過 remote data source 向 backend 抓取 (step 4);收到回應後寫入 local data source (step 8),觸發 posts data stream 向上傳遞,最終更新畫面 (step 13)。
進入下一節前,先回頭檢查功能與非功能性需求是否都被覆蓋。現在補洞比深入技術細節後才發現遺漏容易得多。
Step 4:設計深入探討#
高階骨架已就緒,接下來要針對關鍵細節深入討論。面試深入的方向通常取決於面試官的關注點與系統特性。本章聚焦以下幾個主題:
- 本地儲存與離線模式支援。
- Optimistic writes 與離線互動。
- 顯示 rich post content。
- 排查動態滾動卡頓 (janky scrolling)。
本地儲存與離線模式支援#
離線仍可使用是 News Feed app 的核心需求之一,需要穩健的本地儲存策略在效能、資源與體驗間取得平衡。
選擇合適的儲存方案#
本地儲存的主要用途:
- 離線瀏覽與網路不穩的韌性:無連線時仍可捲動已快取貼文。
- 效能提升:開啟已快取貼文即時顯示,不需等待網路。
對 News Feed 的資料結構與存取模式來說,比較如下:
| Relational Database | Non-relational Database | Key-value store |
|---|---|---|
| 貼文是結構良好的資料,需複雜關聯與進階查詢 (過濾、排序、搜尋)。可提供資料完整性與一致性,具備良好擴展性。 | 貼文資料結構良好,不需 NoSQL 的彈性;需複雜查詢,不能容忍本地儲存不一致。 | 不擅長大量資料或複雜結構;缺乏關聯、進階查詢與交易操作,易造成資料冗餘與效能退化。 |
Table 3:本地儲存貼文資料的取捨
決策:採用 relational database 作為 News Feed 的本地儲存。
絕大多數 mobile app 的預設首選就是 relational database:可擴展、原生支援佳、工具成熟。除非有特殊需求,否則將它當作 default 起點就對了。
Database Eviction Policy#
不可能把所有貼文永久存在 client 上,需要聰明的淘汰策略。採用三種政策的混合式方案:
- LRU (Least Recently Used):優先保留近期檢視過的貼文。
- TTL (Time-to-Live):設定過期時間避免內容陳舊。
- Custom Minimum Threshold:保證永遠有一定數量貼文可離線檢視。
起始預設值:
- 最大快取大小:100–200MB,可依使用模式調整。
- 預設 TTL:一般貼文 15 天。
- 最低門檻:50 則,或約 2–3 個螢幕份量。
這些參數應可由 backend 遠端設定,以便根據實際分析資料微調。
離線模式使用者體驗#
使用者無網路開啟 app 時,會看到先前快取在資料庫中的貼文。做法上遵循 Single Source of Truth (SSOT) 模式,收到 backend 資料後立即寫入本地資料庫,並於使用者登出時清除以保護資料。
Posts Repository 不論網路狀況皆從 local data source 一致地對外暴露資料流——也就是說,本地資料源成為 client 端貼文資料的唯一真實來源。
記住 SSOT 模式
本架構離線模式的關鍵原則:
- 所有 UI 元件只從本地資料庫讀取,絕不直接從網路取得。
- 網路回應只更新本地資料庫,由它向上傳遞至 UI。
- 使用者互動先寫入本地資料庫,再同步到 server。
資料流無論網路狀態皆為:Backend → Local Storage → UI。
若使用者在離線狀態嘗試刷新動態:
- Posts Repository 嘗試從 remote data source 擷取資料。
- 因連線失敗而 fail。
- 失敗向上傳遞至 state holder,更新 UI state。
- UI 顯示網路連線相關提示。
- 既有快取內容繼續顯示。
業界案例
- 早期 Facebook iOS app 以 Core Data 快取動態,規模擴大後改為輕量、去正規化的物件序列化磁碟快取,效能大幅改善。
- Instagram mobile app 同樣快取動態內容,維護 feed JSON 的 response store 與影像/影片快取,離線時可從磁碟供應內容。
- Reddit 因部分裝置外部儲存不可靠,改用 Android 內部儲存做影片快取。
Optimistic Writes 與離線互動#
Optimistic writes 與離線互動對提升體驗、黏著度至關重要,特別是在低階裝置或網路不佳的環境。
Optimistic write 是一種設計策略:不等 backend 確認就立即更新 UI,讓使用者感覺動作瞬間生效。
當 state holder 收到需要 optimistic write 的 UI 事件 (如按讚),會並行執行:
- 立即更新 UI state 呈現該動作。
- 通知 Posts Repository 更新應用資料並對 backend 發送請求。
日後 backend 同步完成後,UI 維持不變,因為初始 optimistic write 時已經更新過了。
本節接下來從以下四個面向展開:
- 資料層更新。
- 錯誤處理策略。
- 常見挑戰與因應。
- Optimistic write 資料流示例。
資料層更新#
Optimistic write 對某些事件有效,但還需再進一步:所有使用者互動應先儲存在本地,直到 backend 確認。這除了支援離線操作,也避免在網路不良時遺失資料。
作法是把本地資料庫當作 queue,依發生順序記錄使用者互動。離線期間累積的互動,裝置恢復連線後由 repository 背景處理。
為維持順序,新增 UserInteraction 資料模型,使用 AUTO INCREMENT 欄位作 id。此模型還包含:
postId與userId:識別相關貼文與使用者。action:互動類型 (如 LIKED、UNLIKED、SHARED、BOOKMARKED)。status:狀態 (PENDING、FAILED、CANCELED)。updatedAt:最後一次同步嘗試時間。failureCount:失敗次數。
| Kotlin | Swift |
|---|---|
| data class UserInteraction id: Long postId: Long userId: Long action: UserInteractionAction status: UserInteractionStatus createdAt: String updatedAt: String failureCount: Int | struct UserInteraction id: Int64 postId: Int64 userId: Int64 action: UserInteractionAction status: UserInteractionStatus createdAt: String updatedAt: String failureCount: Int |
| enum class UserInteractionAction LIKED, UNLIKED, SHARED, BOOKMARKED | enum UserInteractionAction case liked, unliked case shared, bookmarked |
| enum class UserInteractionStatus PENDING, FAILED, CANCELED | enum UserInteractionStatus case pending, failed, canceled |
即使同一貼文有多次互動,也要全部同步回 server,這些資料對分析有價值。衝突可用 “last write wins” 策略處理。
在本地資料庫新增 UserInteractions 表儲存所有互動。互動成功同步後,Posts Repository 將其從 UserInteractions 表移除,並更新 Posts 表對應條目。
資料庫表在資料層設計 (Figure 7) 中以 Data Access Object (DAO) 元件呈現。
錯誤處理策略#
互動同步失敗時,採用 exponential backoff 重試機制:
- 網路請求失敗時將互動標記為
FAILED。 - 系統以漸進延遲重試。
- 持續直到達到最大重試次數。
達到上限時的處理取決於互動的重要性:
- 不重要的動作:更新為
CANCELED並回滾 UI。 - 重要動作:通知使用者。例如貼文送出失敗可發推播提示手動重試。
衝突解決:若 backend 狀態在 optimistic write 後已變更,選項包括捨棄本地互動、嘗試 merge 變更,或通知使用者讓其決定。最佳策略因情境而異。
常見挑戰#
- Client 與 backend 一致性:把 backend 當作 single source of truth,每次收到資料就同步更新本地 Posts 資料庫。
- 互動可能被亂序套用:記錄建立與修改時間戳,並依序送至 backend 確保順序處理。
- Optimistic 更新被回滾或衝突解決不明確可能造成 UI 困惑:對關鍵場景主動通知並指引下一步。
- 重試與衝突解決對 backend 造成額外壓力:透過 exponential backoff 減緩,必要時批次化互動。
Optimistic write 資料流#
- State holder 收到 UI 事件時並行兩件事:
- 更新 UI state 反映變更 (step 1.1),提供即時回饋。
- 通知 repository (step 1.2),由它寫入本地 (step 2) 並同步 backend (step 3)。
- 同步成功 (step 4) 後,local data source 先移除 user interaction (step 5) 並更新 Post (step 6)。
- 後續本地資料庫更新不會造成 UI state 的新發射 (step 7),因為 UI state 已於 step 1.1 樂觀更新。
業界案例:Instagram 在 mobile app 中開發了 Direct’s Mutation Manager 來處理 optimistic writes 中的網路失敗與資料不一致問題。
顯示 Rich Post Content#
需求要求支援 rich text 貼文。雖然 Post 內容以 String 儲存,但純文字顯示無法滿足期望 (連結、提及、樣式、標題、粗體、斜體等)。
處理 rich content 有兩條路:自訂 markup 語言,或使用現成的 HTML / Markdown。
自訂 markup 成本高且需長期維護。Facebook 的 FBML 最終廢棄,轉用標準 HTML 與 React;X (Twitter) 仍使用自有 text library;Reddit 採用 Markdown 的客製版本。本章不建議自行開發 markup。
| 選項 | 優點 | 缺點 |
|---|---|---|
| HTML | 支援 rich 格式;可擴充自訂元素 (如高亮、mention);跨平台渲染一致。 | 對非技術使用者撰寫較複雜;需 sanitization 防 XSS;HTML 渲染較吃資源,低階裝置的滾動效能可能受影響。 |
| Markdown | 使用者撰寫簡單;可轉成 HTML 保留彈性。 | 格式選項有限,對複雜版面不足;仍有語法學習成本。 |
Table 4:貼文內容編碼取捨
決策:採用 HTML 作為貼文內容編碼格式。
為管理 HTML 的複雜度:
- 制定 best practice 確保同一功能以一致方式實作。
- 後端送出的是 HTML 字串,因此 validation 與 sanitization 必不可少,以防止 XSS 等安全問題。
平台實作細節
- Android:OWASP Java HTML Sanitizer、jsoup。
- iOS:HTMLKit、Purifier。
渲染 HTML 編碼內容#
顯示 HTML 有兩種方式:用 WebView 直接渲染,或解析 HTML 以原生元件顯示。
| 選項 | 優點 | 缺點 |
|---|---|---|
| WebView | 輕鬆處理複雜格式、全 HTML 支援集於一身;跨平台呈現一致 (同瀏覽器引擎)。 | 記憶體用量高,影響動態滾動;互動不夠原生、難以與 feed 動畫整合。 |
| Native UI components | 滾動效能更佳;符合平台設計語言,與手勢整合順暢。 | 需自訂 HTML → native parser;開發與維護成本較高。 |
Table 5:HTML 貼文渲染的取捨
對我們需要的 rich 文字、連結、圖片等內容而言,將 HTML 解析成原生元件能在效能與畫質間取得平衡。
決策
- 以 HTML 編碼貼文內容支援 rich text。
- 以 原生 UI 元件 渲染 HTML 編碼內容。
高階架構圖更新#
為此新增:
- Post Content Renderer:負責以原生元件渲染貼文,由 HTML Parser 輔助。所有畫面皆依賴此元件,提升可讀性、可測試性與共用性。
- Post Editor:供 Create Post 畫面使用,讓使用者輸入 rich 內容,再由 HTML Encoder 轉成字串。
業界案例
- 2012 年 Facebook iOS app 從 HTML5 切換至原生 iOS 以最佳化效能。
- Reddit 採用 Server-Driven UI 並精簡回應後,首頁動態延遲降低約 12%。
動態滾動卡頓 (Janky Scrolling)#
面試官可能拋出假設情境測試你的除錯能力與技術深度,例如:「News feed 為何會卡頓、滾動不順?你會怎麼修?」
渲染效率不佳#
View recycling 效率 是關鍵:目標是重用離開可視區域的 view,而非為每個項目重建。當新 view 進入可視區域時,應從 recycling pool 取得可用的相同識別碼 view,重新套用資料即可。此舉大幅減少建立與 layout 成本,讓主執行緒能專注於互動與動畫。
平台實作細節
- Android:View system 的 RecyclerView,或 Jetpack Compose 的 LazyRow / LazyColumn。
- iOS:UITableView / UICollectionView,或 SwiftUI 的 List。
非同步內容載入:在資料模型中,圖片、附件以 URL 字串存在,呈現時再非同步載入,避免阻塞主執行緒。
其他常見的 UI 渲染效率問題:
- Main thread blocking:避免在主執行緒做重計算、同步網路或 I/O,改放背景執行緒。
- Complex layouts:扁平化 view 階層,避免深層巢狀與重疊。
- Inefficient state management:避免不必要的 re-render,可實作 debounce、caching、memoization。
- Unoptimized images:使用高效能的媒體載入 library,必要時先調整大小。
動態影像與影片品質#
滾動體驗直接受裝置效能影響。針對低階裝置,可依 CPU、記憶體、GPU、網路與電池狀況動態調整影像與影片品質:
- 縮短載入時間、提升回應性。
- 維持滾動時穩定幀率。
- 更有效利用裝置資源。
- 更能適應多樣裝置與網路條件。
品質分級範例:
- 影像:Low (480x320 以下)、Medium (720x480)、High (1080x720 以上)。
- 影片:Low (360p)、Medium (720p)、High (1080p 以上)。
Backend 必須能提供同一媒體的多品質版本:預先產生或依 client 請求即時轉檔。
業界案例
- Facebook 為提升 Android 滾動流暢度開發了開源 UI 渲染框架 Litho,將大量 UI 工作移出主執行緒,News Feed 滾動效能最多提升 20%,部分 list 子視圖甚至到 42%。
- Pinterest 優化 Adaptive Bitrate (ABR) 串流管線,影片啟動延遲降低約 36%。
- Reddit 在使用者快速滑動時主動取消影片預抓,延後非必要工作。
Step 5:Wrap-up#
本章完成了一個完整的 News Feed 系統設計:使用者能建立、瀏覽並與 rich 貼文互動。重點回顧:
- 以 HTTP + REST API 支援 client-initiated 請求,JSON 作為資料編碼。
- 建立遵循 REST 原則的 backend endpoint 支援核心功能。
- 設計支援離線的 client 架構,深入討論了:
- 選擇合適的本地儲存。
- Optimistic writes 與離線互動。
- Rich post content 的渲染。
- 動態滾動卡頓的成因與對策。
若還有時間或想挑戰額外功能,可延伸探討:
- 留言功能:讓使用者對貼文與其他留言回覆。
- 推播與即時更新:即時推送互動通知與計數更新。
- 搜尋能力:線上與離線搜尋,離線可研究 Full-Text Search (FTS)。
- 貼文編輯:處理已同步貼文的更新策略。
- 排程發文:允許使用者排定未來發布貼文。
- 智慧預抓:裝置狀態理想時背景預抓貼文。
- 多語系支援:讓系統能處理各種語言的貼文。