レイアウトシフト問題

井上明紀氏:フロントエンドエンジニアの井上明紀です。本セッションでは「LINE NEWSにおけるレイアウトシフト問題への取り組み」というタイトルでお話いたします。私はLINEの新卒入社3年目で、フロントエンドエンジニアとしてLINE NEWSやLINEスケジュールなどの開発に携わっています。

本セッションでは、LINE NEWSのサービスについて軽く触れた上で、前半ではLINE NEWSが抱えるレイアウトのシフト問題について紹介いたします。後半では、レイアウトシフトの解決策としてSkeletonScreenを導入し、どのような問題に対峙したか。また導入後どのように変わったかについて紹介いたします。

まずはLINE NEWSについて紹介いたします。LINE NEWSは、2021年8月時点での月間アクティブユーザー数は7,700万人、月間PV数は154億と、多くのユーザーに利用していただいているサービスです。代表的な機能にはスライド上で見せている、トークに記事を送るダイジェストやLINE NEWSの入り口となるLINEアプリのニュースタブなどがあります。ニュースタブなどいくつかの機能は、WebViewで実現しています。

WebView、つまりWebフロントエンド技術を用いているLINE NEWSですが、そんな中でも、ページによって技術スタックが異なっていたりします。本セッションでは、ニュースタブについて扱います。ニュースタブは、TypeScriptとReactを用いて開発されて、記事を一覧表示したり天気や株価をサマリ表示したり、最近ではコロナに関する情報のサマリを表示したりと、さまざまな機能があり、取り扱うAPIやReactコンポーネントは非常に多く存在します。

ここからが本題です。みなさんはLINE NEWSのフロントエンド開発においてどのような問題があると思いますか? 私たちが最近抱えた問題が、本セッションのテーマであるレイアウトシフト問題です。ニュースタブは現在も日夜機能追加が活発に行われており、API呼び出しの数が日に日に増大しています。これにより、レイアウトシフトの問題が顕在化していき、問題解決に取り組む必要が出てきました。

レイアウトシフトとは一定時間の間に発生するレイアウトのズレ、ガタつきのことを指します。みなさんも、Webページが一度表示されたあとから広告が挿入されることにより、レイアウトがガタついているのを見たことがあるのではないでしょうか。このようなガタつきが発生することで、誤った操作を誘発してしまう恐れがあります。

レイアウトシフトは、APIのレスポンスやレンダリングにかかる時間がUIコンポーネントごとに異なっているために発生してしまいます。そしてLINE NEWSにおいても、レスポンスやレンダリング速度からレイアウトシフトが発生する要因がありました。

LINE NEWSの特徴的な3つのAPI

それはAPIの特性になります。LINE NEWSには多くのAPIがありますが、その中でも特徴的なのはパーソナライズとA/Bテストと広告の3つです。

ユーザー一人ひとりの興味に沿ったコンテンツやレイアウトを提供する仕組みがパーソナライズです。通常のWebサービスよりも、LINEアプリ内のLINE NEWSでは、よりユーザーの特性からコンテンツやデザインを出し分けることができ、一番力を入れている箇所でもあります。

開発観点だとユーザーごとに異なるレスポンス内容であるため、CDNキャッシュが利用できないという点があります。またAPIにもよりますが、ユーザー一人ひとりに作用するAPIというのは認証や特定のデータベースへのアクセスといった作業が発生するため、通常のAPIよりもレスポンスが遅くなる傾向にあります。

次にA/Bテストについてですが、LINE NEWSでは新規のUIやUXをリリースする際に、A/Bテストをよく実施しています。UIコンポーネントとその組み合わせを複数用意し、どの選択肢がもっとも有効かといった判断をテスト結果の数値を用いて行っています。また新規機能を開発してリリースする際には、問題が起きた時のユーザーへの影響を最小限に抑えるために、段階的に機能を公開していくロールアウトなども実施しています。

最後に、広告の仕組みを紹介します。LINE NEWSは、LINE独自のプラットフォームを通じて広告を配信しています。広告のデータを取得するAPIの裏ではユーザーごとの出し分けや在庫の管理などがリアルタイムで行われているため、通常のAPIよりもレスポンスが遅い原因となっています。

これら3つのAPIは、通常のAPIよりもレスポンスまでに時間がかかったり、レスポンス結果によってレイアウトを変更させたりするものであるため、レイアウトシフトの問題が発生してしまいます。そこで私たちは、このレイアウトシフトの問題解決のために、SkeletonScreenの導入を決めました。

SkeletonScreenは、必要なリソースが読み込まれるまでの間、そのコンテンツの枠組みだけを表現する方法です。一般的なSkeletonScreenではUIコンポーネントの画像やタイトル部分を灰色の要素で埋めておき、あらかじめコンテンツ分の高さを確保しておきます。そして、コンテンツの描画に必要なリソースが揃ったら、SkeletonScreenとコンテンツを切り替えることで、ガタつきのないレンダリングを実現するという方法です。

SkeletonScreenとコンテンツとをガタつきなく切り替えるためには、それらの高さがピッタリと揃っていることが必要です。つまりコンテンツのリソースがまだ揃っていない段階から、レイアウトの情報を知っておく必要があるということです。

独自のSkeletonScreenを実装

さて、先ほど紹介したLINE NEWSと3つのAPIは、レイアウトシフトを引き起こし得るものでしたが、それだけではなく、レスポンス結果によってレイアウトやコンテンツの高さを大きく変えてしまうものでもあります。例えばパーソナライズであれば、APIレスポンスの結果によって表示するコンポーネントの順番が異なります。

