一休.com Developers Blog

一休のエンジニア、デザイナー、ディレクターが情報を発信していきます

Chrome Dev Summit 2018に参加しました!

この記事は一休.comアドベントカレンダー2018の1日目です。

こんにちは。レストラン事業本部の西村です。

11月12、13日にサンフランシスコで開催されたChrome Dev Summit 2018に参加しました。

f:id:nishimurae:20181130175843p:plain:w560

今年はChromeが10周年ということで、この10年で変わったこと、これからについての話で始まりました。 2日に渡って行われた22のセッションの中で、特に注目した点について深掘りしていきます。

1日目のセッション

1日目は現在提供している技術について、具体的な事例を交えながら紹介されました。

VisBug

VisBugは、hoveringとKeyboard shortcutsでブラウザ上でサイトの画像を差し替えたり、一部のコンテンツの内容やスタイルを変更できるChrome extensionです。

ブラウザ上でちょっとしたスタイル修正や画像の入れ替えをしてデザイン決め、といったことが簡単にできます!

f:id:nishimurae:20181129160030g:plain:w560

こちらはDay 1 Keynoteの最後に紹介されました。

Performance

例年以上にWebサイトのパフォーマンスに関する話が多く、ケーススタディも豊富でした。 ネイティブアプリがメインだったけれど、Webでも利用できるようにしたらアプリのダウンロード数も上がった!という話がSpotifyの事例で紹介されていました。

f:id:nishimurae:20181129172315p:plain:w500

こちらはGet Down to Business: Why the Web Mattersの中で紹介されています。

改善戦略の立て方

セッションの中で、プロジェクトの進め方に関する重要な教訓がありました。

  • 長期的なビジョンを持つ
  • 短期的な目標を計画する
  • 目標の照準を長期的なビジョンに合わせていく

パフォーマンスを改善してもビジネス的なKPIに直結するわけではなく、すぐに成果が出ないことに悩む時もあると思います。 長期的なビジョンを持ちながら短期的な目標を設定し着実にクリアしていくことを意識した方が達成感も持てると感じました。

Walmartの事例では、長期的な目標としてTime to Interactiveを70%削減、それに対して短期的な目標としてJavaScriptを500KB/CSSを40KB削減と設定しています。

f:id:nishimurae:20181129173452p:plain:w560

こちらはECの事例を用いて、Modern Websites for E-commerce in the Real Worldの中で語られています。

Performance Budgetの設定

Performance Budgetとは、Webサイトのパフォーマンスに影響する要素における数値的な限界値です。 計測ツールを使って様々な視点から分析し、適切なPerformance Budgetを設定することが重要です。

こちらはCarousellが設定しているPerformance Budgetです。

f:id:nishimurae:20181129172900p:plain:w560

Pinterestの事例では、JavaScriptのバンドルサイズ、そしてPinner Wait Time(PWT)という独自の指標を設定していました。 PWTはTime to Interactive + Image Loadingです。サービスにとって重要な画像を意識した指標ですね。

このように、それぞれの事業に沿ったPerformance Budgetを設定することはパフォーマンス改善の戦略を立てる上でとても重要です。

継続的な測定、改善

You can't improve what you don't measure. - Peter Drucker

計測しないものは改善できない、ということですね。セッションの中でも計測ツールが複数紹介されました。

Lighthouse

Lighthouseとは、Webサイトの品質向上に役立つ、オープンソースの自動化されたツールです。 Chrome Dev ToolsのAuditsタブに組み込まれているので、すでに利用している方も多いと思います。 今回は最新の変更点を紹介していました。

  • PWAのチェック項目が追加
  • Scoreの判定が厳しくなった
  • 回線の呼称を変更(Fast 3G => Slow 4G)

Scoreの判定が厳しくなった結果、緑と黄の比率が大きく変わっています。

f:id:nishimurae:20181129182943p:plain:w560

Lighthouseはlighthouse-ciのように、CIにも活用しやすくなってきています。 また、他のツールとの統合も積極的に進めていて、CalibretreoSpeedCurveSquareSpaceといったサードパーティ製のツールが紹介されました。

Chrome User Experience Report

Chrome User Experience Report(CrUX)は、ChromeユーザーによるWeb体験の実データを集計したレポートです。 BigQueryやPageSpeed Insightsで利用することができます。

最近の更新では、国ごとにデータセットが用意され地域別の分析ができるそうです。また、CrUX Dashboardではレポートをカスタマイズし、共有できます。

f:id:nishimurae:20181130110514p:plain:w560

PageSpeed Insights

PageSpeed Insightsとは、Webサイトのパフォーマンスを数値化して具体的な改善点を提案してくれる、統一されたツールです。 今回は、新しくリリースしたv5での変更点が紹介されていました。

  • 分析にLighthouseを使用
  • Lighthouseのパフォーマンスカテゴリのスコアを提供
  • フィールドデータにChrome User Experience Report(CrUX)を活用

これまでPageSpeed Insightsは独自の仕組みで測定をしていましたが、Lighthouseを利用することで以前とは違う検証結果になっているようです。

web.dev

web.devは、Lighthouseを使用した統合ツールの1つです。計測ツールとドキュメントが一体化されているため、測定後の次のステップが分かりやすくなっています。 現在beta版として提供されているので、少し紹介しようと思います。

f:id:nishimurae:20181129161228p:plain:w560

Lighthouseを使って結果をレポートしてくれます。その下にTodoがあり、優先度も表示されています。 ドキュメントがリンクされているのも特徴的です。

いくつか計測ツールを紹介しましたが、Googleはこれまでバラバラだった計測ツールをLighthouseに一本化していくことを進めていくようです。 Google製のツールだけでなく、サードパーティ製のツールも奨励していました。

計測するツールに関する基本的な部分は、State of the Union for Speed Toolingで語られています。

画像やフォント、JavaScriptに関するパフォーマンス改善の具体的なTipsについては、Speed Essentials: Key Techniques for Fast Websitesを見ると参考になります。

2日目のセッション

2日目は現在開発している新しい技術について、これからの展望が語られました。
すでにトライアルできるものから開発初期段階のものまで様々です。

virtual-scroller

virtual-scrollerは、バーチャルスクロール(画面内の必要なコンテンツのみレンダリングし、ユーザーのスクロールに応じて更新していく)を実現します。
Layered APIプロジェクトの1つとして進められていて、開発初期段階です。

Virtual Scrollとは

大量のコンテンツを表示するケースではレンダリングに時間がかかってしまいます。 例としてAddress BookやSNSのフィードなどが挙げられていました。

Virtual Scrollでは、画面外の不要なDOMを削除し必要な部分のみレンダリングするため、このようなケースのパフォーマンス改善に有効です。

f:id:nishimurae:20181128164519g:plain:w350

Virtual Scrollの技術自体は以前から存在し、特にネイティブアプリではお馴染みの技術です。 しかしWebに関してはまだ最善とは言えず、解決すべき問題が存在します。例えば、

  • 同じコンテンツ内でのリンクが動かない(全てのDOMが揃っていないため)
  • ページ内検索できない
  • 検索にインデックスさせられない

SEO観点での問題が大きいですね。画面外のDOMが存在しないため、実用面においてある程度犠牲を払わなければならないのが現実です。

どのように解決するのか

virtual-scrollerでは、リンクやページ内検索などの問題をInvisible DOMで解決しようとしています。 Invisible DOMとは、見えないけれども検索ができるDOMです。 検索などはできますが、レイアウトやスタイリング等のコストが不要なためパフォーマンスが上がります。

f:id:nishimurae:20181128164700g:plain:w350

通常のVirtual Scrollでは、画面外の不要な部分はDOMが存在しませんでした。Invisible DOMを利用したvirtual-scrollerでは、画面外の部分は見えませんがDOM自体はすべて存在します。

virtual-scollerではInvisible DOMをサポートする他にも、新たな問題を探しその解決にも取り組むそうです。 virtual-scroller: Let there be less (DOM)で詳細が語られています。

バーチャルレンダリングでは主にSEO観点での懸念があり、メリット/デメリットを理解した上で使用する必要があるというのが現状です。 今後Invisible DOMをサポートできるようになればより実用的になりそうです。

Web Packaging

Web Packagingは、端的に言うとWebサイトをパッケージ化する技術のことで、今回はSigned ExchangesBundled Exchangesが紹介されました。

1つ目のSigned Exchangesは、Exchange(HTTPリクエスト/レスポンスのペア)に署名して、任意のキャッシュサーバーから配信できるようにする仕組みです。

活用例として、AMPページのURLの最適化が紹介されていました。 Signed Exchangesにより署名されたAMPページを公開することで、Google検索は、AMPのキャッシュから配信されるAMPページではなく、Googleのキャッシュから配信される署名されたAMPページにリンクするようになります。

これがユーザーにとって何が嬉しいのか、もう少し具体的な話をしましょう。

f:id:nishimurae:20181126181634p:plain:w350

こちらは一休レストランのAMPページです。Googleの管理するAMPのキャッシュサーバーから配信されているので、URLを見るとドメインはwww.google.co.jpとなっています。 また、AMPページ用のヘッダーがアドレスバーの下に表示されています(上図赤枠)。

Signed Exchangesを利用することで、AMPページを通常のページと同様のドメインで、AMP用のヘッダーの表示なしで提供することができます。

2つ目のBundled Exchangesは、複数のExchange(HTTPリクエスト/レスポンスのペア)を1つにまとめる仕組みです。 現在プロトタイプの段階だそうですが、オフラインでのPWAのインストールなどに活用できるとのことでした。

Portals

Portalsは、SPAのようにサイト・ページ間のスムーズな遷移をブラウザで実現してくれる、現在開発中の技術です。 セッションではNavigationからTransitionへと説明されていましたが、異なるドメインであっても遷移間の摩擦を感じないページの移動が可能になります。

f:id:nishimurae:20181122173534p:plain

メリットとしては、

  • サイトを再構築することなく、
  • 異なるドメインであっても、

seamlessなページ遷移を可能にするという点です。

<portal>タグを埋め込むことでiframeのように利用することができます。iframeとの具体的な違いは以下の通りです。

f:id:nishimurae:20181128172647p:plain:w560

Portalが作動すると、documentがportalに置き換えられるため、常にトップレベルのブラウジングコンテキストとして作られます。 iframeは入れ子構造になるため、その点でも違いがあります。

まだ仕様策定の初期段階ではありますが、実用化が楽しみです。 デモでは「となりのヤングジャンプ」の事例が紹介されていましたが、ページ数の多い電子書籍にはかなり活用できそうですね。

