ニュースフィード(フロントエンド)

news_feed.jpeg

要件の明確化

面接のはじめ少なくとも以下の要件を確認してから設計にはいりましょう

コア機能

  • ニュースフィードの閲覧: ユーザーは画像、テキスト、タイムスタンプを含むニュース記事のリストを見ることができます。
  • フィードの更新: サーバーは定期的にニュース記事を更新し、最新の情報をユーザーに提供します。

クライアント

  • モバイル vs ウェブ ?

非機能要件

面接官によってこれは話さなくても良いというケースもあるがどこまで話す必要があるのか擦り合わせると良いです

  • データサイズを考慮
  • 新しい記事の更新の遅延
  • UX中心の指標

フロントエンドなのでUX中心の指標を考慮すべき。いわゆるどの体験を与えたいのかここは面接官と話しながら決めましょう。いくつかweb.devから取ってきてるもの

  • First Contentful Paint(FCP):** ページの読み込みを開始してから、ページのコンテンツのいずれかの部分が画面に表示されるまでの時間を測定します。
  • Largest Contentful Paint(LCP):** ページの読み込みを開始してから、最大のテキスト ブロックまたは画像要素が画面にレンダリングされるまでの時間を測定します。ラボフィールド
  • Interaction to Next Paint(INP): ページで行われたタップ、クリック、キーボード操作のレイテンシを測定し、操作数に基づいて、ページの最も低い(または最も高いものに近い)インタラクション レイテンシを、ページの全体的な応答性を表す単一の代表値として選択します。ラボフィールド
  • 操作可能になるまでの時間(TTI):** ページの読み込みを開始してから視覚的にレンダリングされ、初期スクリプト(存在する場合)が読み込まれてユーザーの入力にすばやく確実に応答できるようになるまでの時間を測定します。ラボ

また計測方法についても話せると良いでしょう

データモデル

データモデルは以下のようなエンティティから構成されます:

  • ニュース記事: 画像URL、テキスト内容、タイムスタンプ、一意なIDを持ちます。

ハイレベル設計

必要なコンポーネント

必要なコンポーネントをリストアップしてそれぞれの役割を説明しましょう。

architecture

  • server: フィード投稿を取得するための HTTP API と、新しいフィード投稿を作成するための HTTP API を提供します。
  • reducer/controller: アプリケーション内のデータフローを制御し、サーバへのネットワークリクエストを行います。(reducerはreduxから来たものですが他のアーキテクチャの似たようなものを使っても構いません)
  • store: アプリケーション全体で必要なデータを保存します。ニュースフィードの場合、store内のデータのほとんどは、フィード UI に必要なサーバ由来のデータとなります。
  • フィード UI: ニュース一覧のリスト

描画(render)のアプローチ

従来のWebアプリケーションでは、コンテンツのレンダリング先(サーバーまたはクライアント)を選択するための複数の選択肢があります。

  • サーバーサイドレンダリング(SSR):最も伝統的な方法で、HTMLをサーバーサイドでレンダリングします。SEOが必要な静的コンテンツや重いユーザーの対話が不要なコンテンツに最適です。ブログ、ドキュメンテーションサイト、Eコマースサイトなど、SSRを使用して構築されたウェブサイトがあります。
  • クライアントサイドレンダリング(CSR):ブラウザ内でレンダリングし、JavaScriptを使用してページに動的にDOM要素を追加します。対話的なコンテンツに最適です。ダッシュボード、チャットアプリなどのアプリケーションはCSRを使用して構築されます。

興味深いことに、ニュースフィードアプリケーションはその中間に位置しており、静的なコンテンツが多い一方で、対話も必要とされます。Twitterなどはハイブリッドアプローチを使用しており、SSRによる高速な初回読み込みを行い、その後、ページにイベントリスナーをアタッチするためにページをハイドレートします。その後のコンテンツ(ユーザーがフィードの最後に達すると追加される投稿など)やページナビゲーションはCSRを使用します。

API設計

  • ニュースフィードを取得: ユーザーのニュースフィードを取得します。ページ番号とサイズのパラメータを受け取り、ページングを行います。
GET /news

{
  result: [
    { "id": 412, "title": "史上最高に旨い...", "content": "...", "image": "https://www.example.com/news-images.jpg", }
  ]
}

また大量のデータを読み込むと遅延が発生するため、ページングが必要です。

オフセットベースのページング

GET /news?page=1&size=10

{
  "pagination": {
    "size": 10,
    "page": 1,
    "total_pages": 20,
  },
  result: [
    { "id": 412, "title": "史上最高に旨い...", "content": "...", "image": "https://www.example.com/news-images.jpg", }
  ]
}

APIに開始位置(page)と取得するレコード数(size)を指定してニュース一覧を取得きているものです

// 一つ目のリクエスト
GET /news?page=1&size=10

{
  "pagination": {
    "size": 10,
    "page": 1,
    "total_pages": 20,
  }
  ...
}

