新聞動態消息 (News Feed) 是許多熱門社群平台的核心功能。從 Facebook News Feed、X (Twitter) Timeline 到 Instagram Feed,這些不斷更新的內容流透過持續提供來自朋友、家人與追蹤帳號的新動態,讓使用者持續滾動並維持高黏著度。

本質上,News Feed 就是一個持續更新的內容清單,通常包含狀態更新、照片、影片、連結等。雖然各平台包裝方式不同,但核心概念大同小異。絕大多數 News Feed 系統都具備以下關鍵特性:

  • 可滾動的內容清單。
  • 個別貼文的詳細檢視頁。
  • 無限滾動 (Infinite Scrolling) 功能。
  • 常見的社群互動:按讚 (like)、留言 (comment)、分享 (share)。

雖然骨架類似,但不同的內容型態與面試官要求會導向不同的技術路線。本章將逐步帶你完成一款 News Feed app 的設計。

Figure 1: A typical news feed mobile 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 畫面:顯示完整貼文內容以及按讚、分享選項。

Figure 2: Basic sketch of our news feed mobile app showing the screens and their relationships

有了清楚的問題定義與使用者流程,就可以進入 API 設計。

Step 2:API 設計#

API 設計的目的是在 client 與 backend 間建立清楚的契約,確保候選人與面試官對功能需求的理解一致。本節將討論:

  • 通訊協定 (Communication protocol)。
  • API 端點 (API endpoints)。
  • API 設計的額外考量。

通訊協定#

先盤點 client 與 backend 之間會發生的主要互動:

  1. 動態擷取:開啟 app 時從 backend 抓取最近的貼文。
  2. 貼文建立:將使用者建立的新貼文送到 backend。
  3. 貼文細節:點擊貼文時抓取更多細節。
  4. 使用者互動:按讚或分享時通知 backend。

Figure 3: Client-initiated requests to the backend in the news feed app

上述情境皆由 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:

JSONXMLProtocol 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 物件。

KotlinSwift
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 Created

Client 以 POST 送出 NewPostRequest,server 回 201 但 body 空白。

KotlinSwift
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 不同
  • 關鍵資料 (postIdcreated_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
KotlinSwift
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 中,常見同一內容使用兩種模型:

  1. 輕量模型 (PostPreview):列表夠用,使用縮圖或低解析度圖以節省頻寬。
  2. 完整模型 (PostDetail):使用者點擊才載入,含全部內容與高解析度圖。

此兩層策略能平衡回應性與使用者體驗,節省網路、CPU 與記憶體資源,對低階裝置尤為關鍵。

使用者與貼文的互動#

Authentication: Bearer <token>

POST /v1/posts/{postId}/interactions
    Body: PostInteractionRequest
    Response: 201 Created
KotlinSwift
data class PostInteractionRequest
requestId: Long
type: String
struct PostInteractionRequest
requestId: Int64
type: String

type 可為列舉值,如 LIKEDREMOVED_LIKESHAREDrequestId 通常是 client 生成的 UUID,用於 backend 去重,並非長期保存欄位。

POST 請求中不包含 user id,backend 會從 auth token 取得。此設計不只節省頻寬,更能避免 user id 與 auth token 不一致導致的安全問題。

API 設計的額外考量:ID 與時間戳生成#

建立新貼文時不送 postIdcreated_at 是刻意的設計。將這類關鍵資料集中在 backend 生成具有多項優點:

  1. 一致性:backend 可強制統一格式與驗證規則。
  2. 可靠性:client 時鐘可能錯誤或被竄改,造成時間戳不準。
  3. 唯一性:即使高交易量仍能保證 ID 不衝突。
  4. 資料庫整合:可善用資料庫 auto-increment 與 CURRENT_TIMESTAMP 等機制。

許多公司為此打造專門的 ID 生成系統,如 X (Twitter) 的 Snowflake ID,可在多 server 情境下產生具時間順序且不衝突的分散式 ID。

Step 3:高階 Client 架構#

在定義好協定、endpoint 與資料模型後,接著設計 mobile 系統的高階架構。本節涵蓋:

  1. 高階 mobile 架構設計。
  2. 外部 server 端元件。
  3. UI 層與資料層的核心元件。
  4. 資料跨層流動。

下圖展示高階架構,注意箭頭代表資料流向而非元件相依。

Figure 4: News feed high-level mobile architecture

外部 server 端元件#

Backend#

系統主要外部元件是 Backend:以 HTTPS 與 JSON 為基礎的 RESTful 服務。它肩負兩個主要職責:

  1. 提供 client 所需資料 (貼文、使用者資訊)。
  2. 永久儲存使用者的貼文與互動。

多數業務邏輯 (動態生成、貼文建立、互動處理) 皆在 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 concernssingle responsibility 等原則,並以 dependency injection 維持元件間鬆耦合、提升可測試性。

架構決策

  • 採用 UI + Data 兩層式 (layered) 架構
  • UDFReactive 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),負責:

  1. 管理 UI 狀態 (要顯示什麼)。
  2. 處理畫面邏輯 (處理使用者互動、資料轉換、協調資料層)。

透過 Navigation 元件串聯畫面並傳遞必要參數,如開啟 Post Detail 時傳入 postId

使用情境:News Feed#

News Feed State Holder 訂閱 Posts Repository,取得貼文資料,並透過可觀察的 NewsFeedUiState 流向 UI 暴露。UI state 除了貼文資料,也可能包含錯誤訊息、載入狀態等。

KotlinSwift
data class NewsFeedUiState
feed: List<PostPreviewUiModel>
isLoading: Boolean
errorMessage: String?
struct NewsFeedUiState
feed: [PostPreviewUiModel]
isLoading: Bool
errorMessage: String?