Web PackagingとPortalsの話は、From Low Friction to Zero Friction with Web Packaging and Portalsで語られました。

フォーラム

朝から夕方まで休憩を挟みながらセッションが続きますが、その間隣の会場ではフォーラムが開催されています。 ここでは各ブースでデモを見たり、Googleのエンジニアと直接交流することができます。 セッションで語られたweb.dev、Lighthouse、VisBugなどのデモはもちろん、WebのパフォーマンスやUI/UXなど何でも相談できるReview Clinicというブースもあります。

今回はReview Clinicで一休.com レストランのスマホサイトのUI/UXについて聞いてみました。

最近のUI変更について聞いてみました!

10月にリリースした、プラン一覧のフィルターのUIについても聞いてみました。 プランを平日限定、飲み放題などの属性や時間帯で絞ったり、並べ替えを切り替えたりする機能です。

f:id:nishimurae:20181126193118j:plain:w350

上からこだわりフィルター(横スクロール)、時間帯タブ、並べ替えトグルの順番で並んでいます。 便利ですが多機能なため、どのように分かりやすく見せるかが難しい点です。

全部違うデザインなので少し分かりにくい。揃えたほうが良いと思う。
例えばフィルターの部分に関しては、スクロールできることが明らかに分かる方が良い。
スクロールバーを常に表示するとか、文字が分かりやすく途中で切れるようにするなど、何がベストか探してみてほしい。

やはり懸念していたように、複雑に見えたようです。

明らかなデザインをどのように実現していくかは、実際のユーザーの行動を数値的に見て判断する必要があります。 A/BテストをしたりしてどのようなUIが好まれるのか深掘りしてみると良いかもしれません。

学んだこと

Be Obvious: 明らかなデザインを意識すべき

会話の中で何度も "Be Obvious" と語られていました。 Googleでも様々なUIをユーザーの行動の結果を見て比較しているけれど、「明らかなデザイン」が良いという結果が出ているそうです。

機能としてはまず使ってもらえないと意味がなく、使ってもらうためには明らかに分かりやすいデザインでなければならないと感じました。

具体的に言うと以下のようなアプローチがあります。

  • カルーセルは画像がスクロールする方向に必ず半分くらいはみ出るようにして明らかに続きがあるように見せる
  • クリックできるボタンのスタイルを揃えてクリックできないものと区別する
  • アイコンにテキストで機能的な説明をつける(例: トレンドのアイコンに「トレンド」と説明をつける、など)

アクセシビリティを意識すべき

アクセシビリティとは、簡単に言うと、いかなるユーザーが、いかなるデバイスを使い、いかなる環境の下であっても利用できるようにする、ということです。 Webの制作者としては、具体的に以下の3つを意識すると良い、と話されていました。

  • タップターゲット
  • alt属性
  • 色のコントラスト比

ちなみにアクセシビリティはLighthouseで測定できます。 1日目のセッションで紹介したVisBugでも簡単にチェックすることができるので、そちらを利用してもいいかもしれません。

定量/定性的なユーザーフィードバックを元に意思決定すべき

1つ目の明らかなデザインの中でも触れていますが、UIに関する意思決定は定量/定性的なユーザーフィードバックを元に判断するべき、という話もされていました。

当然のことですが、一概に「このデザインが良い・悪い」ということはなく、ユーザーの動向を見ながら改善戦略を考えることの大切さを改めて感じました。

まとめ

初のChrome Dev Summit参加でしたが、総じて興味深い内容が多かったです。 特にセッション内でもケーススタディが多く、PWAの導入事例が国内外問わず増えたと感じました。 ケーススタディでは、実際に各社が設定したPerformance Budgetの話もされていて参考になりました。

技術的な観点で言うと、最も興味深かったのはやはりWeb PackagingPortalsです。 アプリケーションを再構築することなく利用できる、というのが嬉しいですね。

また、フォーラムやアフターパーティーのように交流できる場もあり、Googleのエンジニアやデザイナーとフランクに話ができるのも魅力でした。

おまけ:Women & Allies Receptionに参加しました!

実はChrome Dev Summit 2018の前日の夜にWomen & Allies Receptionというイベントにも参加しました。

Women TechMakersが主催していて、Chrome Dev Summit 2018参加者の女性全員が招待されていたようです。 実際参加していたのは50名くらいだったと覚えています。

イベント名の通り女性同盟なので参加者同士の交流がメインで、Googleの女性エンジニア、プロダクトマネジャー数人によるKeynote sessionもありました。 また、Googleからは、男性も含めChrome Dev Summitでセッションを担当している方など他に数名参加されていました。

Keynote sessionでは、ハラスメント問題や女性スピーカーが少ない問題などが話されていました。 つい最近起こったGoogle社でのストライキも、ハラスメント問題が関係していましたね。

女性スピーカーが少ない問題については、こちらの記事が取り上げられていました。

インポスター症候群やハラスメントに対する恐れなどが挙げられています。 Keynote session後のQ&Aでもこのあたりに関する議論が多かったように感じます。

明日は@ryo511さんによる「SVGスプライトアイコンの作り方・使い方」です。お楽しみに!

イベント開催のお知らせ ~12/12(水) Ikyu Frontend Meetup~

こんにちは。今日はイベント開催のご案内です。

12/12(水) に一休.com / 一休レストランの開発事例についてのミートアップイベントを開催いたします。

Ikyu Frontend Meetup

今回は「フロントエンド開発」をテーマとして

  • 一休レストラン スマートフォン検索ページのSPA化
  • 一休.com スマートフォンホテルページのパフォーマンス改善

を軸にNuxt.jsの導入、コンポーネント設計、CSS設計、画像最適化によるパフォーマンス改善などの事例をご紹介いたします。

セッションのほかにも、パネルディスカッションを予定していますので、参加者の皆さまと交流しながら、日々の学びを交換できればと思っています。

お申込みはこちらから。

ikyu.connpass.com

イベント実施に至ったきっかけ

user-first.ikyu.co.jp

この記事を公開したところ、「情報交換しましょう!」というお問合せを多数いただいたので、内容を膨らませてフロントエンド開発の取組みについてお話したら面白いのではないか、ということでイベントを開催することにしました。

お問合せをいただいた会社の方には、よいきっかけを与えてくださり、とても感謝しています。ありがとうございます。


まだ、募集枠には余裕がありますので、皆さまのご応募をお待ちしております!

インフラエンジニアからSREへ ~クラウドとSaaS活用が変えるサービス運用のお仕事~

2018年4月、データセンター完全クローズ

一休は、今年の4月にデータセンターを完全にクローズしました。現在、すべてのサービスをAWSを使って提供しています。
この過程で各種運用ツールやビルド/デプロイのパイプラインなどをすべて外部サービスを使うように変更しました。
これによって、インフラエンジニアやサービス運用担当者の役割や業務が大きく変わりました。本稿では、その背景を簡単に紹介したいと思います。

ざっくり言えば、

  • 物理サーバのセットアップ&データセンターへの搬入のような仕事はなくなった。
  • アプライアンスの保守契約、パッチ適用、運用ツールのバックアップのような仕事もなくなった。
  • 各種メトリクスを見ながら、Infrastructure as Codeでクラウドリソースの管理や調整をする仕事がメインになった。
  • 必要に応じて、プロダクトのソースコードに踏み込んで必要な改修を行い、サービスの安定化を支援する仕事も増えている。

結果として、SREとしての役割が求められるようになっています。

クラウド移行

一休のクラウド移行は、2016年末にキックオフ、2017年夏のアプリケーションサーバの移行を経て、2018年2月のデータベースの移行で、完全に完了しました。
概要は、過去の@shibayanid:ninjinkunの記事でも紹介しています。

user-first.ikyu.co.jp

user-first.ikyu.co.jp

user-first.ikyu.co.jp

これに伴って、以下のようなツールやミドルウェアも外部サービスに移行しました。

  • ソースコード管理はGitHub Enterprise から GitHub.comへ
  • CI/CDはJenkins + ファイル同期ツールからAppVeyor & CircleCIへ
  • リバースプロキシはBIG-IPからFastlyへ
  • メールサーバはアプライアンス製品からSendGridへ
  • etc

それぞれに、

  • 移行の動機
  • 移行のタイミングで一緒に改善できたこと
  • 移行によってなくなった作業

があります。

GitHub Enterprise(GHE) から GitHub.comへ

  • [移行の動機] GHEを使い続ける必然性がない。
  • [一緒に改善] 不要ファイルを削除してリポジトリのサイズダウン。
  • [なくなった作業] GHEのバックアップとバージョンアップ。

もともと、社内にGitHub Enterprise(GHE)を立て、ソースコードを管理していました。しかし、社内環境で自前で管理する必然性がありませんでした。
そこで、運用環境をクラウドに移行するのをきっかけにして、GitHub.comへコードを移行しました。
このとき、宿泊予約サービスのソースコードリポジトリに大量に残っていた不要な画像をGitHubへの移行対象外にしました。これによってリポジトリが小さくなり、CI/CDの速度が向上し、開発時のリポジトリ操作の快適になりました。
そして、GHEのバックアップやバージョンアップという作業がなくなりました。

※ 画像の改善については、以下のid:kentana20のスライドに詳しいです。

speakerdeck.com

Jenkins + ファイル同期ツールからAppVeyor & CircleCIへ

  • [移行の動機] 複雑になってしまったJenkinsをクラウドに持っていく必然性がなかった。
  • [一緒に改善] ビルドパイプラインの安定化 &リリース回数アップ。
  • [なくなった作業] Jenkinsのジョブのメンテナンス、不安定なファイル同期ソフトの管理。

CI/CDは社内に立てたJenkinsが担っていました。自前でブルーグリーンデプロイを実現するために複雑なジョブを組んだ結果、メンテナンスしにくくなっていました。またJenkins はバージョン1系を使っていたのでジョブの定義をGitHubで管理できていませんでした。
そして大きな問題だったのがビルド結果を配布するのに使っていたファイル同期ツールです。このツール、有償の製品だったのですが、動作が安定しない。。。
結果、途中でデプロイが失敗して手動でリカバリする、という辛い作業を繰り返していました。
運用環境をクラウドに移行するに伴い、このデプロイの仕組みをそのまま持っていく必然性はありませんでした。
また、アプリケーションはAWS Elastic Beanstalk(EB)を使うことに決まっていたため、デプロイは完全にEBが提供仕組みに任せることができました。
あとは、ビルドをどうやるか、ですが、AppVeyorとCircleCIで実現することにしました。CircleCIだけではWindowsプラットフォームで動作するアプリケーションのビルドに対応できないため、AppVeyorも併用しています。
AppVeyorもCircleCIもymlファイルで制御できるので、アプリケーションのリポジトリで一緒に管理することでメンテナンスしやすくなりました。
この移行によって複雑なJenkinsジョブや不安定なファイル同期ソフトのメンテナンスをする必要がなくなり、CI/CDパイプライン自体が大幅に安定しました。
その結果、リリースに手がかからなくなったため、本番リリースの回数を増やすことができました。