// ユーザーのスクリーンが大きのでニュース記事を多めに表示したいのでsizeをあげる
// 二つ目のリクエスト
GET /news?page=2&size=20

// こうなると 11~20th番目のニュース記事をスキップすることになる
  • クエリパフォーマンスの低下: テーブルが大きくなるにつれてクエリパフォーマンスが低下します。巨大なオフセット (例: OFFSET 1000000) の場合、データベースは引き続き count + offset 行まで読み取る必要がある

カーソルベースのページング

データセット内の特定のレコードを指すカーソルを使用して、後続のレコードを取得します。大規模なデータセットに対してより効率的です

GET /news?page=1&size=10

{
  "pagination": {
    "size": 10,
    "next_cursor": "_0xd9a2h"
  },
  result: [
    { "id": 412, "title": "史上最高に旨い...", "content": "...", "image": "https://www.example.com/news-images.jpg", }
  ]
}

ページング方式の比較:

  1. オフセットベースのページング

メリット:

  • ユーザーは特定のページにジャンプできる
  • 総ページ数を簡単に確認できる
  • バックエンドでの実装が簡単(SQL クエリ: SELECT * FROM posts LIMIT 10 OFFSET (${page} - 1) * ${size})

デメリット:

  • 頻繁に更新されるデータの場合、ページ結果が不正確になる可能性がある
  • ページサイズの変更が難しい(オフセットがページサイズと要求ページの積に依存するため)
  1. カーソルベースのページング

メリット:

  • 大規模データセットで高速
  • リアルタイムデータに最適
  • 不正確なページング問題を回避できる

デメリット:

  • 特定のページに直接ジャンプできない(前のページをすべて見る必要がある)
  • オフセットベースに比べて実装がデータベースに依存し、やや複雑

メリット、デメリットを挙げた後にじゃあどれにするか意思決定をするのです。面接官をコミュニュケーションを取ってこのサービスに取って一番良い選択肢をしていくことが大事です。

ニュースフィードに適したページング方式は?

要約すると、オフセットベースのページングとカーソルベースのページングの選択は、データと要件に大きく依存します。オフセットベースは、静的データセットや小規模データセットで、ページへの直接アクセスが重要な場合にシンプルで優れています。カーソルベースは、データシーケンスが重要で頻繁に変更される大規模な動的データセットに対して、より効率的で信頼性が高いものです。

以下の条件に当てはまる無限スクロールのニュースフィードでは、カーソルベースのページングは明らかにオフセットベースのページングよりも優れており、ニュースフィードのユースケースに最適です。

  • 新しい投稿が頻繁にフィードの上部に追加される
  • 新しく取得した投稿はフィードの末尾に追加される
  • テーブルサイズがかなり早く大きくなる可能性がある

詳細なコンポーネント、最適化、およびその他の機能

ニュース一覧の最適化

無限スクロール