NewsFeedUiState 使用的是 PostPreviewUiModel 而非 PostPreview,這是刻意區隔 domain/data modelUI model。UI 往往只需部分資料,甚至需要預先處理過的格式,保持分離讓 UI 精簡、減少記憶體耗用,也允許畫面呈現與後端儲存獨立演進。愈靠近 UI 的模型,愈該反映畫面的需求

當使用者點擊貼文,News Feed Screen 通知 Navigation 元件切畫面並帶上 postId (Figure 5)。這是事件式 navigation library 的常見做法。

Figure 5: Opening a post detail data flow

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)。

Figure 6: User login data flow

進入下一節前,先回頭檢查功能與非功能性需求是否都被覆蓋。現在補洞比深入技術細節後才發現遺漏容易得多。

Step 4:設計深入探討#

高階骨架已就緒,接下來要針對關鍵細節深入討論。面試深入的方向通常取決於面試官的關注點與系統特性。本章聚焦以下幾個主題:

  • 本地儲存與離線模式支援。
  • Optimistic writes 與離線互動。
  • 顯示 rich post content。
  • 排查動態滾動卡頓 (janky scrolling)。

本地儲存與離線模式支援#

離線仍可使用是 News Feed app 的核心需求之一,需要穩健的本地儲存策略在效能、資源與體驗間取得平衡。

選擇合適的儲存方案#

本地儲存的主要用途:

  • 離線瀏覽與網路不穩的韌性:無連線時仍可捲動已快取貼文。
  • 效能提升:開啟已快取貼文即時顯示,不需等待網路。

對 News Feed 的資料結構與存取模式來說,比較如下:

Relational DatabaseNon-relational DatabaseKey-value store
貼文是結構良好的資料,需複雜關聯與進階查詢 (過濾、排序、搜尋)。可提供資料完整性與一致性,具備良好擴展性。貼文資料結構良好,不需 NoSQL 的彈性;需複雜查詢,不能容忍本地儲存不一致。不擅長大量資料或複雜結構;缺乏關聯、進階查詢與交易操作,易造成資料冗餘與效能退化。

Table 3:本地儲存貼文資料的取捨

決策:採用 relational database 作為 News Feed 的本地儲存。

絕大多數 mobile app 的預設首選就是 relational database:可擴展、原生支援佳、工具成熟。除非有特殊需求,否則將它當作 default 起點就對了。

Database Eviction Policy#

不可能把所有貼文永久存在 client 上,需要聰明的淘汰策略。採用三種政策的混合式方案

  1. LRU (Least Recently Used):優先保留近期檢視過的貼文。
  2. TTL (Time-to-Live):設定過期時間避免內容陳舊。
  3. 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 模式

本架構離線模式的關鍵原則:

  1. 所有 UI 元件只從本地資料庫讀取,絕不直接從網路取得。
  2. 網路回應只更新本地資料庫,由它向上傳遞至 UI。
  3. 使用者互動先寫入本地資料庫,再同步到 server。

資料流無論網路狀態皆為:Backend → Local Storage → UI

若使用者在離線狀態嘗試刷新動態:

  1. Posts Repository 嘗試從 remote data source 擷取資料。
  2. 因連線失敗而 fail。
  3. 失敗向上傳遞至 state holder,更新 UI state。
  4. UI 顯示網路連線相關提示。
  5. 既有快取內容繼續顯示。

業界案例

  • 早期 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 事件 (如按讚),會並行執行:

  1. 立即更新 UI state 呈現該動作。
  2. 通知 Posts Repository 更新應用資料並對 backend 發送請求。

日後 backend 同步完成後,UI 維持不變,因為初始 optimistic write 時已經更新過了。

本節接下來從以下四個面向展開:

  • 資料層更新。
  • 錯誤處理策略。
  • 常見挑戰與因應。
  • Optimistic write 資料流示例。

資料層更新#

Optimistic write 對某些事件有效,但還需再進一步:所有使用者互動應先儲存在本地,直到 backend 確認。這除了支援離線操作,也避免在網路不良時遺失資料。

作法是把本地資料庫當作 queue,依發生順序記錄使用者互動。離線期間累積的互動,裝置恢復連線後由 repository 背景處理。

為維持順序,新增 UserInteraction 資料模型,使用 AUTO INCREMENT 欄位作 id。此模型還包含:

  • postIduserId:識別相關貼文與使用者。
  • action:互動類型 (如 LIKED、UNLIKED、SHARED、BOOKMARKED)。
  • status:狀態 (PENDING、FAILED、CANCELED)。
  • updatedAt:最後一次同步嘗試時間。
  • failureCount:失敗次數。
KotlinSwift
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) 元件呈現。

Figure 7: Data layer design update

錯誤處理策略#

互動同步失敗時,採用 exponential backoff 重試機制:

  1. 網路請求失敗時將互動標記為 FAILED
  2. 系統以漸進延遲重試。
  3. 持續直到達到最大重試次數。

達到上限時的處理取決於互動的重要性:

  • 不重要的動作:更新為 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 樂觀更新。

Figure 8: Optimistic write data flow

業界案例: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 的複雜度:

  1. 制定 best practice 確保同一功能以一致方式實作。
  2. 後端送出的是 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 轉成字串。

Figure 9: UI layer design update

業界案例

  • 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)。
  • 貼文編輯:處理已同步貼文的更新策略。
  • 排程發文:允許使用者排定未來發布貼文。
  • 智慧預抓:裝置狀態理想時背景預抓貼文。
  • 多語系支援:讓系統能處理各種語言的貼文。