BIG-IPからFastlyへ

  • [移行の動機] クラウドに持っていくのは高コスト。
  • [一緒に改善] FastlyのCDN積極活用によるサイトのUX改善と安定化。
  • [なくなった作業] アプライアンスの監視、メンテナンスや保守契約更新。

データセンターでは、ロードバランサ/リバースプロキシとしてBIG-IPを使っていました。
BIG-IPにはiRuleと呼ばれる独自のリライト、ルーティングの機構が搭載されており、一休でもこれを使っていましたが、管理が大変でした。
また、BIG-IPをクラウドに持っていくのも、移行後の運用コストを考えると難しいと判断しました。
そこで、クラウド移行に伴い、BIG-IPの代わりにFastlyを使うことにました。
FastlyをマネージドなVarnishが搭載されたリバースプロキシととらえ、VCLファイルをGitHubで管理し、TerraformとCircleCIでCI/CDを構築しました。結果、インフラエンジニアでなくても、ルーティングのルールを修正できるようになりました。変更に対する心理的な負担も大幅に軽減されました。
また、負荷対策やUXの改善のためにCDNとしての機能も積極的に活用しています。
さらに、にFastlyの進化の恩恵を自動的に受けられるのも大きいです。例えば、ほとんど何もせずにHTTP/2を導入することができました。

アプライアンス製品のメールサーバからSendGridへ

  • [移行の動機] クラウドにアプライアンスを持って行くわけにいかない。
  • [一緒に改善] メール関連の処理をリファクタリングして合理的に。メルマガ配信も簡単にSendGridに移行。
  • [なくなった作業] アプライアンスの監視、メンテナンスや保守契約更新。

データセンターではアプライアンス製品のSMTPサーバを使っていましたが、これもクラウドに持っていけません。そこですべてのメールをSendGridを使って送信するようにしました。日本の代理店である構造計画研究所様にもしっかりとサポートしていただきました。

移行の詳細は、過去の@shibayanの記事や、@minato128のスライドが詳しいです。

user-first.ikyu.co.jp

https://speakerdeck.com/minato128/ikyu-mail-platform

移行に伴い、積極的にリファクタリングしました。
例えば、メール送信の必要性を棚卸しして、不要なメールは廃止しました。また、開発者向けのエラー通知メールなどメールである必要のないものはSlackへの通知に切り替えました。
この移行でアプライアンスの保守更新やパッチ適用などの各種メンテナンス作業から解放されました。
また、別の外部サービスで実現していたマーケティングのメール配信も比較的簡単にSendGridに移行できました。これによってメールマガジンの配信コストが大幅に削減できました。

仕事が減った(^^) じゃあ何する?

ほかにも、アプリケーションログの管理や本番データベースの定期運用などにも同様の変化が起きました。この結果、インフラエンジニアの管理作業が大幅に削減できました。
では、今はどんなことをしているかというと、、、

Infrastructure as Codeでクラウドリソースの管理

もちろん、クラウドに移行したからといって、キャパシティプランニングや配置設計のような仕事がなくなるわけではありません。
むしろ、リソースを柔軟かつ素早く確保できるというクラウドのメリットを積極的に活用してサービスの安定化に貢献する必要があります。
一休ではクラウドリソースの管理をTerraformを使ってAWSのリソースを管理しています。
モニタリングはDataDogを使っています。DataDogのメトリクスを見ながら、リソースの消費具合の推移をにらみつつ、必要な調整を行うのは重要な仕事の一つです。
また、新規サービス構築ではプロダクトを開発しているエンジニアと協力して、Terraformを使って必要なクラウドインフラをセットアップし、モニタリングの設定を行います。Terraform自体のバージョンアップやメンテナンスも重要な仕事です。

プロダクトのソースコードに踏み込んで必要な改修を行う。

プロダクトのコードを修正する機会も増えています。特に、サービスの安定化に貢献できるような修正をしています。
例えば、キャッシュの導入や高負荷なSQLの分割 & 非同期化などです。

つまり、サービスの安定化がミッションになっています。

というわけで、 インフラエンジニア改めSREになりました。 一休ではインフラエンジニア改めSREとして活躍してくれる仲間を募集しています。

hrmos.co

当社については以下の紹介記事をご覧ください。

user-first.ikyu.co.jp

この記事の筆者について

  • システム本部CTO室所属の 徳武 です。
  • サービスの技術基盤の開発運用、宿泊サービスの開発支援を行なっています。

一休.comレストランのスマートフォン検索ページがSPAになりました

一休.com レストランは今年の 7 月 18 日、スマートフォン向け検索ページのリニューアルを行いました。このエントリーでは、その中身について少し紹介させていただきます。

検索ページの課題

一休.com レストランではスマートフォン向け検索ページに対して「遅い」という課題意識がありました。これは技術面で少しブレイクダウンすると;

  • パーソナライズドを含む複雑な処理を行っているため、サーバーサイド処理が重い。
  • UI 上無駄な遅延処理を行っているため、クライアントサイドの描画が遅い。

というサーバー側とクライアント側両方の課題がありました。クライアントサイドの「無駄な遅延処理」というのは;

  • 検索結果取得が REST API 化されているにも関わず、再検索の度にページリロードを行い、サーバーサイドの描画からやり直している。

という実装に問題がありました。下図がリニューアル前のページ描画の様子です:

f:id:supercalifragilisticexpiali:20180924215006p:plain
画面描画後に動的な検索結果が遅延描画されている図

社内では UI 上のこの問題点に対して課題意識が大きく;

「検索と再検索の回遊性を改善したい」

という要求が強くなっていました。

Web フロントエンドのコンポーネント化

話は変わり、一休.com レストランは古い技術セットの上に構築されています。Microsoft の Classic ASP と VBScript を主体としたアプリケーションで永らく運営されてきました。その一方でユーザー向け web ページの表現力を向上させて行きたいという要求から、Vue.js によるフロントエンド実装も徐々に始まっていました。

Vue.js は SPA のような高い表現力を簡単な記述で実現できるのが魅力です。従来の web 開発では;

  • HTML
  • CSS
  • JavaScript

という ファイルタイプによる縦割り開発 が主流でした。しかし;

  • BEM や ECSS のようなCSS フレームワークの提唱する思想、
  • Vue.js のような JavaScript フレームワークのメカニズム、
  • Web Components による標準化の流れ

から明らかなように、UI 改善において コンポーネントを中心に串刺し設計する事の重要性 が認知されてきています。

一休.com レストランでは、あらゆるフロントエンド実装を Vue.js に移行する事で Smart UI パターンのような開発形態から、コンポーネント化された web アプリケーションへ移行する事を技術的な目標としています。

実現すればコンポーネント指向の持つユーザーメリットを享受できるようになり、一休の掲げる「ユーザーファースト」を後押しする開発体制が実現できると考えています。

SEO とサーバーサイドレンダリング

しかしここで問題がありました。SEO の考慮です。一休.com レストランの高い集客力は SEO に真摯に取り組んできた結果でもあります。

この SEO の観点から;

  • ページ上のあらゆる重要なコンテンツは、サーバーサイドレンダリングされていなければならない。

という要求がありました。これは、クライアントサイドレンダリングを基本とする Vue.js でコンポーネント化された web アプリケーションへ移行する目論見と衝突しました。

  • コンポーネント指向開発を実現する事。
  • 重要なコンテンツはサーバーサイドレンダリングされる事。

この 2 つが「ユーザーファースト」を目指す上で必要な技術要件でした。

尚、今年の Google I/O 2018 の Google Webmasters からのアナウンスで状況は変わりつつありますが、リニューアルの意思決定から開発の段階では、クライアントサイドレンダリングで良しと言える状況ではありませんでした。

業務課題と技術課題の合致

このような背景から、コンポーネント指向とサーバーサイドレンダリングという 2 つ の技術課題の解決手段として、サーバーサイド JavaScript の導入が現実味を帯びてきました。Vue.js においてはユニバーサル JavaScript を実現するフレームワーク Nuxt.js が存在しており、これによるプロトタイプを社内で進めるようになります。

結果として、一休.com レストランの技術スタックの理想像は下図のようになって行きました:

f:id:supercalifragilisticexpiali:20180830215511p:plain
フロントエンド構成のヴィジョン

これなら業務課題である検索ページの UI 改善と、技術課題を解決できます。最終的に、一休.com レストランの検索ページリニューアルで、Nuxt.js の導入を決断しました。

次の項では Nuxt.js で実装が開始された検索ページの設計を紹介します。


コンポーネント指向設計

検索ページリニューアルでは、全面的にコンポーネント指向設計を導入しました。

コンポーネントの定義

一休.com レストランの web フロントエンドでは、コンポーネントを下図のように捉え定義しました:

f:id:supercalifragilisticexpiali:20180924222349p:plain
コンポーネントの定義

  • データ、テンプレート、ロジック、スタイル、それぞれ関連性が深いもの同士をモジュール化
  • ファイルタイプによる縦割りではなく、関連性によるファイルタイプ横断の串刺しでグループ化
  • フロントエンド実装のあらゆるアセットをコンポーネントと捉えて管理

上記を基本とし、CSS、JavaScript、画像、Vue 単一ファイルコンポーネントなど全ての分割粒度として、共通のコンポーネントという概念を前提としました。

ITCSS によるレイヤードアーキテクチャ

これらコンポーネントを共通のレイヤードアーキテクチャで管理するため、一休.com レストランでは CSS アーキテクチャの 1 つである ITCSS を採用しました。

f:id:supercalifragilisticexpiali:20180924220842p:plain
ITCSS レイヤードアーキテクチャ

ITCSS 自体は CSS エンジニアである Harry Roberts 氏 が提唱する CSS の詳細度を管理するためのレイヤードアーキテクチャです。

  • CSS でコンポーネント指向設計を実践する事を前提にしたアーキテクチャである点、
  • 抽象度を管理するレイヤードアーキテクチャとして柔軟である点、

これらの点から web フロントエンドのコンポーネント抽象度化レイヤーとして自然に捉え直す事ができます。このアーキテクチャを利用し、以下のようにレイヤーごとの責務を定義しました:

レイヤー 定義
Settings CSS 変数や、定数などのデータを扱う。
Tools CSS ミックスイン、フィルター、またはバリューオブジェクト、DTO のようなアプリケーション上の型となる定義を扱う。
Generic CSS 要素型セレクターによるグローバルスタイル定義、アプリケーション全体で共通化された処理、グローバルな副作用を持つビジネスロジックを扱う。
Elements Atoms のようなプリミティヴなコンポーネントを扱う。これ以下のレイヤーで Vue.js SFC を扱う。
Objects Molecules のようなアプリケーション上のコンテキストを含むコンポーネントを扱う。
Components Organisms のようなアプリケーション上意味のある機能単位のコンポーネントを扱う。

上図のように Elements レイヤーより Atomic Design のレイヤー概念も取り入れています。そもそもとして、下図のような Atomic Design によるレイヤードアーキテクチャも検討しました。

f:id:supercalifragilisticexpiali:20180924220846p:plain
Atomic Design レイヤードアーキテクチャ

しかし;

  • Atomic Design における Atoms レイヤーはグローバルなデータやビジネスロジックなどを表現するレイヤーとしてはスコープが広くなり過ぎる。
  • 一方でこれらの表現に向いていそうなクリーンアーキテクチャでは、UI コンポーネントの抽象化を表現するには表現力が低い。
  • ITCSS はその点で、グローバルなデータやビジネスロジックを表現するレイヤーと UI コンポーネントを表現するレイヤーが最初から定義されておりバランスが良い。

と考え ITCSS を採用しました。

パフォーマンスの観点

ITCSS は CSS 詳細度を管理するアーキテクチャであり CSS のクライアントパフォーマンスを最大化する目的があります。また web フロントエンドはコンパイラより実装者にパフォーマンス最適化の責務があると考えます。

この観点から Tools と Generic レイヤーでデータ定義とビジネスロジックを扱うレイヤーを分ける事が、オブジェクトにメソッドを生やしてビジネスロジックを実装するのを避ける事につながり、webpack の tree-shaking による最適化を享受しやすい実装を導出するメカニズムになると考えています。

コンポーネント指向の置き換え可能という特性を、パフォーマンスの観点を持ったレイヤードアーキテクチャで管理する事で DRY を実践しやすく、結果としてハイパフォーマンスなフロントエンドが実現可能なメカニズムとなる事を期待しています。


つづいて Nuxt.js による BFF 実装を進めていく上で難しかった点をいくつか紹介します。

ユニバーサル JavaScript

まず Nuxt.js の最大の特徴は、Vue.js による実装で、サーバーサイドもクライアントサイドも透過的に記述できる事でしょう。これによりコンポーネント指向設計において、サーバーとクライアントという動作環境の違いを、ファイルシステムの違い同様、串刺しにコンポーネントとしてカプセル化できるようになります。これが Nuxt.js の大きなメリットです。

サーバーサイドとクライアントサイド API の違い

しかし同じ JavaScript とは言え、Node.js と web ブラウザの API には違いがあります。Nuxt.js が用意していないインターフェースで、サーバー/クライアントを透過的に扱いたかったのが次の 2 つです:

  • サーバー/クライアントでの Cookie インターフェースの違いを吸収する
  • バグレポード Bugsnag のサーバー/クライアントサイドのクライアントを透過的に扱う

これらの実現に、Nuxt.js の modules と plugins 機能を用いました。

ユニバーサル Cookie

Nuxt.js で Cookie を透過的に扱うにあたって UniversalCookie コンポーネントを作成し、サーバー/クライアントサイドでの CRUD 処理を透過的に記述できるインターフェースを用意しました。そしてこれを Nuxt.js の plugins として登録しました。

plugins/cookie.ts:

import * as http from 'http';
import { createUniversalCookie } from '@/components/generic/UniversalCookie';

export default function ({ req, res }: { req: http.IncomingMessage, res: http.ServerResponse }, inject): void {
  const cookie = createUniversalCookie(req, res);
  inject('cookie', cookie);
}

上記のような plugin を登録する事で、Nuxt.js のコンテキスト上では app.$cookie.set(key, value)this.$cookie.get(key) と言った記述で、サーバー/クライント関係なく cookie の読み書きができるようになりました。

ユニバーサル Bugsnag

一休.com レストランではクライアントサイドのバグ検知に Bugsnag を利用しており、ユニバーサル JavaScript 化に当たって、サーバーサイドの JavaScript 処理エラーも Bugsnag へレポートするインターフェースを用意しました。

modules/bugsnag/index.js:

const path = require('path');
const bugsnag = require('bugsnag');

module.exports = function BugsnagModule(moduleOptions) {
  bugsnag.register(moduleOptions.SERVER_API_KEY, {
    appVersion: (process.env.VERSION_SHA1).slice(0, 7),
    autoCaptureSessions: false,
    autoNotify: process.env.NODE_ENV !== 'development',
    releaseStage: process.env.NODE_ENV || 'development',
  });
  this.nuxt.hook('render:setupMiddleware', app => app.use(bugsnag.requestHandler));
  this.nuxt.hook('render:errorMiddleware', app => app.use(bugsnag.errorHandler));
  this.addPlugin({
    src: path.resolve(__dirname, 'plugin.js'),
    options: moduleOptions,
  });
};

modules/bugsnag/plugin.js:

import Vue from 'vue';

export default function (context, inject) {
  const VERSION = (process.env.VERSION_SHA1).slice(0, 7);

  // サーバーサイド Bugsnag
  if (process.server) {
    const bugsnag = require('bugsnag');

    bugsnag.register('<%= options.SERVER_API_KEY %>', {
      appVersion: VERSION,
      autoCaptureSessions: false,
      autoNotify: process.env.NODE_ENV !== 'development',
      releaseStage: process.env.NODE_ENV,
    });

    inject('bugsnag', bugsnag);
  }

  // クライアントサイド Bugsnag
  if (process.client) {
    const bugsnagJs = require('bugsnag-js');
    const bugsnagVue = require('bugsnag-vue');

    const client = bugsnagJs({
      apiKey: '<%= options.CLIENT_API_KEY %>',
      appVersion: VERSION,
      autoCaptureSessions: false,
      autoNotify: process.env.NODE_ENV !== 'development',
      releaseStage: process.env.NODE_ENV,
    });

    client.use(bugsnagVue(Vue));
    inject('bugsnag', client);
  }
}

上記のような module を登録する事で、app.$bugsnag.notify(new Error('...')) ないし this.$bugsnag.notify(new Error('...')) でハンドリングされたエラー処理のレポートを透過的に記述できるようになり、例外もサーバー/クライアントサイド両方を検知できるようにしました。

ただしこれらは Nuxt.js のコンテキストを通じてインターフェースを初期化する必要があるため、 this.$cookiethis.$bugsnag への参照を持つコンポーネントは Nuxt.js 実装に密結合となります。なのでこれら plugins へのアクセスは layouts/pages を通じてのみ行うルールとし、コンポーネントの責務をコントロールしています。

副作用の考慮

クライアント JavaScript をユニバーサル JavaScript に対応する過程にも注意する事がありました。グローバルな初期処理による副作用です。

例えば都度参照ではコストが大きい window.innerWidth のようなグローバルプロパティ値をキャッシュする次のようなモジュールがあります。

windowsize.js:

let windowWidth;
let windowHeight;

function resize() {
  windowWidth = window.innerWidth;
  windowHeight = window.innerHeight;
}

window.addEventListener('resize', resize, false);
window.addEventListener('orientationchange', resize, false);

resize();

export {
  windowWidth,
  windowHeight,
};

このようなモジュールが Nuxt.js の pages コンポーネントで import されるとサーバーエラーとなり、ユーザーには 500 エラーが返る事になります。しかしクライアントサイドに限った JavaScript 実装であれば、ありがちな実装であり、グローバルな副作用を局所化する手段としても合理的です。しかし window オブジェクトのようにクライアント JavaScript にしか存在しない API の暗黙的な参照が発生する上記のようなモジュールをうっかり Nuxt.js で import すればアプリケーションが起動しなくなります。こういった点はユニバーサル JavaScript において煩わしさを感じる点でもあり、副作用の影響を考える上で面白い点でもあると思います。

これを解決する方法としては、あらゆるグローバルな処理を Nuxt.js (Vue.js) のライフサイクルに載せるという方法を取りました。上記の windowsize.js は次のように変更しました。

windowsize.js:

import Vue from 'vue';

export const WindowSize = Vue.extend({
  data() {
    return {
      height: undefined,
      width: undefined,
    };
  },
  created() {
    if (this.$isServer) {
      return;
    }
    window.addEventListener('resize', this.resize, false);
    window.addEventListener('orientationchange', this.resize, false);
    this.resize();
  },
  beforeDestroy() {
    window.removeEventListener('resize', this.resize);
    window.removeEventListener('orientationchange', this.resize);
  },
  methods: {
    resize(): void {
      this.height = window.innerHeight;
      this.width = window.innerWidth;
    },
  },
});

export const windowSize = new WindowSize();

こうする事でクライアントサイドの API に依存するグローバルな処理が記述されたモジュールでも透過的に import できるようになりました。

クライアントサイドのみまたはサーバーサイドのみの実装とは違い、いくつか考慮するべき事はありますが、結果的により堅牢な実装を求められる点がユニバーサル JavaScript の面白さでもあると思います。


リニューアルの成果

リリースされた検索ページでは下図のように不要なリロードを必要としない SPA にリニューアルされました。これによってスマートフォンでの再検索のストレスが軽減されたと考えています。

f:id:supercalifragilisticexpiali:20181004235229g:plain
SPA 化による不要なリロードの無くなった検索ページ

またクライアントサイドのパフォーマンス指標である Speed Index の RUM 値も、リリースを境に改善する事ができました。

f:id:supercalifragilisticexpiali:20181005103148p:plain
RUM-SpeedIndex トラッキング値の変化

このリニューアルを契機に Classic ASP による密結合なアプリケーション実装から、Python をバックエンド、API とし、BFF と Nuxt.js をフロントエンドとする疎結合な開発体制が確立しました。今後はこの開発体制のメリットを最大化しユーザー体験の向上へと繋げていくのが、新フロントエンドの課題です。

モダン・フロントエンドで提供する価値とは

変化の激しい web フロントエンドですが、昨年は PWA の推進や AMP の登場、パフォーマンス指標の定量化など、めまぐるしい年だったように思います。一休.com レストランの web サイトは、これら技術を最大限活用し、ユーザーにとってより良い web サービスを提供してゆきたいと考えています。