scroll イベント + `Element.getBoundingClientRect() vs Intersection Observer API

仮想リスト

Virtual List

無限スクロールでは、すべての読み込まれたフィードアイテムは一つのページ上にあります。ユーザーがページを下にスクロールすると、より多くの投稿が DOM に追加され、フィード投稿の構造が複雑であるほど (レンダリングする詳細が多いほど)、DOM サイズは急速に増加しますので巨大なリスト(ニュースフィード)を高速に表示するために仮想リストを使います。

仮想リストは、ビューポート内にある投稿のみをレンダリングする技術です。

https://zenn.dev/so_nishimura/articles/6a934ad066bedf

メリット:

  • パフォーマンスの向上: 仮想リストを使用すると、DOM サイズを小さく保ち、ブラウザの描画と仮想 DOM の調整にかかる時間を短縮できます。
  • メモリ使用量の削減: DOM サイズが小さいため、ブラウザが使用するメモリ量が少なくなります。
  • スムーズなスクロール: ビューポート内の投稿のみがレンダリングされるため、ユーザーがスクロールするとフィードがよりスムーズに移動します。

デメリット:

  • 実装の複雑さ: 仮想リストの実装は、通常のリストの実装よりも複雑です。
  • ブラウザの互換性: 仮想リストはすべてのブラウザでサポートされているわけではないため、すべてのユーザーが同じエクスペリエンスを得られるとは限りません。

仮想リストは、無限スクロールのニュースフィードのパフォーマンスを向上させる効果的な技術です。ただし、実装が複雑であり、ブラウザの互換性に関する課題があることを認識する必要があります。

新着記事

ユーザーがニュースフィードアプリケーションをブラウザタブとして開いたままにしていて、まったく更新しないことは珍しくありません。最後に取得したタイムスタンプが数時間以上前であれば、新しい投稿がある可能性があり、読み込まれたフィードが古いと見なされるため、ユーザーにリフレッシュまたはフィードの再読み込みを促すのは良い考えです。新しいフィードを再読み込みすると、現在のフィードはメモリから完全に削除されてメモリスペースを解放することができます。

New Article

もう 1 つの方法は、新しいフィード投稿を自動的にフィードの上部に追加することですが、これは望ましくない場合もあり、スクロール位置に影響を与えないように細心の注意を払う必要があります。

ニュース一覧記事

テキストのrendering

テキストの描画形式の比較:

  1. HTML

例:

<a href="...">
  <h1>史上最高に旨い...</h1>
</a>

メリット:

  • 最もシンプルでフロントエンドも加工せずそのまま表示できる

デメリット:

  • クロスサイトスクリプティング (XSS) の脆弱性が発生する可能性がある
  • 将来、レンダリング前にメンション/ハッシュタグを装飾したり、リンクにクラス名を追加したい場合は、HTML形式ではAPIをWeb以外のクライアント (iOS/Android など) で再利用するのが難しくなります
  1. カスタム構文

例: [link:史上最高に旨い...](https://example.com)

メリット:

  • プラットフォーム非依存で、様々なクライアント(Web、iOS、Android)で再利用しやすい
  • XSS脆弱性のリスクが低い
  • 将来的な拡張性が高い(新しい構文を追加しやすい)

デメリット:

  • パース処理が必要で、実装が複雑になる可能性がある
  • 独自の構文を学習する必要がある
  1. Rich text

例: https://lexical.dev/

メリット:

  • 豊富な書式設定オプションを提供(太字、斜体、リスト、テーブルなど)
  • WYSIWYG(見たままが得られる)エディタと相性が良い
  • 構造化されたデータとして保存可能

デメリット:

  • 実装が複雑になる可能性がある
  • データサイズが大きくなる傾向がある
  • プラットフォーム間での一貫した表示が難しい場合がある

画像の最適化

ニュースフィードの読み込み速度を改善し、ユーザーエクスペリエンスを向上させるために、以下の画像最適化手法が有効です

  • CDN への画像ホスティング: CDN (Content Delivery Network) を使用して画像をホストし、世界中のユーザーに高速に配信します。これにより、サーバーの負荷を軽減し、画像の読み込み時間を短縮できます。
  • WebPを使う:WebP は、優れた可逆圧縮と非可逆圧縮を実現する画像形式
  • 画像の alt 属性の適切な設定:アクセシビリティ向上とSEOに役立ちます
  • デバイスの画面サイズに基づいた画像読み込み:フィードリストのリクエストにブラウザの寸法を含めることで、サーバーはどのサイズの画像を返すかを決定できます。これにより、最適な画像サイズがユーザーに提供され、読み込み速度が向上します
  • srcset:画像処理(リサイズ)機能がある場合は、srcset を使用して、現在のビューポートに最適な画像ファイルをロードします。これにより、不要なデータ転送を減らし、読み込み速度を向上させることができます。
  • ネットワーク速度に応じた画像読み込み:
    • 高速インターネット接続/Wi-Fi: ビューポート外にあるが、もうすぐビューポートに入るオフスクリーン画像をプリフェッチします。これにより、ユーザーが画像に到達する前に読み込みが完了し、シームレスなスクロール体験を実現できます。
    • 低速インターネット接続: 低解像度のプレースホルダー画像をレンダリングし、ユーザーが高解像度画像をロードするには明示的にクリックするように要求します。これにより、データ消費量を削減し、読み込み速度を向上させます。

アクセシビリティ

ニュースフィードのアクセシビリティに関するいくつかの考慮事項を説明します

ニュース一覧

  • フィードの HTML 要素に role="feed" を追加します。

フィード (feed) は動的にスクロール可能な記事 (article) のリスト (list) で、ユーザーがスクロールすると記事がリストのどちらかの端から追加または削除されます。 フィード (feed) により、スクリーンリーダーは閲覧モードの読み取りカーソルを使用して、リッチコンテンツのストリームを読み込みながらスクロールすることを可能にし、ユーザーが読むにつれてコンテンツをさらにロードすることで無限にスクロールし続けることができます。 🔗 MDN

ニュース記事

  • 各フィード投稿の HTML 要素に role="article" を追加します。

記事 (article) ロールは、ページ、文書、またはウェブサイト上で容易に自立することができるページのセクションを示します。 これは、通常、コメント、フォーラム投稿、新聞記事、または 1 ページにまとめられたその他項目などの関連コンテンツの項目に設定します。 🔗 MDN

  • フィード作成者名を含む HTML タグに id 属性があり、その ID を使用して aria-labelledby="<id>" を設定します。

aria-labelledby 属性は、適用される要素のラベル付けを行う要素(複数可)を識別します。 🔗 MDN

  • フィード投稿内のコンテンツは、キーボードでフォーカス可能にする必要があります ( tabindex="0" を追加) 。また、適切な aria-role を設定する必要があります。e

その他

サーバサイドのエラーハンドリング

サーバーサイドがエラーを返す時どのように処理しUIにどういうように表示するのも話しましょう

  • 429 Too many request
  • 500 Internal server error