こんにちは、フロントエンドを中心に開発しています、原 (@herablog)です。
昨年10月にアメブロ2016 ~ React/ReduxでつくるIsomorphic web app ~という記事で、アメブロのJavaベースアプリから、Node.js・Reactベースアプリへのリニューアルについてお伝えしました。今回は、より進化した2017年版のWebアプリケーション開発に向けて、その後おこなわれた改善についてお伝えします。
https化
2016年4月に、ameblo.jpのhttps化をおこないました。セキュリティ観点としては当然のこと、SEO効果やブラウザの新しい機能の利用など、https化はWebアプリケーションのクオリティアップには必須といってよいでしょう。
まず、サブドメイン化されたサブシステムのhttps化をおこない、その後アメブロ本体のドメインをhttps化しました。https対応ページの動作確認には専用サーバーを作成し、膨大な数のエラーを収集するためにCSPレポートを利用して自動的に対応すべきリソースを洗い出しました。まだ対応しきれていないページも引き続き作業中です。
また、この機会にスキーマレスの方針廃止やリファラーポリシーの更新もおこないました。アメブロ2017 – 大規模サービスhttps化 ~ All Greenを目指して ~に詳細が記述されていますのでぜひご覧ください。
バックエンドキャッシュの改善
2016年9月のリリース時には、サーバーサイドでレンダリングされたHTMLのキャッシュを入れることでバックエンドのレスポンスタイムの向上をはかりました。その後、HTMLキャッシュだけでなくAPIにも適切にキャッシュを入れることで、レスポンスタイムが向上しました。New Relicで対応前後を比較してみると平均レスポンス時間が31.5msから15.7msになりました。CAS (Check And Set)も組み込まれているので、現在の構成ではキャッシュの更新もうまくいっています。キャッシュの設定についてはReact/Redux/Node.jsのSSR/SPAを速くする6つのチューニングポイントをご覧ください。
New Relicによるキャッシュ改善前後のレスポンスタイム。同一スループット60分間の平均値で比較しています。キャッシュ対応後は、Web external (API)リクエストの割合が減り、きちんとキャッシュが利用された結果、全体のレスポンスタイムが向上しているのがわかります。(キャッシュ対応前にゆらぎがないのは古いデータのため間引かれて?いる)
Node.jsのアップグレード
アメブロのNode.jsアプリケーションはDockerを利用しているので、Node.jsのアップグレードも比較的簡単です。影響範囲をコンテナ内に絞れるため、もし問題があった場合でも前のDocker Imageに切り戻せば以前のNode.jsのバージョンでアプリケーションを動作させることができます。アメブロでは、Node.js v8のリリース後にDockerfileの変更、依存モジュールの更新・微調整、動作確認というフローですぐにプロダクション運用できるようになりました。さらに、ネイティブasync/awaitによるパフォーマンス改善には期待を寄せていますが、NewRelicのアップデートを待っているところです。
Alpine LinuxによるDocker Imageサイズの縮小
Node.jsアプリケーションをDocker化することで、ビルドやデプロイを簡略化することはできましたが、Docker Imageのサイズが大きくなり、ビルド・デプロイ時間がある程度かかるようになっていました。それでは、日々の機能開発やリリースに時間がかかり快適ではありませんでした。そこで、Alpine Linuxのイメージを採用し、必要最低限に絞ることでイメージサイズが約10分の1サイズになり、デプロイ時間の短縮につながりました。
Lighthouseやjsx-a11yを使ったアクセシビリティ改善
現時点ではHTML属性で対応できるところが中心ですが、Lighthouseやeslint-plugin-jsx-a11yを使ってアクセシビリティ改善をはかっています。大きなものでは、user-scalable=no
の設定を無効にしました。iOS10ではデフォルトで無効にされているのに加え、ブログは読み物という特性上よりよいと判断したためです。その他にもaria-hidden
やalt
の抜け漏れを徐々に対応しています。今後はreact-a11yの利用も検討しています。また、より広義のアクセシビリティについては、FRESH!のようにガイドラインとして作成、公開も進めていく予定です。
Lazy LoadコンポーネントのIntersection Observer化
現在のアメブロのページ構成は、いち早く記事を表示し、記事下の関連モジュール群はスクロールに応じたLazy Loadで表示しています。Lazy Loadする起点の判定は従来通りスクロールイベントを使った判定でおこなっていましたが、表示パフォーマンス向上のため、Intersection Observerを利用した判定をするように変更しました。(まだ、Lazy Loadコンポーネントとは別の部分でスクロール判定している部分がありますが)
コード分割
昨年9月のリニューアル時には、JavaScriptのビルド結果をひとつのファイルにまとめていました。当時は記事に関連するページだけが対象でしたが、ページ数が増えてくると、必要のない記述が含まれ無駄にファイルの容量が大きくなってきてしまいました。ファイル容量が大きいと、ダウンロードに時間がかかるのみならずJavaScriptの評価にも時間がかかり、それらが終了するまでWebアプリケーションが適切に使えないという影響を及ぼします。また、JavaScriptはシングルスレッドであるため、細かいタスクに分けて処理させたほうが効率がよくもあります。
今後も対応ページやモジュール群が増えてくることが予想されています。その時にバンドルされたJavaScriptがボトルネックにならないように、Webpackを使って共通部分であるmain.jsとそれ以外を分割するようにしました。現時点ではページ内の主要パーツごとに分割し、スクロールアクションによって各スクリプトを読み込み、実行するようにしています。結果的に、main.jsの評価時間は2.88sから223msになりました。ただし、トータルで見るとまだまだ最適化しきれていない部分もあるのでチューニングしていくとともに、まだ導入していないHTTP/2とも相性が良さそうですので、このあたりも検証していきたいところです。
コード分割前後のmain.jsの評価時間の比較です。同一URLをChrome 59 (Incognito mode), Regular 3G, 5x slowdown CPUで計測し、DevToolsからデータを取得しました。
依存モジュールの更新・変更
Node.js、Reactを中心としたエコシステムでは、多くのモジュールを組み合わせてひとつのアプリケーションを作っていくこととなります。比較的変化のはやいエコシステムであるため、改善がはやく、最新機能もいち早く使える一方、しばらく更新作業を放って置くと、気づいたら時代遅れや利用不可になっている可能性があります。これらは素早い機能開発の妨げになるため、使っているものは早めにアップグレード、使わないものは削減といったように最適な状態に保っておくことが大切です。アメブロでは週一でoutdatedな依存モジュールを更新したり、定期的に依存モジュールの変更や最適化をおこなっています。
依存モジュール更新・変更の例
- momentjs → data-fns lodashのようなモジュールパターン、わかりやすいAPI、immutableなどのために移行しました。
- React v15.6 React.PropTypesはreact-codemodを使って一気に置換しました。React v16を楽しみにしています。
- Webpack 3 移行済み、パフォーマンスがあがりました。
- react-router 4 将来的に以降予定ですが、難航中、様子見です?
- Phantomjs → Chrome headles 対応中です。
ページ速度評価指標のアップデート
アメブロでは長らくページロードのタイミングを指標として計測してきました。そのタイミングは、単純にサーバーサイドで生成されたHTMLを描画する際にはある程度有効ですが、IsomorphicなアプリケーションではJavaScriptの実行状態に依存する部分が大きいため、実際のユーザー体験をモニタリングできません。そこで、ページ速度評価指標のアップデートを進めています。
Speed Indexと直帰率、回遊性の相関
まず最初に、実端末でのSpeed Indexの値を集計しはじめました。アメブロはブログという特性から多くの外部リンクによって一時的に訪問されることが多く、最初の1ページ (特にAbove the fold)がいかに早く表示されるかが重要です。Speed Indexは、それをモニタリングする値として適切です。また、実端末で取得しているため、より実際のユーザー体験に近い形で多くのデータを収集できるようになりました。Speed Indexの算出にはRUM-SpeedIndexを利用し、Google Analyticsに送信して分析しています。Speed Indexと直帰率、回遊性の相関を出してみたところ、当たり前ですがやはり早く表示されるほど、直帰率、回遊性ともによい結果となっています。より詳細の分析結果については別の記事で取り上げられる予定ですので、お楽しみにしていてください。
実際のユーザー体験を理解するための、新たなMetrics
Leveraging the Performance Metrics that Most Affect User Experienceという記事で紹介されているように、現在のWebアプリケーションで実際のユーザー体験を理解するためには、ページロードではなく、いくつかの表示状態や操作フィードバックに応じた新たな指標を理解する必要があります。
それぞれの指標に対するスクリーンショット。https://developers.google.com/web/updates/2017/06/user-centric-performance-metrics から引用しました。
First Paint (FP)とFirst Contentful Paint (FCP)は、何かしらの表示がされ始めたタイミングで、真っ白なスクリーンから変化することで、メインコンテンツの表示が準備されていることが利用者に伝わります。First Meaningful Paint (FMP)はメインコンテンツが表示されたタイミングで、ここから利用者は目的となるコンテンツを閲覧できるようになります。Time to Interactive (TTI)は各リソースが読み込まれWebアプリケーションとして正常に操作できるようになったタイミングを表します。
これらの指標は、ブラウザ、WebPagetest、Lighthouseなどでの実装が進んでおり、Webアプリケーションのパフォーマンスを理解する指標として利用できるようになってきています。
SSRとSPA
アメブロのWebアプリケーションでは、初期表示の速度を上げるためのSSRとそれ以降のUXを高めるためのSPAを併用していますが、上述の新たな指標に当てはめて検証してみるとどうでしょうか。そこで、5つのパターンのページを使って、各指標を計測してみました。
同一URLをSSR (Landing), SSR (Repeat), AMP (Landing), AMP (Repeat), App Shellの5つのパターンで表示し、Chrome 59 (Incognito mode), Regular 3G, 5x slowdown CPUで計測しました。Landingとは初回表示つまりキャッシュがない状態を表します。App ShellはService WorkerによるプレキャッシュありのSPA初回表示パターンです。
検証した結果、SSRではFirst Meaningful Paintまでは約1秒と、AMPページと同じくらい早く表示されていました。ただし、Time to Interractiveまでの時間が大きくなっていることがわかります。それは、バンドルされたJavaScriptのサイズが大きくダウンロード・評価に時間がかかっているからでした。
対してApp Shellでは、First Meaningful Paintは約2秒とけして早くないものの、Time to InterractiveがSSRに比べかなり早いことがわかります。
現時点でのアメブロではSSRでFirst Meaningful Paintを活かした構成がマッチしていると思います。しかし、バックエンドへの負荷軽減やユーザー体験向上などApp Shellモデルには可能性があり、もしかしたら、App Shellの方が良いかもしれません。このあたりは実際にテストしつつ選択していきたいと思っています。
Progressive Web App (一部ブログでテスト中)
フロントエンドのシステムリニューアル、https化を無事果たしたことで、Progressive Web App対応をする環境が整いました。現時点では、Service Workerによるプレキャッシュ・ランタイムキャッシュ、一部ブログでのAdd to Home Screen、App Shellモデル、オフライン対応をテスト運用中です。実運用例やチューニング実例については改めてお伝えできればと考えています。
Accelerated Mobile Pages (AMP)のチューニング
アメブロのAMPは2016年2月に公開され、現在はいくつかのテスト・チューニングを経た後、ほとんどすべてのページに適用されています。2017年6月時点では数千万記事がキャッシュされ、世界の中でも最もAMP対応ページを公開しているサービスのひとつと言えそうです。AMPの公開を進めていくうえではメディア指標・広告効果・表示速度の3つの指標を評価しながら進めていきました。
AMP公開によるメディア指標 (PV)の変化
アメブロはドキュメントコンテンツであるため、より多くの人に多くの記事を読んでもらうことが大切です。通常の検索結果に加え、ニュース枠にも表示されるためページビュー数は増えることはあっても減ることはないという想定でした。検索結果からの流入はアルゴリズムの変更やニュースなどに大きな影響を受けるため正確には掴みにくいですが、ニュース枠に出やすい特定のブログには効果があり、全体としては大きな変化は見られないという結果でした。
これはある特定のブログのページビュー数です。ニュース枠に出やすいため、AMP公開後に増加しているのが分かります。
なお、AMPアイコンが付くことによる検索流入の増加はあるのか気になるところですが、現時点では正確にはわかっていません?
AMP公開による広告指標 (CPM)の変化
広告指標に関しても、少なくともオリジナルサイトと同等の広告効果 (CPM)を目指しました。一時期、オリジナルサイトよりも20%ほど低い時期もありましたが、チューニングをおこない現時点では特定のカテゴリではオリジナルサイトよりも約10%向上、全体としてはほぼ同等というところまで改善しました。
チューニングの際には、広告の位置変更・サイズ変更、スティッキー広告amp-ad-sticky
の追加をしました。スティッキー広告の導入はユーザー体験上躊躇していましたが、AMPではスクロールを始めるまで表示されない・誤タップしないようフレームに囲われているなど配慮があったため導入に至りました。
アメブロにおける検索体験ベストプラクティス
アメブロでは、「表示速度はユーザー体験における最重要項目のひとつ」と考えているため、表示速度という観点で言えば、検索結果からの表示にはAMP表示をするのが最速です。また、閲覧者がそのブログに興味を持ち、1ページ以上ブログを読みたいと思った場合にはオリジナルページに遷移し、ブログのフル機能を体験してもらうのがよいと考えています。アメブロのオリジナルページはSPAで構成されており、ページ間の遷移はAMPよりも高速です。
もちろん全てのサービスにとってのベストプラクティスとはいえないと思いますが、アメブロはドキュメントベースのサービスであり相性が良かったと言えます。メディア指標・広告指標に大きな向上は今のところありませんでしたが、表示速度はあがっているのでヨシと考えています。
AMPを実装・運用してみて
AMPを実装してみてわかったことは、HTMLとCSSで宣言的に記述できるので見通しがよく、何かを追加・削除する際にはどこを変更したら良いのか簡単に見分けることができることです。近年のWebアプリケーション作成にはJavaScriptが必須になってきていますが、AMPの仕様であれば多くのメンバーが制作にあたれるでしょう。
また、問題の切り分けがしやすいということもあげられます。先日、広告のviewログが異常値であるという問題がありましたが、AMPの仕様から広告側のスクリプトに問題があることがすぐにわかりました。通常サイトであればオリジナルサイトと広告のスクリプトどちらに問題があるのかまず調査する必要があったでしょう。
ただし、ハードなチューニングが必要な場合にはやはりオリジナルサイトが必要です。AMP自身も「AMPは1番手でなく、遅い良くない多くのサイトを2番手の位置まで押し上げるのが目的」と述べています。
おわりに
アメブロではIsomorphicなWebアプリケーションモデルを取り入れ、サーバーサイド・クライアントサイド両方のパフォーマンス向上をしてきました。現時点ではSSRでの初期表示のはやさが魅力的な一方、Service WorkerやES Modules、Web components、HTTP/2などクライアントサイドアプリケーションに適した機能が強化され、サーバー・クライアントの役割を決めるにあたり、過渡期といえそうです。
今後は、HTMLレスポンスのさらなる静的化、HTMLキャッシュのCDN化によるレスポンスタイムの安定性向上や耐障害性の強化をはかるとともに、SPA単体としても成り立つようにチューニング、アプリライクなUIを取り入れ、ユーザー体験を向上させていく予定です。より品質を向上させるため、様々なチャレンジをしていきます。