今後の web フロントエンドの取り組みとしては;

  • 継続的なパフォーマンス改善
  • PWA 化による web 体験のエンハンスメント
  • ユーザー体験の向上につながるドラスティックな UI 改善

などを考えています。

そんなわけで 一休.com レストランでは、ユニバーサル JavaScript が得意なフルスタックエンジニア、BFF 設計や GraphQL を得意とされる Node.js エンジニア、コンポーネント指向を実践でき Web Components のような標準仕様にも敏感な web フロントエンドエンジニア、デザインシステムや Brad Frost 氏の提唱する Atomic Design へ高い関心をお持ちのデザイナーなど、プロフェッショナリズムにあふれたメンバーを募集しています。ラグジュアリーなサービスを最高のクラフトマンシップで支えてくれる方からのご応募お待ちしております!

www.ikyu.co.jp

www.wantedly.com

以上、CTO 室レストラン担当エンジニアの id:supercalifragilisticexpiali がお伝えしました。

一休.comスマホサイトのパフォーマンス改善(サーバサイドとQAとリリース編)

こんにちは。 一休.comの開発基盤を担当しています、akasakasです。

今回は、一休.comスマートフォンホテルページリニューアルをリリースし、パフォーマンスが改善した話を書きます。

概要編はこちらになります。

user-first.ikyu.co.jp

JavaScriptパフォーマンス改善編はこちらになります。

user-first.ikyu.co.jp

CSS・その他パフォーマンスチューニング編はこちらになります。

user-first.ikyu.co.jp

この記事ではスマートフォンホテルページリニューアルで実施したサーバサイドチューニングについて書きます。

ここでお話しする内容

  • サーバサイドチューニング前後のOverview
  • プロジェクトの大まかなタイムライン
  • ボトルネック洗い出し
  • 対策
    • SQL改善
      • 不足インデックスの設定 => 600msの改善
      • 複雑なselect文をシンプルなselect文に分割し、aync/await で非同期化 => 130msの改善
    • Solr高速化
      • SolrへのリクエストをHttpClientを使って可能な限り async/awaitで非同期にする。=> 20ms程度の改善
      • Solrを5系から6系にバージョンアップ => 100msの改善
      • Solrが動いているEC2インスタンスをc4系からc5系にスケールアップ =>30msの改善
    • JavaScriptを使った遅延取得化 => 100msの改善
    • ここまでやって、速くなった、が、、、
    • アプリケーションをASP.NET MVCに移行 => 250msの改善
      • I/O処理を一か所に集めて、async/awaitで非同期
      • 鮮度が求められないデータはElasticacheでキャッシュ
  • QAとリリース
    • QA
      • chromelessを使った自動テスト
      • 手動&実機テスト
    • リリース
  • まとめ

サーバサイドチューニング前後のOverview

スマートフォンホテルページのサーバサイドの改善前の構成は、以下の図の通りです。

  • ASP.NET Web FormsのWebアプリがページを提供している
  • このWebアプリは、検索API経由で対象ホテルの宿泊プランや部屋の情報を取得している
  • このWebアプリは、宿泊プランや部屋の情報以外の情報は、検索APIを経由せずに直接DBアクセスして取得している

改善後は

  • WebアプリからASP.NET Web FormsからMVCにリプレイス
  • Webアプリは、DBを直接見ない
  • データ取得処理は、検索APIの一か所にまとめてasync/awaitで取得
  • 検索APIの呼び出しもasync/await
  • 鮮度を問わないデータはElasticaheでキャッシュ
改善前 改善後
f:id:akasakas:20180910203004p:plain f:id:akasakas:20180910203015p:plain
  • 検索APIが内部通信ではなくFastly経由になっているのは、Fastlyのキャッシュの仕組みを利用しているためです。

プロジェクトの大まかなタイムライン

  • 4月に、速度改善プロジェクトを本格的に始動
    • この時点の構成は↑の改善前の図
  • 4月の時点で、サーバサイドの処理速度は1週間平均で1500msくらい
  • これを、200ms以下にするのが目標
  • まず、比較的簡単な性能課題を解消することで、6月の時点で500msくらいまで改善
  • その後、全面的な作り直しを実施
  • 7月末にほぼ実装完了し、QA開始
  • 8月中旬に先行リリース、8月末に完全リリース
  • 最終的には、9月初頭の時点で、1週間平均で250msくらい
    • 中央値なら200ms前後。ほぼ目標達成
    • 現時点での構成は↑の改善後の図

ボトルネック洗い出し

まず、どこで時間がかかっているかを、洗い出しました。
ツールは、 Newrelic 。それと、アプリケーションのログも参考にしました。

見つかったボトルネックは、

  • 外部へのI/O処理、つまり、検索APIのデータベースアクセスとSolrへのクエリ
  • ユーザーの操作に応じてフロントエンドでJavaScriptを使って遅延取得すればいい情報をサーバーサイドで取得している。

まずは、一番取り組みやすく効果も見込めるselect文の改善に着手しました。
↑の改善前の図の「表示する情報を取得①」と「表示する情報を取得②」のselect文です。
Newrelicなら、ひとつのリクエストで、どのSQLが平均何回呼ばれ、1回あたりどのくらい時間がかかっているか教えてくれます。
有償版なら、対象のSQL文全体を表示してくれます。

対策

SQL改善

不足インデックスの設定 =>600msの改善

性能改善の基本的なアプローチで、一番簡単かつ効果があるのが、不足インデックスの付与です。
SQL Serverなので、SQL Server Management Studioでselect文を実行し、実行計画を確認すると不足インデックスをサジェストしてくれます。↓のように。

f:id:akasakas:20180910203027p:plain

Newrelicで採取したselect文にパラメータを補完して、本番と同等のデータが入っている開発用データベースでSQLを実行し、不足インデックスを洗い出します。その結果、数本のselect文でインデックス不足が見つかりました。
このインデックス付与で大幅に改善しました。

複雑なselect文をシンプルなselect文に分割し、aync/await で非同期化 => 130msの改善

インデックス付与には大きな効果があったのですが、まだまだ目標には達しません。
「表示する情報を取得②」のselect文はかなり複雑になっており、インデックスでの改善は見込めませんでした。
そこで、この複雑なselect文をデータの多重度がそろうように3分割し、async/awaitで非同期で取得して、ソースコード上で、Joinするようにしました。
ソースコード上でJoin、というとコードが複雑になりそうですが、LinqとObject to Object MapperライブラリであるMapster を活用することで、2,3行で実現できました。
.NET で Object to Object MapperというとAutoMapperがメジャーですが、より高速でよりシンプルに扱えるMapsterを選択しました。
検索APIは、ASP.NET Web APIですが、アクションメソッドにasyncキーワードがついていませんでした。そこで、このタイミングで、すべてのアクションメソッドをasyncにして、非同期処理をかけるようにしました。

Solr高速化

SolrへのリクエストをHttpClientを使って可能な限り async/awaitで非同期にする。=> 20ms程度の改善

検索APIのすべてのアクションメソッドをasyncにしたので、HttpClientを使った外部へのHttpリクエストもasync/awaitで非同期にできるようになったため、非同期にしました。
最初のリクエストの結果が次のリクエストの結果に入力になっている場合は、 非同期にできませんが、それ以外は可能な限り非同期化、です。

Solrを5系から6系にバージョンアップ => 100msの改善

もともとSolrの5系を使っていたので、6系にアップしました。
単純にバージョンを上げたら性能が改善するんじゃないか、という仮説でした。
これに伴ってJavaのバージョンも上がりました。

Solrが動いているEC2インスタンスをc4系からc5系にスケールアップ =>30msの改善

一休のサービスはすべてAWS上で運用されており、SolrもEC2で動いています。
c4系のインスタンスを使っていたのですが、昨年発表されたc5インスタンスに移行しました。
単純にCPUのスペックが上がっているので、cpu intensiveなSolrならある程度恩恵をうけるんじゃないか、という仮説でした。

JavaScriptを使った遅延取得化 => 100msの改善

Below the fold のパーツに対して、画面表示のリクエストでサーバサイドでデータを取得している箇所がありました。
これを、ユーザーの操作に応じてフロントエンドから非同期でデータを取得するようにしました

ここまでやって、速くなった、が、、、

ここまでの改善で、1500msかかっていた処理が、500msくらいまで改善しました。
大幅に改善したのですが、目標はサーバサイドの処理で200msを切る、です。
主に検索APIの改善でここまで高速化しました。
逆に言うと検索APIのこれ以上の雑巾絞りできなさそうです。

アプリケーションをASP.NET MVCに移行 => 250msの改善

あとは、Web Formsで作られているアプリケーションの改善のポイントを見出すしかありません。
ここまでの改善で、async/awaitを積極的に使えば、まだまだ改善が見込めることは明らかでした。
そこで、ホテルページだけASP.NET MVCで全面的に作り直すことにしました。以下のような理由です。

  • 既存のWeb Formsのコードがかなり複雑。この複雑さを前提にして速度改善を進めるのは厳しい。
  • フロントエンド側も、大幅な性能改善のためには全面的に書き直しが必要な状態だった。
  • Web Formsでasync/awaitを使うには特殊な書き方をする必要がある。Web Forms固有の概念や書き方に縛られたくない。

事前に @shibayan が横断的な関心事を処理するMVCの基盤を作ってくれていましたので、これをベースに黙々と作り直しをしました。
既存のコードを読み、動作を確認し、Todoを洗い出し、実装する、というのをぐるぐる繰り返しました。

I/O処理を一か所に集めて、async/awaitで非同期

改善前は、画面に表示する情報を、Web FormsのアプリケーションとWeb APIの検索APIの両方で取得していました。
これをWeb APIの一か所にまとめて、可能な限りasync/awaitを使って非同期にすることで、IO待ちをなるべく発生しないようにしました。

鮮度が求められないデータはElasticacheでキャッシュ

画面に表示する情報には、頻繁に変更されないデータがあります。その中で、データ取得のコストが高いデータはElasticacheでキャッシュするようにしました。

QAとリリース

QA

8割できかがったところで、同じく全面的に作り直しているフロントエンドと結合して、試験をします。
試験は、2段階に分けて行いました。

chromelessを使った自動テスト

今回の速度改善は、機能的には改善前と改善後で変わりません。既存のmasterブランチが正解の挙動です。
そこで、以下のようなスクリプトを作り自動テストを行いました。

  1. 本番のfastlyのアクセスログからリクエストパスを抽出する。
  2. 本番相当のデータが入っているデータベースにつないだmasterブランチとテスト対象のブランチに対して、chromelessで1. のリクエストパスでリクエスト。両者のレスポンスから表示情報を抽出。
  3. 2.の表示情報を突合。差異があったらエラーとする。

