サービスの成長に伴ってスモールスタートでリプレイスがスタート

清水恭子氏:「DMMブックス(※発表当時はDMM電子書籍)をリプレースしようとした時に痛感したこと」というタイトルでLT(ライトニングトーク)をします。今、合同会社DMM.comの電子書籍事業部でエンジニアをしています。清水と申します。今日はよろしくお願いします。

今日の話は、私が今所属しているチームで開発している「DMMブックス(DMM電子書籍)」というサービスに関する話です。名前から連想できるとおり、書籍を買ったり読めたりできるWebサービスで、20年以上続いている歴史の長い大規模サービスです。

今回、これらを新しい環境へリプレイスしたいとなったときに一番大変だったこと、痛感したことをお話したいなと思います。

そもそもなぜリプレイスというワードがチーム内で出てきたかというと、もともとDMMブックス(DMM電子書籍)はフロントエンドもバックエンドもPHPで実装されたモノリスなアプリケーションでした。

サービス成長に伴い、モノリスコードによる開発的な課題が増えて、きちんとフロントエンド、バックエンドが分離された新しいアーキテクチャにリプレイスしたいという欲が高まりました。

ただ、先ほども言ったとおり、ユーザー数も売り上げも一定数ある大規模サービスなので、一気にフルリプレイスしようとすると相当なコストがかかります。また、チーム内にもこの規模のリプレイス実績がなかったので、まずはミニマムスタートをしたいとなりました。

なので、まず1stリプレイスのプロジェクトを発足しました。このプロジェクトでは、フロントエンドを既存のアーキテクチャから引き剥がすことに注力して、対象も1画面に絞り、まずリプレイスの実績を作ることにフォーカスしました。今回は、バックエンドのリファクタリングやインフラ構成の見直しなどは考慮していないです。

before/afterはどうなったかというと、一部インタラクティブなUXがJSやJQueryで実装されていたものを、PHPでバックエンドとフロントエンドの一部を担うようにしました。

ここにあるような画面の大枠部分やルーティング周りを引き剥がそうとするとちょっとコストがかかりそうだったので、まずは残して残りの約8割くらいのフロントエンドの実装をReactで開発して、クライアントサイドでレンダリングできるようにしました。

このフロントエンドのアーキテクチャに関しても、今回はSPAやSSRという技術選定はしていないです。ミニマムでスタートしたかったので、一番シンプルなかたちでスタートすることになりました。

特にSEOの観点で、今回対象の画面はCSRでも事足りることが検証からわかったので、本当にシンプルな、PHPからReactで書いたJSを呼び出して、CSRするというかたちで開発を進めていきました。

必要な情報はAPI経由で取得をしています。画面仕様は変えていないです。ただCSRになったので、初期描画のUXは少し変わりました。

4つのフェーズで一番大変だったのは効果計測と改善

このような設計をして、開発して、効果計測をして、本リリースして、という4つの大きなフェーズがプロジェクトの中にあったんですが、今回一番大変だったフェーズは効果計測と改善の部分です。

今回のプロジェクトでは、新しいアーキテクチャが売上とパフォーマンスに悪影響を及ぼさないことがリリースの条件だったので、効果計測を行いました。パフォーマンスに関してはLighthouseやDevToolsのPerformanceツールで、途中からDatadog RUMにもデータを入れてユーザーの実データを見られるようにしました。

売上に関してはA/Bテストを行いました。サーバーサイドで、新しい画面と古い画面のどちらを表示するかを振り分けて、GoogleOptimizeにデータを送って、詳細なデータはGA(Google Analytics)で効果計測を行なっていました。

最初の効果計測のときは悪影響なんてないと思っていた

最初の効果計測のとき、私は、今回画面仕様も変わっていないし、Lighthouseでパフォーマンス測定したら改善傾向にあったから、売上に悪影響なんてないよ、今までよりもむしろいいものができたと思っていました。

ただ現実はそうではなく、なぜかよくわからないですがA/Bテストの結果を見ると売上が悪化していて、最終的にA/Bテストを5回やって効果計測と改善を繰り返しました。これは、半年以上かかる長期戦でした。

データから、リリース前の検証では気づかなかった課題が見えました。課題は大きく2つです。1つは、サービスの利用率が減っていそう。もう1つは特定のSP(スマートフォン)端末での売上が悪化していることです。両方ともけっこう致命的な課題です。

それぞれに対して、どういった改善をしたかについてお話しします。まずサービスの利用率が減っていそうに関しては、画面の離脱率が悪化傾向にあったり、画面下部にある要素のクリックイベントや表示される頻度が減少傾向にあったりすることがわかりました。

さらに詳細なパフォーマンス計測をしてみると、実際になにかしらが画面描画されるタイミング、いわゆるFCP(First Contentful Paint)は改善傾向にあったのですが、ユーザーが本当に見たい要素の描画、主要なコンテンツが描画されるタイミングは悪化傾向にあることがわかりました。

Lighthouseの6になってからは非推奨になったFMPというものを、当初の計測のときには使っていたんですが、このFMPのタイミングが計測ごとで差異があったので、指標的には改善していても、それがUXやパフォーマンスの改善に直結しているかというと、そうではないことがわかりました。なので、改めて初期描画のパフォーマンスとUXの改善を強化することにしました。

初期描写のUXの見直しでCLSの悪化を防ぐ

初期描画のUXの見直しです。beforeにあるように、真っ白い画面が何秒か続いて、そのあとにパッと書籍やコンテンツが描画されていたものが、今回、レンダリング手法が変わったことにより、afterでは真っ白い画面のあとにPHP側に残したコンテンツが描画されて、そのあとにガタッと主要な書籍コンテンツが描画されるという、コンテンツ描画までにすごくカクつきが目立っていました。