A/Bテストであれば、そもそも特定のコンポーネントを描画しないかもしれません。広告の場合では、広告の在庫によって毎回異なったサイズの広告が表示される可能性があります。つまりAPIの結果によって、レイアウトやコンテンツの高さが変化するLINE NEWSでは、あらかじめ高さを確保しておく一般的なSkeletonScreenの導入は不可能でした。

そこで私たちは、独自のSkeletonScreenを実装することにしました。それは画面全体を1つのSkeletonScreenで覆う方法です。こうすることで、各UIコンポーネントがどういうデザインか、それらがどういうレイアウトで配置されるかを考慮する必要がなくなります。

こう聞くと、APIレスポンスによってレイアウトが大きく変わるのであれば、SkeletonScreenと実際のコンテンツとでデザインの乖離が起き、SkeletonScreen解除に違和感が出るのではないかと思う方もいるかもしれません。

私たちも実装前は、そう懸念していました。しかしいくつかの工夫により、その懸念は解消されました。1つは切り替え時のフェードアウトアニメーションを細かく調整したこと。もう1つはSkeletonScreenのデザインをある程度出現頻度の高いレイアウトに寄せたことです。中でも、ユーザーの目線が集中しがちな画面上部のSkeletonScreenを実際のコンテンツに限りなく近づけたことで、SkeletonScreen解除時の違和感はかなり解消されていると思います。

実際にニュースタブで動作しているSkeletonScreenのデモをお見せします。いかがでしょうか? 違和感は感じなかったかと思います。

どのような条件でSkeletonScreenを解除するか

ここまで、私たちのSkeletonScreenのUIにまつわるコンセプトについて紹介してきましたが、もう1つ大切な要素があります。それはどのような条件でSkeletonScreenを解除するかです。先ほど紹介したようにコンテンツのレイアウトや高さはAPIレスポンスに大きく依存するので、まずはAPIを軸に解除条件を考えてみましょう。

例えば、リクエストをしているすべてのAPIが完了することを条件にするのはどうでしょうか。これはあまりよくありません。LINE NEWSでは、ページを開いてからすぐさまコールされるAPIは20以上にも達します。そのため、すべてのAPIを待つとSkeletonScreenの解除が極端に遅くなってしまう可能性が高いです。またAPIの数は、固定ではなく遅延読み込みやサーバーのレスポンス状態によっても変わる可能性があり、どこまでのAPIを待つかは曖昧です。

では、事前に決め打ちした特定のAPIの完了を解除条件とする場合はどうでしょうか。これも難しいです。なぜならパーソナライズの仕組みによって、ユーザーごとに呼び出すAPIが変わるためです。

そもそもの問題として、レイアウトシフトが起きなければSkeletonScreenを解除できるわけですが、レイアウトシフトが起きない最低限の条件はどのようなものでしょうか。

それは画面全体を覆う1つのSkeletonScreenの裏に位置する各コンポーネントの描画が終わり、高さが確定した時となります。言い換えると、画面内、つまりはviewport内に入るコンポーネントの高さが確定した時です。なぜならviewportに入らない部分でレイアウトシフトが起きても、ユーザーは認識できず、問題にはならないためです。

コンテンツがviewportに入るかどうかとは、APIに関係はしているものの、API呼び出しだけで決まるものではありません。API呼び出しが完了しコンテンツが描画され、高さが確定することで初めて、画面内に入っているかどうかを判断できます。つまり私たちが実現するSkeletonScreenでは、API呼び出しだけではなく、描画後の高さも考慮してSkeletonScreenを外すような仕組みが必要となります。

LINE NEWSでSkeletonScreenをどのように実現したか

ここまでは、LINE NEWSが抱える問題としてレイアウトシフトの問題が存在し、解決策としてSkeletonScreenを導入、そして独自のSkeletonScreenが必要であるというお話をしました。ここからは、LINE NEWSでSkeletonScreenをどのように実現したかについて詳しく紹介いたします。SkeletonScreenの実装についてはボリュームが多いので、次のような項目で細かくお話していきます。

SkeletonScreenの導入は、一部の機能として独立して導入できるものではありません。LINE NEWSで利用しているアーキテクチャに沿って、導入する必要があります。ですので、LINE NEWSのアーキテクチャの概要を説明してから、実装の詳細を紹介しようと思います。

次の図は、LINE NEWSのアーキテクチャの一部を表したものになります。図の右側に位置する青い要素は、ニュースタブを構成するReactのコンポーネントと、その関係図になります。ニュースタブの中には、トップ・国内など複数のタブが横並びに配置されていて、それに対応するタブコンポーネントが存在します。

各タブコンポーネントを含むコンポーネントツリーは、Store群からステートを受け取ります。Storeの中身が更新されると、タブコンポーネントも更新され再描画されます。そしてStoreにはActionからAPIで取得したデータなどを格納しています。Storeを更新するためのActionはさまざまな箇所で利用でき、例えばファーストビューに必要なコンテンツのデータはアプリケーションの立ち上げとともに、ActionとAPIを通じてStoreに格納されます。

お気づきの方も多いと思いますが、これはFluxアーキテクチャに則っており、Facebook製のFlux Utilsを利用して実現しています。ここまでが、ニュースタブを構成するアーキテクチャの説明になります。

後半へつづく