これを、2週間くらい毎日繰り返しました。リクエストにばらつきがあったほうが、バグの検出が高まるからです。
また、スクリプトをあまり厳密に作りこみすぎないように注意しました。発生条件が複雑なバグは手で触らないと見つからないだろう、と割り切って、あくまで「正常系のユースケースでちょっと触ったらすぐに見つかりそうなバグ」を見つけるためのツールとして作りました。

手動&実機テスト

実装が一通り終わった段階でテストケースを作成し、プロジェクトメンバー3人で実機テストをしました。
テストケースをすべて消化したら、会議室を押さえて、スマートフォンホテルページの仕様に詳しい有識者6人くらいで1時間黙々と実機で触りました。
この最後のQA会でもいくつかの重要な問題が見つかりました。
仕様に詳しい有識者だからこそ発見できる問題があり、ここで大きなBugfixを潰すことができたのは大きかったと思います。

リリース

リリースもテスト同様、QA同様、2段階で行いました。
まず、いくつかのホテル、施設のみを先行リリースします。

  • 表示情報が多く、それほどアクセスが多くない施設を選びました。施設タイプ(ホテル、旅館、ビジネスホテル、貸別荘)もばらつかせました。
  • 先行リリースには、IISのRewrite機能を使いました。

先行リリース後は、DatadogやNewrelic、Google Analyticsなどを用いて、先行リリースした施設に何かおかしなことが起こっていないか、注視しました。
QAフェーズに時間をかけたのが功を奏し、とくに問題は起こりませんでした。 1週間ほど様子を見て、問題なかったため、全面的にリリースしました。
サーバサイドの1週間平均の処理速度は、500msから250msまで下がりました。

まとめ

今回は、一休.comスマートフォンホテルページリニューアルをリリースし、パフォーマンスが改善した話を書きました。 その中でも、サーバサイドチューニングとQA・リリースについて書きました。 このプロジェクトを通じて、感じたことをまとめると

  • async/awaitは積極的に使っていくべき。
    • 複雑なJoinのselect文を一発実行するよりも、複数のシンプルなselect文をasync/awaitで発行して、ソースコード上でJoinするほうが、いろいろな面で良い。
    • 複雑なJoinのselect文はメンテしにくい。かつ、実行計画の変化で突然、遅くなる場合がある。
  • QA大事。
    • 計画に対して1週間ほどスケジュールが遅れましたが、これは、QAに予想以上に時間がかかったため。
    • QAにかかる工数の見積もりは難しい、と感じましたが、時間をかけただけの意味はあったようです。
    • リリース後に見つかったバグはほとんどなかった。
  • Newrelicは非常に価値のあるプロダクトだと改めて感じました。
    • ボトルネックの調査で大活躍。
    • 性能面の調査だけでなくインシデント発生時の調査でも有効。

一休.comスマホサイトのパフォーマンス改善(CSS・その他細かいチューニング編)

こんにちは。 一休.comの開発基盤を担当しています、akasakasです。

今回は、一休.comスマートフォンホテルページリニューアルをリリースし、パフォーマンスが改善したお話をさせて頂きます。

概要編とJavaScriptパフォーマンス改善編はこちらになります。

user-first.ikyu.co.jp

user-first.ikyu.co.jp

この記事ではスマートフォンホテルページリニューアルで実施したCSS・その他細かいチューニングについてお話しします。

ここでお話しする内容

  • CSS再設計&チューニング編
    • リニューアル前のスマートフォンホテルページのCSSの現状整理と抱えていた課題
    • リニューアルをするにあたり、CSS再設計
      • CSS Modules
      • FLOCSS
    • パフォーマンス
      • インライン展開
      • 非同期読み込み
    • ドキュメント整備
  • その他細かいチューニング編
    • resource hints/preconnect
    • 画像の遅延ロード
    • imgix
  • まとめ
  • おまけ:こんなスタイルは嫌だ

CSS再設計&チューニング編

リニューアル前のスマートフォンホテルページのCSSの現状整理と抱えていた課題

リニューアル前のスマートフォンホテルページのCSSに関する課題は以下の点が大きな悩みでした。

  • 7,000行越えのメンテナンス困難なCSS
  • どのスタイルがどこで使われているか不明
  • いろんなところでいろんなCSSファイルが呼ばれている

7,000行越えのメンテナンス困難なCSS

上書きに上書きを重ねているので、メンテナンスが大変でした。

f:id:akasakas:20180902204612p:plain:w300

どのスタイルがどこで使われているか不明

ネストが深く、命名ルールもなかったので、どのスタイルがどこで使われているのかがわかりづらかったです。

.search_cont #rmsrch_frame_disp_main .rmsrch_frame_disp_row .pep_frame {
  width: 24%;
}

いろんなところでいろんなCSSファイルが呼ばれている

概要編でお話しましたが 一休.comのほとんどはVB.NETでシステムが構築されています。
ASP.NET Web Formsベースの独自フレームワークです。

プレゼンテーション層はMasterPageがあり、
それぞれの画面に対して、aspxファイルがあり、
また、細かい部品ごとに対して、ascxファイルがあります。

それぞれのファイルに対して、cssファイルが定義されていたため、
bodyタグの途中でCSSファイルが呼ばれているなんてこともありました。

レイアウトは下記のようなイメージです。

f:id:akasakas:20180910191958p:plain

一例ですが、
divタグ内でstylesheetが呼ばれているということもありました。

f:id:akasakas:20180907234134p:plain:w500

リニューアルをするにあたり、CSS再設計

リニューアルをするにあたり、上記の課題を解決し、パフォーマンス面の改善も図りました。
キーワードは以下になります。

CSS Modules

一休では Vue.js を採用しています。
Vue コンポーネントに関しては CSS Modules を積極的に採用しています。
CSS Modules を採用し、コンポーネントごとにスコープを切れば、どのスタイルがどこで使用されているかがわかりやすくなり、メンテナンス性が保証されると考えました。

FLOCSS

しかし、CSS Modules 以外でグローバルに影響を及ぼすCSSを書かなければいけないケースが発生します。
上記のケースについては別途対応が必要だと考えました。
こちらはFLOCSSを採用しました。
FLOCSSについてはこちらをご覧いただければ、わかると思います。

github.com

ディレクトリ構成は下記のようなイメージです。

├─app
│      sd_hotel.css
│
├─foundation
│      _base.css
│      _reset.css
│
├─layout
│      _footer.css
│      _header.css
│
└─object
    ├─component
    │      _component1.css
    │      _component2.css
    │
    ├─project
    │  │
    │  └─hotel
    │          _hotel_part1.css
    │          _hotel_part2.css
    │
    └─utility
            _utility1.css
            _utility2.css

CSS プリプロセッサにPostCSSを採用しました。
Foundation/Layout/Objectのcssファイルを統括するためのsd_hotel.cssがあるようなイメージです。
sd_hotel.cssはFoundation/Layout/Object内のcssファイルを適宜インポートしています。
ここはFLOCSSの基本的な考え方を採用しています。

@charset "utf-8";

/* ==========================================================================
// Foundation
==========================================================================*/

@import "../foundation/_reset.css";
@import "../foundation/_base.css";

/* ==========================================================================
// Layout
==========================================================================*/

@import "../layout/_header.css";
@import "../layout/_footer.css";

/* ==========================================================================
// Object
==========================================================================*/

/* ==========================================================================
// Component
==========================================================================*/

@import "../object/component/_component1.css";

/* ==========================================================================
// Project
==========================================================================*/

@import "../object/project/hotel/_hotel_part1.css";
@import "../object/project/hotel/_hotel_part2.css";

/* ==========================================================================
// Utility
==========================================================================*/

@import "../object/utility/_utility2.css";

パフォーマンス

CSSはレンダリングブロック対象になります。 レンダリングブロックはパフォーマンス低下に繋がるので、これを解消するために下記2点を気をつけました。

インライン展開

Above the Fold に入るスタイルはインライン展開することでレンダリングブロックを回避しました。
ただ、htmlファイルに直接、インライン展開したスタイルを書くとメンテナンス性を保証することはできません。

そこで、サーバサイド側で外部cssファイルを読み込んで、インライン展開するヘルパーメソッドを1つ用意しました。
これにより、外部ファイルで管理できて、かつインライン展開ができるので、メンテナンス性とパフォーマンスの向上を図ることができました。

非同期読み込み

Below the Fold になるスタイルはインライン展開せずに、loadCSS を使って、非同期で読み込むようにしました。

ドキュメント整備

今後の色んなエンジニア・デザイナーがメンテナンスするためにドキュメントを整備しました。 書いたこととしては

  • 設計方針
  • ディレクトリ構成
  • 命名ルール
  • パフォーマンス
  • アンチパターン

です。

f:id:akasakas:20180920185039p:plain:w500

その他細かいチューニング編

resource hints/preconnect

サードパーティドメインに対してはpreconnectを指定して、あらかじめDNSの名前解決に加え、TCPコネクションまで貼っています。

画像の遅延ロード

Below the Fold の部分の画像は初回リクエストに含めず、遅延ロードさせてます。
一休では画像の遅延ロードにlazysizesを使っています。

github.com

imgix

imgixの導入で画像最適化ができたので、パフォーマンス改善に大きく寄与しました。
imgixについての記事はこちらをご覧頂ければと思います。

user-first.ikyu.co.jp

まとめ

今回は、一休.comスマートフォンホテルページリニューアルをリリースし、パフォーマンスが改善したお話をさせて頂きました。
その中でも、CSS・その他細かいチューニングについてお話させて頂きました。

リニューアル前には下記の課題がありましたが、

  • 7,000行越えのメンテナンス困難なCSS
  • どのスタイルがどこで使われているか不明
  • いろんなところでいろんなCSSファイルが呼ばれている

上記の課題に対して

  • CSS Modules/FLOCSS を採用し、メンテナンス性を担保
  • インライン展開/loadCSSを使った非同期読み込みでパフォーマンス改善

今後はこれをスマートフォンホテルページだけでなく、他のページにも展開して行き、ユーザに高い価値を提供していきたいと思います。

おまけ:こんなスタイルは嫌だ

CSS再設計をしている途中で見つけたリプレイス前の残念スタイル集を少しだけお見せします。
過去から学び、今後に活かしていきたいです。

HTMLに直接書いている

<span style="font-size:13px; font-weight:normal;"> 

ネストが深すぎる、HTML側のDOM修正時に影響を受けやすい