指標で言うと、Cumulative Layout Shift(CLS)が悪化していたので、スタイルの見直しをして悪化を防ぐようにしました。

パフォーマンス改善に行ったいくつかの施策

パフォーマンスの改善に関してはいくつかやりました。まずAPIの分割です。もともと必要なコンテンツすべてを返していた大きいAPIが1個あったんですが、ファーストビューで必要な要素をより早く表示できるようにコンテンツを分割して取得できるようにAPIを改修しました。

その際に、先ほどのようなCLS(Cumulative Layout Shift)が悪化しないように検索中状態を適切に表示させることも併せて行いました。

もう1つ行なったのは画像の遅延読み込みです。電子書籍系のサービスにはけっこうありますが、画像やバナーなど1画面に大量の画像が読み込まれていました。初期描画に不必要な読み込みがたくさんあったので、それらを必要最低限に抑えました。

これはJSのIntersection Observer APIを使って、対象要素が可視領域に70パーセント……ここはチューニングできるんですが、70パーセント入ったら、画像の読み込みを発火させるようにしました。これによって、初期描画に行われていた画像の読み込みが約10分の1になりました。

最後に行ったのは、ReactのバンドルJSの読み込みをより速くする改修です。今回API経由でコンテンツを取得するようにしたので、描画中に行われている通信やJSのバンドルサイズが初期描画に大きく影響するようになりました。

まず、CSSやJSの読み込みの見直しと精査を行なって、Reactのバンドルサイズの削減を行いました。

バンドルサイズの削減に関しては、DMMブックス(DMM電子書籍)はPC版とSP版があるんですが、それぞれ画面仕様が大きく異なるので、Reactのコンポーネント自体が分かれています。なのでバンドルファイルを分割して、それぞれ必要最低限のJSのみを読み込むようにしました。

ほかにも、「webpack bundle analyzer」を使ってなにか削減できるものはないか見ていたんですが、1つ大きくあったのが、Material-UIです。

私はこのプロジェクトで初めてReactを触ったんですが、当初興味本位でMaterial-UIで作って入れたコンポーネントがいくつかありました。それらはぜんぜんスクラッチでも書けるレベルだったので、きちんとリプレイスして依存しているパッケージを削減しました。これらが初期描画のUXとパフォーマンス改善に向けてやった取り組みです。

もう1つ、効果計測から発見した課題として、特定のSP(スマートフォン)端末での売上悪化がありました。ここに関しては、怪しい端末で改めて動作確認したところ、大きく2つ課題があったので、これらを改善しました。

1つは画像遅延読み込みです。パフォーマンスで入れた改善による、描画時のUX悪化と、実際にSP(スマートフォン)でスクロールするとたまに画面がカクつくという課題です。

画像読み込みの処理の見直しは、特にネットワーク環境が良くない環境で横や縦にスクロールすると、要素が可視領域に入って初めて読み込みが走るので、インジケーターが大量に描画されます。その結果、UXが悪化している可能性が高いことがわかりました。

要素が入ったときに、画像の読み込みは変わらず行うのですが、さらにその対象要素のN個先にある画像の要素も先に読み込むようにしました。結果的に、遅延読み込みと先読みが混在する仕様にして、もともとやりたかったパフォーマンス改善と、それによって悪化したUXに対しての改修の両方ができるようにしました。

もう1つ、スクロールをすると画面がカクつく問題は、GC (garbage collection)が起きていたことが原因だったので、JS処理の負荷軽減をしました。一番影響がありそうだったのがインジケーターの実装ですね。もともと、けっこう処理負荷が高い実装になっていたので、これはCSSのアニメーションを使ってメインスレッドでの処理軽減をしました。

あとはReact環境のレンダリング処理の最適化をしたりstateの見直しをしたりして、なるべく再レンダリングされないかたちを取りました。

stateの管理は、もともとReduxを使っていたんですが、storeに入れなくてもいい要素がいっぱい入っていて、不要なstate更新というか、storeの更新が多く目立っていたので、管理する必要がないものはきちんと除外して、このタイミングでReactのHooksへ移行しました。

5回目のテストでパフォーマンスとUXどちらも改善

これらの改善を段階的に少しずつ行なって、5回目のテストを行いました。結果ですね。パフォーマンスとUXがともに改善されて、売上にも貢献でき、めでたく本リリースできました。

もともとのモノリス構造と比較したときに、主要なコンテンツが描画されるまでの時間が約3秒改善されたこともわかり、パフォーマンスUXもきちんと良くなって、成果をあげることができました。

徐々に新しい環境に新規の画面や機能が載ってきています。

まとめです。今回このプロジェクトを通じて、私が一番痛感したことは、ユーザーに価値あるものを作るのは本当に難しいということです。特に開発していると、日々作っているものに目が慣れてしまっているので、気づけていないことや見落としていることが多くあると感じました。

なので、自分が作ったものやリリースしたものが本当に有益だったのか、ほかにできることはないのかを実データを通じてきちんと見ることを引き続きやっていきたいなと思いました。

NEXTですね。冒頭で言ったとおり、まずミニマムスタートでリプレイスのファーストステップを踏み出したいと始まったプロジェクトだったので、今度は全体リプレイスに向けてリアーキテクチャ中です。

フロントエンドに限らず、バックエンドエンジニアやインフラエンジニアなどいろいろなメンバーが集まって、DMMブックス(DMM電子書籍)をどういうふうに作っていきたいかを引き続き考えて、またさらにリプレイス開発に向けていきたいなと思っています。

以上です。今日はありがとうございました。