#sdGuidePage .figureimg-list-roomlist .shisetu_box.guide_top_photo .photoTop img,#sdGuidePage .figureimg-list-roomlist .shisetu_box.guide_top_photo .photoNext img,#sdGuidePage .figureimg-list-roomlist .shisetu_box.guide_top_photo .photoPrev img {
  height: 24px;
}

同じ内容

.mt10 {
    margin-top: 10px;
}

.top_m_10px {
    margin-top: 10px;
}

CSSエンジニア募集

一休ではユーザに高い価値を提供することができるCSSエンジニアを募集しています!

hrmos.co

参考資料

一休.comスマホサイトのパフォーマンス改善(JavaScript編)

宿泊事業本部の宇都宮です。

一休.com スマホサイトのホテルページパフォーマンス改善プロジェクトでは、フロントエンドには以下のような要件がありました。

  • デザイン面は既存を踏襲する
  • 機能はほぼ従来通り
  • 日付等を変更した際の再検索は、画面遷移を挟まず、画面内で行えるようにする
  • パフォーマンスをできるだけ改善する

要するに、従来と同様の機能+αを実現し、かつ、従来と同等以上のパフォーマンスを実現する、というミッションです。

このために、どのような取り組みを行ったか、紹介します。

パフォーマンス目標値の設定

まず、パフォーマンスの目標値を設定する必要があります。モバイルでは、ユーザの帯域幅は回線や時間帯によって大きな変動があります。多少回線状況が悪くても、閲覧を妨げない程度のパフォーマンスを実現する必要があります。

一休へアクセスするユーザのモニタリングを見ると、極端に遅い回線を使っているユーザは少ないことから、回線速度のベースラインは1.4Mbpsとしました(この数字はChrome DevToolsのFast 3Gの回線速度なので、シミュレートしやすく、開発上都合が良いという理由もあります)。また、ロードが完了し、ユーザーが操作できるようになるタイミング(TTI, Time To Interactive)までの所要時間は5秒以内を目標とします。

計測とボトルネックの把握

次に、現行サイトのパフォーマンスの計測を行いました。詳細な計測結果については、概要編をご覧ください。

計測の結果、現在のパフォーマンスは目標値(3G FastでTTI 5秒以内)に届いていないことがわかりました。TTIはおよそ15秒で、快適に使えるとはいいがたい状況でした。

また、フロントエンドでは、js/cssによるレンダリングブロックが大きなボトルネックになっていることがわかりました。

レンダリングブロックの解消

レンダリングブロックとは、ブラウザが画面の描画を開始するまでに読み込みの必要なリソースがあるせいで、レンダリングを始められない現象です。具体的には、headタグ内でjsやcssを読み込んでいる際に発生します。レンダリングブロックが発生しているページは、たとえサーバのレスポンスが十分に速くても、画面が表示されるのは遅くなってしまいます。

JavaScriptのレンダリングブロック解消のための手段は簡単で、全てのJavaScriptをbodyタグの末尾で読み込むことです。しかし、既存コードでは、headタグでライブラリを読み込んでいたり、bodyの途中にscriptタグを書いていたりして、レンダリングブロックが発生していました。そこで、全てのJSコードを見直し、bodyタグの末尾で読み込む単一のjsファイルが全ての処理の起点になるよう改めました。バグを作り込まないよう慎重に行う必要はありますが、それほど難しい作業ではありません。

これによってレンダリングブロックの解消ができました。めでたしめでたし…といいたいところですが、これだけでは十分なパフォーマンス改善は実現できませんでした。

リソースの削減

前述したように、帯域幅のベースラインは1.4Mbpsとしました。1.4Mbpsの回線で、5秒以内に操作可能にするためには、Webページの初期ロード時に読み込む全リソースの合計を、875KB(1.4Mbps(=175KB/s) * 5s)に収める必要があります。この数字はJavaScriptの実行時間等を考慮しない、理論上の上限値であり、実際にはこれより減らす必要があります。

では、一休の現行ホテルページはどうかというと、約1MBのリソースを読み込んでいました。これでは、どう頑張ってもパフォーマンスの目標値を達成することはできません。そこで、全リソースを合計したサイズの上限を、ひとまず700KBに設定します。

次に、Webページにおいて最も「重い」リソースであるJavaScriptについて。JavaScriptは、ダウンロードだけでなく、実行の時間もかかるため、極力サイズを減らす必要があります。一休の現行ホテルページでは400KBものJavaScriptを読み込んでいましたが、これは300KBに抑えることを目標にしました。 まとめると、

  • 全体で700KB
  • JSは300KB

を上限として、読み込むリソースの削減を目指しました。

JavaScriptの最適化

初期ロード時のJavaScriptの最適化には、2つのポイントがあります。1つはjsファイルの削減、もう1つは実行時間の削減です。

ここでは、主にビルド周り(webpack, Babel等)の最適化と、Vue.jsアプリケーションの最適化を行いました。

JSバンドルサイズの最適化

JavaScriptコードを削減するには、不要なコードの読み込みを減らす必要があります。一休.comでは、JavaScriptはwebpackでバンドルしています。従来のwebpackビルド設定は、バンドルの粒度が大きめで、不要なコードを読み込みがちという問題がありました。そこで、ページ毎にバンドルを作成するようにしました。これによって、不要コードを大幅に削減することができました。

また、後述しますが、dynamic importを使ってVueコンポーネントを動的に読み込むことで、初期ロード時のバンドルサイズを削減しています。

Babel設定の最適化

先日、Babel 7の正式版がリリースされました。Babel 7を導入することで、若干ですが、ビルドサイズの削減が見込めます。ホテルページの場合、productionビルド後のjsファイルのサイズが584KB => 549KBに減りました。

また、一休.comでは、IE 11でもPromise等のES2015以上で利用可能な機能を使えるようにするため、 @babel/polyfill を使っています。最新ブラウザをターゲットにするなら、polyfillを減らせます。そこで、モバイルOSのみをターゲットにビルドして、ビルド後のサイズがどの程度になるか確認しました。結果、ターゲットをiOS >= 9にすると544KB、iOS >= 10にすると518KBといった結果になりました(ちなみに、最新のChromeだけをターゲットにすると507KBまで減らせます)。

iOS 9系のユーザ数はかなり少なくなっていて、近い将来、ターゲットはiOS >= 10になることが予想されます。そこで、PCとモバイルでbabelのpresetを分け、別々にビルドするようにしました。

Vue.jsのランタイム限定ビルドを使う

Vue.jsには完全ビルドとランタイム限定ビルドがあります。ランタイム限定ビルドはVueコンポーネントのコンパイラを含まないため、30%ほどサイズが小さくなります。vue-cliを使えばデフォルトでランタイム限定ビルドが使われますが、webpackのconfigを手作りしている場合には完全ビルドを参照していることがあるので、設定を見直してみると良いと思います。 https://jp.vuejs.org/v2/guide/installation.html#%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE%E3%81%AA%E3%83%93%E3%83%AB%E3%83%89%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

遅延レンダリングによるJavaScript実行時間の削減

Vue.jsでは、条件付きレンダリングを行うためのディレクティブとして、 v-showv-if が用意されています。この2つの使い分けは、ドキュメントには以下のように記載されています。

一般的に、v-if はより高い切り替えコストを持っているのに対して、 v-show はより高い初期描画コストを持っています。 そのため、とても頻繁に何かを切り替える必要があれば v-show を選び、条件が実行時に変更することがほとんどない場合は、v-if を選びます。 https://jp.vuejs.org/v2/guide/conditional.html#v-if-vs-v-show

どちらもそれほど変わりがないように思えますが、大量のコンポーネントを条件付きレンダリングする際には、どちらを使うかはとても重要です。私は、条件付きレンダリングでは原則として v-if を使うべきだと考えています。なぜなら、v-ifを使うと、その要素は遅延レンダリングされるからです。

// SomeComponent のコードは実行されない
<some-component v-if="false" />

// SomeComponent のコードは実行され、描画も行われるが、非表示になる
<some-component v-show="false" />
  • デフォルトで非表示の要素 => v-ifで遅延レンダリングする
  • デフォルトで表示する要素 => 原則v-ifを使うが、v-showでもよい

といった具合で使い分けています。また、v-ifは次節で解説する非同期コンポーネントと組み合わせできる点でも、パフォーマンス上有利です。

非同期コンポーネント

ECMAScriptのモジュールシステムには、静的なimport(import 'module')と、動的なimport(import('module'))があります。後者の動的なimport(dynamic import)を使うと、必要になったタイミングでjsコードを取得できます。

特に、複雑なVueコンポーネントはサイズが大きくなりがちなため、dynamic importで動的にコードを読み込むようにすると、初期ロード時に必要なjsの量を大きく減らすことができます。

Vue.jsでは、以下のように、コンポーネント登録時にimport()を呼び出す関数を使用することで、コンポーネントをdynamic importできます。

// グローバル登録の場合
Vue.component('RoomDetail', () => import('./RoomDetail.vue'));

// ローカル登録の場合
export default {
  name: 'RoomList',
  components: {
    RoomDetail: () => import('./RoomDetail.vue'),
  }
};

このように、dynamic importされるVueコンポーネントのことを、Vue.jsのドキュメントでは非同期コンポーネント(async component)と呼んでいます。

なお、dynamic importは通信のオーバーヘッドが発生する分、静的なimportよりも遅いです。dynamic importの対象は、初期描画には不要なコンポーネントのみにすべきです。

また、v-ifでレンダリングしない状態になっているコンポーネントは、フラグがtrueになった段階でdynamic importされます。以下のように、初期状態では非表示で、フラグによってレンダリングされるコンポーネントがある場合、dynamic importを使用した遅延読み込みを検討すべきです。

<button type="button" @click="flag = true">Click me</button>

<!-- SomeComponentは非同期コンポーネントにできる -->
<some-component v-if="flag" />

<!-- このdivはコンポーネント化すれば非同期コンポーネントにできる -->
<div v-if="flag">
   ...
</div>

Vueコンポーネント設計のアンチパターン

Vueコンポーネントは、ビルド後のサイズはそれほど小さくありません。シンプルなコンポーネントでもminify後で1KBほどになります。たとえば、ボタンやアイコン等、見た目が異なるだけのコンポーネントを細かくコンポーネント化すると、ビルド後のjsファイルのサイズが膨らんでしまいます。パフォーマンスの観点からすると、細かいVueコンポーネントを大量に作るのは避けるべきです。

ライブラリの削減

不要なライブラリを削除するのはもちろんですが、それだけでなく、ライブラリの使用量自体を抑えることを考えるべきです。今回のプロジェクトでは、jquery-migrateを削除することはできましたが、jQueryを依存関係から取り除くには至りませんでした。 jQueryは軽量なライブラリではないため、パフォーマンスの観点からは削除したいライブラリの1つです。一方で、その便利さから至るところで使われているため、簡単に依存を取り除くことはできませんでした。

改善結果

概要編にもいくつか結果が載っていますが、ここでは転送量に着目するためWebPageTestの結果を掲載します。

Before

f:id:ryo-utsunomiya:20180918171234p:plain

After

f:id:ryo-utsunomiya:20180918171256p:plain

転送量(Fully Loaded > Bytes in)が300KB近く減り、Speed Indexは1000以上改善しました。

パフォーマンス監視の導入

パフォーマンスは放っておくと劣化していくので、Webサイトの速度を維持し続ける仕組みが必要です。ここでは、パフォーマンス予算(Performance Budget)の設定と、監視を行っています。具体的には、パフォーマンス監視SaaSのCalibreを使用し、予算を超過した際にアラートがSlackの開発者向けチャンネルに流れるようにしました。

f:id:ryo-utsunomiya:20180919104010p:plain

今後の展望

ホテルページについては一定のパフォーマンス改善を実現することができましたが、サイト全体としてはまだまだ伸びしろがあると考えています。スムーズに宿泊施設を探すには、ページ単体ではなく、検索導線(トップ・リスト・ホテル)の全体的な回遊性が重要です。パフォーマンス改善も含め、引き続きUI/UXの改善を行っていきたいと考えています。

一休では、UI/UXの改善に熱意のあるエンジニアを募集しています!

hrmos.co

一休.comスマホサイトのパフォーマンス改善(概要編)

こんにちは。 一休.comの開発基盤を担当しています、akasakasです。

今回は、一休.comスマートフォンホテルページリニューアルをリリースし、パフォーマンスが改善したお話をさせて頂きます。

UI部分は既存を踏襲する形をとり、UX・パフォーマンス改善にフォーカスして、所々で様々な工夫をしました。

お話ししたいことが盛りだくさんなので

  • 概要編
  • JavaScriptパフォーマンス改善編
  • CSS・その他細かいチューニング編
  • サーバサイド編

の4つに分けて、お送りしたいと思います。

この記事ではスマートフォンホテルページリニューアルの全体像についてお話しします。

詳しいお話をする前に:スマートフォンホテルページってどこ?

こちらになります

https://www.ikyu.com/sd/00001290/

f:id:akasakas:20180902185134p:plain:w300

ここでお話しする内容

  • リニューアル前後のパフォーマンス比較
    • PageSpeed Insights
    • Audits
    • Calibre
  • パフォーマンス改善が必要だった理由とリニューアルの背景
  • ASP.NET Web Forms(VB.NET) → ASP.NET MVC(C#) へのアーキテクチャリプレイス
  • 宿泊スマートフォンサイトが抱えていた大きなボトルネック
    • Time To First Byte
    • レンダリングブロック
    • 肥大化したJavaScript・CSS
  • それぞれの課題に対する解決策とアプローチ
  • まとめ

リニューアル前後のパフォーマンス比較

PageSpeed Insights

PageSpeed Insightsのスコア比較は以下の通りです。
サードパーティクッキーのキャッシュコントロール以外の項目は解消することができました。

before

f:id:akasakas:20180902185650p:plain:w400

after

f:id:akasakas:20180918174337p:plain:w400

Audits

Metricの各指標が改善され、指摘事項も概ね解消できているのがわかると思います。

前提

  • Chrome 68(Lighthouse 3.0beta)
  • Simulated Fast 3G, 4x CPU slowdown

before

f:id:akasakas:20180903103305p:plain:w600

after

f:id:akasakas:20180903103234p:plain:w600

Calibre

一休ではWebフロントエンドパフォーマンスモニタリングSaasの Cailbre を使っています。
Time To First Byte をはじめとして、各指標が改善されているのがわかります。

before

f:id:akasakas:20180902191714p:plain:w400

after

f:id:akasakas:20180902191725p:plain:w400

パフォーマンス改善が必要だった理由とリニューアルの背景

Mobile-First Indexなどの理由はあると思いますが、

  • 初期表示が遅かった
  • 再検索のたびにリロードが走り、遅かった

という点を改善し、ユーザに快適に使って欲しいというところがモチベーションとしてありました。

上記2点を解消する上で障壁となったのが

  • ASP.NET Web Forms(VB.NET) によるレガシーアーキテクチャ
  • 単純に非同期で読み込むことができないレンダリングブロックしている古のJavaScript
  • 上書きに上書きを重ね、7,000行を越えていたCSS(メンテナンス困難)

でした。

宿泊サイトは前回の大規模リプレイスから10年近くと長きに渡って価値を生んでいるサービスなのですが、上記の問題を抱えたままでパフォーマンスを改善し、より高い価値を提供することが難しくなってきたという背景がありました。

これを機会にスマートフォンホテルページのみ部分的に新しいアーキテクチャでリプレイスをしようという決断をしました。

ASP.NET Web Forms(VB.NET) → ASP.NET MVC(C#) へのアーキテクチャリプレイス

一休.comのほとんどはVB.NETでシステムが構築されています。
ASP.NET Web Formsベースの独自フレームワークです。
大規模リプレイスをしたのが2009年頃なので、宿泊サービスを10年弱支えてきてくれました。
ここまでのお話を聞いて頂ければ、色々と察してくださる方も多いと思います。

上述の通り、パフォーマンス面での性能・UX改善が難しくなってきたという背景から、 ASP.NET C# へのアーキテクチャリプレイスを一部実施しました。

過去の学びから大規模リプレイスはやめて、薄いコンポーネントだけ被せる、MVC の作法に則る形をとるようにしました。
ここは宿泊システムのレガシーアーキテクチャ改善を進めてくれた @shibayanに基盤を作ってもらいました。

補足:ASP.NET Core MVC (.NET Core)を選ばなかった理由

「なぜ、ASP.NET Core MVC (.NET Core)を選ばなかったのか?」と疑問に思われる方もいるかもしれませんので、補足しておきます。

  • 既存のFramework/Componentsを使う必要があり、そこの互換性が.NET Core だと対応してなかった(主に認証に関わる部分)
  • Web forms を捨てることにスコープを置いた
    • これまでのアーキテクチャ改善によって DB アクセス周りや新しい処理は C# へのリプレイスが進んでいた
    • プレゼンテーションレイヤーだけC#で書けない状況を打開することに注力した
  • パフォーマンス改善の文脈とは別にアーキテクチャ改善に重きを置いていたプロジェクトが先行して走っていて、スマートフォンホテルページリニューアルがそのパイロットプロジェクトとなった

という経緯があります。

宿泊スマートフォンサイトが抱えていた大きなボトルネック

アーキテクチャリプレイスだけで、万事解決!!!というわけではなく、他にもパフォーマンス面で抱えていた課題がありました。

上記でも簡単に触れましたが、宿泊スマートフォンサイトが抱えていた大きなボトルネックは以下になります。

Time To First Byte

複雑なSQLをいろんなところから複数回呼び出すような処理が散乱していました。 このため、Time To First Byteが遅かったです。 また、ASP.NET Web Forms(VB.NET)の独自フレームワークでは非同期処理の実装が難しかったという側面もあり、これもTime To First Byte悪化の一因でした。

レンダリングブロック

古のJavaScriptに悩まされ、単純にdefer/asyncを使った非同期読み込みができず、Critical Request Chainが長くなり、ページスピードを下げる一因となっていました。

before/afterでのCritical Request Chainの長さを見れば、一目瞭然だと思います。

before

f:id:akasakas:20180902203526p:plain:w400

after

f:id:akasakas:20180902203536p:plain:w400

肥大化したJavaScript・CSS

上述の通り、肥大化したJavaScript・7,000行越えのメンテナンス困難なCSSがあり、これもまたページスピードを悪化させている要因の1つとして大きかったです。

before:JavaScript転送量

ちなみに、画像転送量と同等のサイズでした。

f:id:akasakas:20180902204042p:plain:w300

after:JavaScript転送量

f:id:akasakas:20180902204052p:plain:w300

7000行越えのCSS

f:id:akasakas:20180902204612p:plain:w300

それぞれの課題に対する解決策とアプローチ

Time To First Byte

  • ASP.NET MVC C# により、非同期処理の実装が容易にできたこと
  • データ取得処理は、検索APIの一か所にまとめてasync/awaitで取得
  • 検索APIの呼び出しもasync/await
  • 鮮度を問わないデータはElasticaheでキャッシュ

というところで、Time To First Byte改善につなげました。

詳しい話はサーバサイド編でお話しできればと思います。

レンダリングブロック

ASP.NET MVC C# リプレイスに乗じて、古のJavaScriptたちも全て捨てました。

  • Vue.jsに寄せ、必要な箇所はコンポーネント化
  • async/deferで非同期読み込み
  • bodyタグの末尾でJavaScriptの読み込み

などでレンダリングブロックを解消し、Critical Request Chainを削減し、パフォーマンス改善ができました。

詳しくはJavaScriptパフォーマンス改善編でお話しできればと思います。

肥大化したJavaScript・CSS

上記に対しては

  • JavaScript
    • dynamic importによる初期ロード時のリソース削減
    • 遅延レンダリングによるJavaScript実行時間の削減
  • CSS
    • インライン展開
    • loadCSSを使った非同期読み込み
    • FLOCSSを採用
    • 不要なCSSセレクタの削除

を実施しました。

至極当然ですが、無駄なJavaScript・CSSをなくせたこともパフォーマンス面でけっこう大きいのかなと思いました。

詳しくは

  • JavaScriptパフォーマンス改善編
  • CSS・その他細かいチューニング編

でお話しできればと思います。

まとめ

一休.comスマートフォンホテルページリニューアルをリリースし、パフォーマンスが改善したお話をさせて頂きました。

単純にレガシーシステムからリプレイスするだけでパフォーマンスが劇的に向上するわけではなく、ちゃんとボトルネックを理解した上で改善しなければ、同じ轍を踏みかねないということを学ぶことができたのは個人的に大きかったです。

また、今まで長い間頑張って価値を生み続けてくれた既存のシステムには感謝と敬意を払いながら、未来のために新システムに切り替えていきたいです。

今回、お話しした内容はあくまでも概要であり、サーバサイド・フロントエンドそれぞれの深いところのお話しは次回以降、お話しできればと思っていますので、ご期待ください。