一休.com Developers Blog

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

E2EテストをSelenium Webdriver からCypress.io に移行した話

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

今回は、E2EテストをSelenium WebdriverからCypress.ioに移行した話をしたいと思います。

一休のE2Eテスト事情

一休では staging/production へのリリース完了をフックにして、主要導線に対してE2Eテストを実施しています。

これを実施している主な理由としては

  • 検証環境での障害の事前検知
  • リリース後も正常に予約ができるかどうかの確認
    • ECサイトで予約を止めるのは致命的なので、これを防ぐ

があります。

詳しくはこちらのスライドに書いてあるので、興味のある方はみてください。

speakerdeck.com

あれから、数年が経過して、、、

完全に動かなくなりました。悲しいです。

f:id:akasakas:20190421204926p:plain

どうしてこうなった???

理由としては

  • SelniumではSPAへの対応が難しくなってきた
  • なんでもかんでもSeleniumに任せようとした弊害

がありました

SeleniumではSPAへの対応が難しくなってきた

一休ではSPA化が徐々に進んできています。

具体的な取り組みについては下記のエントリで紹介しているので、ご興味があれば、ご覧ください。

user-first.ikyu.co.jp

user-first.ikyu.co.jp

Selnieum Webderiver は画面遷移をしていくMPAに対して、効果を発揮するブラウザテストツールであり、

  • 非同期リクエストや動的な画面の書き換え
  • 画面遷移が発生しない

SPAでSelenium Webdriverを使って安定したテストを継続していくのが困難でした。

Wait処理などを上手く使えば、不可能ではないですが、一休ではQA・テストエンジニアのようなポジションはいなく、開発者がテストも修正するようになってます。

開発者にテストを書く負担を減らして、サービス開発に集中して欲しいというのも思いとしてありました。

なんでもかんでもSeleniumで頑張ろうとした弊害

一休.com ではUTが充実していないためか、「なんでもかんでもSeleniumでテストしよう」みたいな雰囲気がありました。

具体的には、APIの疎通確認をしたいが為に、SwaggerUIのようなテスト用の画面を作成し、その画面をSeleniumを使って、APIの疎通確認を行っていました。

APIのテストをわざわざブラウザテストをする必要はないです。 ただでさえ、ブラウザテストは不安定で時間がかかるので、適切なレイヤーで適切なテストができていないというアンチパターンに陥っていました。

いざリプレイスへ・リプレイスをする上で気をつけたこと

上記の理由からSeleniumから別のブラウザテストツールの移行を検討しました

単純なツールの乗り換えだけだと、同じ過ちを繰り返す恐れがあったので、下記の点を注意しました。

  • 開発者フレンドリー
  • 安定性
  • 然るべきレイヤーでテストする(何でもかんでもブラウザテストにしない)

開発者フレンドリー

Selneiumの課題として、セットアップが面倒というのがありました。

開発者にテストへの時間を軽減して、サービス開発に集中して欲しいというのも思いがあったので、下記の点を重視しました。

  • セットアップの敷居が低いこと
  • 開発者が容易にテストを作ることができる

安定性

言わずもがなですが、「移行したはいいが、テストが落ちまくっている」というのは有り得ないので、

  • SPAでも安定してテストが動く

ということにフォーカスしました

然るべきレイヤーでテストする(何でもかんでもブラウザテストにしない)

前述でも書きましたが、

  • APIのテストを無理やりSeleniumで書いていた

というのが、テストの安定性を損ねていた原因の一つでした。

この問題に関しては、APIテストライブラリを導入して、ブラウザテストとは切り分けました。

APIテストライブラリに導入については後日、どこかで書きたいと思います。

技術選定

ブラウザテストでSeleniumからどのツールを選ぼうかを考えた際に、以下の3つが選択肢としてありました

  • WebdriverIO
  • Puppeteer
  • Cypress.io

どの技術を採用するかで重要視したポイントが「開発者フレンドリー」であるかです。

具体的には

  • セットアップ
  • 書きやすさ

の2点です。

f:id:akasakas:20190421211638p:plain

セットアップという点だと、Puppeteer・Cypress.ioがいい印象でした。

書きやすさで見た場合、Cypress.ioの方がテストを書くことに集中できると思ったので、Cypress.ioを採用することに決めました。

Cypress.io とは?

JavaScript製のブラウザテストに特化したE2Eテストフレームワークです。

Seleniumはテストを書くこと以外にもスクレイピング等の用途で使うことができますが、 Cypress.ioはテストを書くことに特化したE2Eテストツールです。

Cypress.io のいいところ

Cypress.io の特徴は色々あると思いますが、個人的に感じるところとしては、次の3点が大きいと思います。

  • セットアップが楽
  • テストを書くことだけに集中できる
  • CI連携が楽

セットアップが楽

Cypress.io はセットアップが非常に簡単です。

npm install cypress

これだけで終わりです。

SeleniumだとGeckodriverやChromedriverをインストールしたり、パス設定したりと、 少し手間がかかるので、セットアップの敷居が低いという点で、非常にありがたいです。

テストを書くことだけに集中できる

SeleniumやPuppeteerを選ぶと、

  • テストランナーどれを選ぼう
  • レポーティングはどれにしよう
  • アサートのライブラリはどれにしよう

などといったところも考えると思います。

Cypress.io はオールインワンでサポートしているので、テストを書くことだけに集中することができます。

https://www.cypress.io/how-it-works/ で紹介されている、下記の図のようなイメージです。

f:id:akasakas:20190421205119p:plain

CI連携が楽

CI連携が楽という点も個人的にはありがたかったです。

  • DockerImageが用意されている
  • 各CI Provider に対して、 example project が用意されていて、わかりやすい

こちらに詳細が書かれているので、興味のある方はご覧ください。

https://docs.cypress.io/guides/guides/continuous-integration.html

Cypress.io の頑張って欲しいところ

Cypress.io に対する不満はそんなにありませんが、あえて1点だけ挙げるなら

  • クロスブラウザ未対応

という点です。

一休で、E2Eテストを実施している目的は

  • 主要導線が正常に動くことを確認すること

なので、クロスブラウザで確認する必要性はないです(確認するに越したことはありませんが)

Cypress でもOpen Issue として挙げられているので、今後クロスブラウザ対応がされる日が来るかもしれません(いつになるのかはわかりませんが)

Proposal: Support for Cross Browser Testing · Issue #310 · cypress-io/cypress · GitHub

その他、移行に関しての細かい話

あと、移行に関する細かい話としては以下の3つがあります

  • 重複テストケースの排除
  • Page Object Design Patternで設計
  • 移行に乗じて、CIもJenkinsからCircleCIに変更

重複テストケースの排除

既存のテストケースを見直すと、同じようなことをテストしている部分があったので、 移行の際にテストケースを精査して、必要最低限のテストケースを実施するようにしました。

Page Object Design Patternで設計

既存のSeleiumでもPage Object Design Pattern を採用しましたが、 画面変更に対して強い設計方法なので、ここは変えませんでした。

移行に乗じて、CIもJenkinsからCircleCIに変更

以前はCIのためにオンプレサーバのJenkinsを用意していましたが、Jenkins起因でE2Eテストが失敗することもしばしばありました。 テストの安定性を考えた場合、CIも乗り換えた方がいいと感じていたので、このタイミングでCircleCIで実行するように変更しました。

そして、E2Eは復活し、平和はおとずれた

かくかくしかじかありまして、E2Eテストが復活しました。めでたしめでたし。

f:id:akasakas:20190421205259p:plain

まとめ

今回は、E2EテストをSelenium WebdriverからCypress.ioに移行した話をしました。

Seleniumがよくないとか、Cypress.ioがいいという話ではなく、 一休のサービス開発が進んでいった結果、SeleniumによるE2Eテストが難しくなり、今回Cypress.ioへの移行をしました。

Cypress.io の利点としては、上述でも書いた通り

  • 開発者フレンドリー

であることだと感じます。

一休のようにQAやテストエンジニアがいなく、開発者がE2Eテストを修正するようなワークフローになっている開発現場ではCypress.ioを採用するのは選択肢の一つとして、ありなのかなと思います。

しかし、正直な話、この仕組みも数年後には破綻するかもしれません。 その時はまたサクッと捨てて、その時に一休のサービス・開発現場にマッチする新しい仕組みに乗り換えればいいと思います。 そういうことができるようにブラウザレベルのテストを極力書かないほうがいいのかもしれません。

また、この数年間、一休.com を守ってくれたSeleniumには感謝と敬意を払いながら、未来のために新システムに切り替えていきたいです。

Rendertron導入でDynamic Renderingしている話

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

今回は、Rendertronを導入してDynamic Renderingをしている話をしたいと思います。

ここでお話しする内容

  • Dynamic Renderingについて
  • 一休.com/一休レストランでDynamic Renderingが必要になった背景
  • Rendertron とは
  • Rendertron にした理由
  • Rendertron 導入イメージ
  • クローキングの懸念
  • 苦労話
    • Rendertronのモバイル対応がバグってた
    • Rendertronのメモリリーク
    • AMPページに対してDynamic Renderingを適用するとレンダリング後が評価されてしまって正常なAMPとして認識されない
  • 学び
    • できたてのライブラリは不完全(どこかしらにバグは潜んでいる)
    • Dynamic Renderingさせるべき画面・させないほうがいい画面を明確にする
  • おまけ:Dynamic Renderingについて思うこと

Dynamic Renderingについて

Dynamic Renderingは、ユーザ向けのリクエストは正常に処理し、bot向けのリクエストはレンダラを経由し、静的HTMLを配信する方法です。

f:id:akasakas:20190308205632p:plain

Dynamic Renderingについては、いろんなところで言及されているので、詳細については割愛します。

詳しくはこちらに書かれているので、興味のある方はご覧ください。

ダイナミック レンダリングの使用方法  |  検索  |  Google Developers

Google ウェブマスター向け公式ブログ: Rendertron によるダイナミック レンダリング

一休.com/一休レストランでDynamic Renderingが必要になった背景

一休.comも一休レストランもSPA化が進んできたというが大きいです。

一休.comの場合

ホテルリストページ スマホ版の速度改善でmetaタグ等をJSで書いて、Full CSRにして、ページスピードを上げていきたいという背景がありました。

詳しくはこちらで書かれていますので、興味のある方はご覧いただければと思います。

user-first.ikyu.co.jp

一休レストランの場合

  • スマホページのSPA化対応後、SEO対策のためにフロント実装が複雑化し、パフォーマンス劣化を助長する形になっていた
  • 検索結果の取得は 1 つの API で出来るのに SEO 上必要な文字列を SSR で描画するため複数の API の待ち合わせをする必要があった
  • またそれらの SSR する情報は必ずしも Above the fold で表示されるコンテンツではないためパフォーマンスの観点で言えば遅延描画するのが合理的だった
  • SEO とユーザーパフォーマンス改善の 2 つがコンフリクトしている現実を突き付けられた
  • Dynamic Rendering ならそれぞれの用途に最適化した結果を返せる

user-first.ikyu.co.jp

一休.com・一休レストランともにDynamic Rendering の必要性が増してきたため、導入を検討しました。

Rendertronとは

Rendertron は Headless Chrome (Puppeteer) をベースとしたレンダラです。

github.com

Rendertronの役割としては

  • レンダリングさせるURLを受け取る
  • 受け取ったURLのJavaScriptまで実行と描画
  • 静的HTMLをレスポンスとして返す

といったところです。

デモ用のエンドポイントもあるので、これを触ればRendertronのことをある程度知ることができると思います。

https://render-tron.appspot.com/

Rendertronを採用した理由

正直な話、ちゃんとした理由はないです(笑)

  • id:supercalifragilisticexpiali がサクッとDockerfileを作ってくれた
  • ST/PRDまでの環境もサクッとできた
  • とりあえずできたから、本番投入して試してみよう

という勢いです。

Redertron以外の選択肢として、以下の2つもアリだと思うので、興味のある方は試してみていいと思います。

github.com

prerender.io

Rendertron 導入イメージ

導入イメージは下記になります。

f:id:akasakas:20190308205321p:plain

やっていることは2つです。

  • Fastlyでルーティングとレスポンスキャッシュ
  • レンダラはRendertronに任せる

という流れです

クローキングの懸念

Dynamic Rendering自体がGoogleお墨付きの手法なので、問題ないとは思いつつも不安でした。

モバイルフレンドリーテストや Fetch As Googleで問題ないのは確認済みだったのですが、それでもちょっと不安だったので、部分導入でSEO面で問題ないかどうかという検証を行いました。

結果としては、問題なかったです。

苦労話

Rendertronのモバイル対応がバグってた

Rendertron導入検討時はモバイル対応がバグってました。

現在は解消中ですが、下記のプルリクエストがマージされる前は、これを取りこんでいました

Fix mobile rendering by danielpoonwj · Pull Request #234 · GoogleChrome/rendertron · GitHub

Rendertronのメモリリーク

Redertronを数日間、運用してみると、メモリ使用量がどんどん増えていくのがわかりました。

http status code 400 か 403 の場合は puppeteerが起動したままになり、プロセスが残り続け、メモリを食いつぶしていくということになってました。

下記のプルリクエストで対応はしておりますが、 2019/03/12 時点ではまだマージされてないので、これを取り込んで、なんとか乗り切ってます。

Always close page before return to prevent memory leak by ramadimasatria · Pull Request #268 · GoogleChrome/rendertron · GitHub

AMPページに対してDynamic Renderingを適用するとレンダリング後が評価されてしまって正常なAMPとして認識されない

Rendertron導入当初、AMPページもDynamic Renderingの対象ページとして含めていました。

しかし、AMPページに対してDynamic Renderingを適用するとレンダリング後が評価されてしまって正常なAMPとして認識されないので、AMPページはDynamic Renderingさせないようにしました。

学び

できたてのライブラリは不完全(どこかしらにバグは潜んでいる)

単純に rendertronを git clone すれば、バグはないし、当然のように運用できるだろうと思ってましたが、その認識が甘かったです。

Dynamic Renderingさせるべき画面・させないほうがいい画面を明確にする

AMPページもそうですが、Dynamic Renderingさせるべき画面・させないほうがいい画面を明確にして、ルーティングを設定するのが重要だなと思いました。

Dynamice Rendering する必要のない画面に対してやっても、ダウンロード時間が余計にかかるため、SEOのスコアを落としかねないです。

Dynamic Rendering の使用方法 でも書かれています。

ダイナミック レンダリングは、JavaScript で生成される変更頻度の高いインデックス登録可能な一般公開コンテンツや、サイト運営が重視するクローラではサポートされていない JavaScript の機能を使用するコンテンツに適しています。 すべてのサイトでダイナミック レンダリングを使用する必要はありません。 ダイナミック レンダリングはクローラ向けの回避策であることに注意してください。

おまけ:Dynamic Renderingについて思うこと

Dynamic Renderingという手法自体が過渡期の技術という印象を受けました。

個人的に思うDynamic Renderingに関する違和感として

  • ユーザーに見せる画面とクローラに見せる画面を異なるように扱う
  • Googleが技術的にできないことを、こちら側でカバーしなければいけない

というのがあるのかなと思います。

Chrome Dev Summit 2018 でFuture Enhancement として古い(Chrome41相当)レンダリングエンジンを解消すると言ってましたが、将来的には、Googlebotが最新のChromeになって、普通にJSを実行しているという未来が早く来て欲しいです。

preloadで画像の表示速度を改善する

宿泊事業本部フロントエンドエンジニアの宇都宮です。先日、ホテルリストページの高速化に関する記事を書きましたが、Resource Hintsのpreloadを利用することで、さらに高速化できました。そこで、preloadによる画像読み込みの最適化方法を紹介します。

以前の記事はこちら:

一休.comホテルリストの表示速度を従来比2倍にしました - 一休.com Developers Blog

また、今回改善対象としたページには下記URLからアクセスできます(スマホでアクセスするか、PCからの場合はUAを偽装する必要があります)。

https://www.ikyu.com/sd/tokyo/140000/

改善前

f:id:ryo-utsunomiya:20190227154455p:plain:w480
PageSpeed Insights: 改善前

f:id:ryo-utsunomiya:20190227155059p:plain:w480
Calibre: 改善前

改善後

f:id:ryo-utsunomiya:20190304172545p:plain:w480
PageSpeed Insights: 改善後

f:id:ryo-utsunomiya:20190304175204p:plain:w480
Calibre: 改善後

今回の改善のターゲットは、施設の画像を早く取得して、画面の主要部分を素早く描画することです。したがって、見るべき指標はSpeed Indexで、ここは約0.7秒改善しています!

多少のブレはありますが、PageSpeed Insights/WebPageTest/Calibreでの複数回の計測でいずれも改善という結果だったので、画像のpreloadは「効果あり」と見てよいと思います。

やったこと

改善前のリストページでは、ページ表示の最終段階で、各施設の大きめの画像を取得するリクエストが走っていました。

f:id:ryo-utsunomiya:20190301172328p:plain:w480
改善前の施設画像取得リクエスト

このようになっている理由は、施設画像の取得リクエストが走るまでに以下のステップを踏む必要があったからです。

  1. 検索APIのレスポンス取得
  2. 検索結果のレンダリング完了
  3. lazyloadの発火

そこで、検索APIのレスポンス取得が終わったタイミングで、施設画像のpreloadを行っては? と思い、実装してみました。

preloadは、リソースの取得処理が実際に発火するよりも先に、ブラウザに「将来このリソースを取得します」と教えることで、ブラウザがリソースを先読みしてキャッシュできるようにする機能です。

developer.mozilla.org

具体的には、HTMLの <link rel="preload"> という要素にpreloadしたいリソースの種別(as)とURL(href)を書いておくと、ブラウザがこのリソースを先に読んでおいてくれます。

JavaScriptでpreloadを実行する場合、以下のような実装になると思います。preloadをサポートしているのはiOS 11.3以上なので、feature detectionは必須です。

// preloadのfeature detection
const supportsPreload = (() => {
  try {
    return document.createElement('link').relList.supports('preload');
  } catch (e) {
    return false;
  }
})();

/**
 * 指定したリソースをpreloadする
 * @param {string} href
 * @param {string} as
 */
function preload(href, as) {
  if (!supportsPreload) return;

  const link = document.createElement('link');
  link.setAttribute('rel', 'preload');
  link.setAttribute('as', as);
  link.setAttribute('href', href);
  link.onload = () => document.head.removeChild(link);
  document.head.appendChild(link);
}

/**
 * 画像をpreloadする
 * @param {string} href
 */
function preloadImage(href) {
  preload(href, 'image');
}

preloadの呼び出し側はこんな感じです。

// ファーストビューに入る施設の画像をpreload
searchResult.accommodationList
  .slice(0, Math.round(window.innerHeight / 300))
  .forEach(a => preloadImage(a.imageUrl));

これによって、以下のように、preloadした画像が優先的に読み込まれるようになりました。

f:id:ryo-utsunomiya:20190304173712p:plain:w480
改善後の施設画像取得リクエスト

この結果、ファーストビューが完全に描画されるまでの時間が短くなりました。

なお、まだviewportに入っていない施設の画像は従来通りlazyloadしているため、リクエストの終盤になっています。

「大きめの画像を全てpreloadする」 vs 「ファーストビューで見える画像のみpreloadする」で比較すると、後者の方が低速回線時のSpeed Indexが良くなったため、ファーストビューで見える画像のみpreloadしています。

まとめ

今回のように、大きめの画像をページ表示の後半で取得しているような場合には、preloadによって一定のパフォーマンス改善効果が得られることがわかりました。 preloadすることでパフォーマンスが改善されるかはアプリケーションの要件次第ですが、簡単に実装できるので、引き出しに入れておくと良いと思います。

一休.comホテルリストの表示速度を従来比2倍にしました

宿泊事業本部フロントエンドエンジニアの宇都宮です。

2018年度下期は、一休.comホテルリストページ スマホ版の速度改善に取り組んできました。その結果、ページのデザインはそのまま、機能面はリッチにしつつ、プロジェクト開始前の約2倍のスピードでページが表示されるようになりました。

本記事では、高速化のためにどのような施策を行ったのか紹介します。

なお、Webサイトの高速化手法については、ホテル詳細ページ高速化プロジェクトを実施した際にも記事を書いています。これらの記事で紹介している手法(たとえば、Imgixによる画像最適化等)については、記述を省略しています。あわせてご覧ください。

また、今回高速化の対象としたホテルリストページには以下のURLでアクセスできます(スマホで開くか、PCの場合はUAをスマホに偽装する必要があります)

https://www.ikyu.com/sd/tokyo/140000/

f:id:ryo-utsunomiya:20190227130107p:plain:w320
ホテルリストページ

プロジェクト開始前の状況

下記画像は、プロジェクト開始前の、PageSpeed Insightsの計測結果です。

f:id:ryo-utsunomiya:20190227094518p:plain:w480
PageSpeed Insights: プロジェクト開始前

パフォーマンス監視SaaSのCalibreでは、計測結果は以下のようになっていました。

f:id:ryo-utsunomiya:20190227094944p:plain:w480
Calibre: プロジェクト開始前

遅い4G回線相当の設定(下り1.4Mbps)とはいえ、Time to Interactiveに10秒以上かかっているのは遅いです。主要な指標(First Meaningful Paint、Speed Index、Time to Interactive)をそれぞれ半分にして、FMP 2秒、Speed Index 2.5秒、Time To Interactive 5.5秒くらいになれば、低速回線でもある程度快適に使えるサイトといえるでしょう。

改善結果

PageSpeed Insightsのスコアは従来の2倍以上になりました。

f:id:ryo-utsunomiya:20190227154455p:plain:w480
PageSpeed Insights: 改善後

Calibreでも指標が軒並み改善しています。TTIがFMPやSpeed Indexよりも早くなっているのは、APIレスポンス待ちでCPUがIdleになっているタイミングがあるからだと思われます。FMPまでの時間は半減しています。Speed Indexも改善していますが、もう一声ほしいところ。

f:id:ryo-utsunomiya:20190227155059p:plain:w480
Calibre: 改善後

計測が難しいため直接の指標にはしていませんでしたが、トップ(https://www.ikyu.com/sd/) => ホテルリストの遷移スピードは、体感的にはかなり速くなったように感じられます。これはFirst Contentful Paintの大幅改善(3.69s => 0.65s)が効いていそうです。


(2019-03-04 追記)本記事の「改善後」よりさらに高速化しました。2019-03-04現在のパフォーマンスは下記記事を参照してください:

user-first.ikyu.co.jp

やったこと

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

高速化を実施するには、まず目標となる値を設定する必要があります。目標設定に際しては、(1) 自分たちのサイトの要件から実現可能な数値であること (2) 競合と比較して遅くないこと の2点が重要だと考えています。また、可能であれば、競合よりも速くして、速度で差別化できると、なお良いでしょう。

今回のプロジェクトでは、Expediaのスマホ向けリストページをベンチマークにして、高速化に取り組みました。

このページは高度な最適化を施されており、PageSpeed Insightsのスコアは63点と、競合の中でも群を抜いて速いページです。

f:id:ryo-utsunomiya:20190227095620p:plain:w480
PageSpeed Insights: Expedia

そこで、今回のプロジェクトでは、以下の2段階のゴールを設けて高速化に取り組みました。

  1. PageSpeed Insights 50点以上、主要指標(FMP/Speed Index/TTI)で20%以上の改善
  2. PageSpeed Insights 65点以上、TTI 5秒以内

1は最低限達成したいゴール、2はややチャレンジングなゴールです。

施策1: ikyu-analytics-clientの最適化

一休では、アクセス解析ツールを内製しています。これはikyu-analyticsと呼ばれ、一休.com等では、ikyu-analyticsのクライアントライブラリを読み込んで使用しています。

パフォーマンスの観点で、ikyu-analytics-clientには大きな問題がありました。アクセスログの記録を 同期XHRで 行っていたのです。

私自身、Firefox等で、メインスレッド同期XHRのDeprecation Warningが出ていることは以前から認識していました。が、これがどの程度悪影響を及ぼしているのかは、分かっていませんでした。

しかし、昨年12月、WebPageTestのBlock機能を使ってikyu-analytics-clientの読み込みを行わないようにしたところ、ページの読み込み完了までの時間が約1秒短くなることに気づきました。

ikyu-analytics-clientのJSはサイズも小さく、やってることもアクセスログの送信程度です。したがって、ikyu-analytics-clientがもたらしている遅延のほとんどは、同期XHRのレスポンス待ち時間だと推測しました。

そこで、データサイエンス部と連携して、ikyu-analytics-clientのアクセスログ送信の非同期化に取り組みました。

具体的には、 navigator.sendBeacon()が使える場合はこれを使い、使えない場合は非同期XHRを行うようにしました。

navigator.sendBeacon() は比較的新しいAPIで、iOSでは11.1以上でないと使えません。非同期なデータ送信を確実に行えるという、非同期XHRにはない特長を持っています。一方、非同期XHRは送信中に画面を遷移したりページを閉じたりすると送信がキャンセルされます。これについては、データサイエンス部と協議し、非同期XHRのキャンセルによるログの送信失敗は許容する、という合意を取りました。

ikyu-analytics-clientの非同期化後、PageSpeed Insightsのスコアは10点改善しました。

f:id:ryo-utsunomiya:20190227103703p:plain:w480
PageSpeed Insights: ikyu-analytics-client非同期化後

ikyu-analytics-clientは、一休.comの全ページのみならず、一休レストランなどでも使用されているため、一休が運営しているサービス全体で、読み込み完了が1秒速くなりました。

逆にいうと、ikyu-analytics-clientが同期XHRを使っていたことで、一休のサービス全体が1秒遅くなっていたということです。 ブラウザのWarningにはちゃんと耳を傾けるべし という教訓を得られました。

施策2: コードの大幅な書き直し

ホテルリストページは、従来、ASP.NET WebForms + jQueryというスタックで実装されていました。これらを、ホテルページと同様、ASP.NET MVC(+Web API) + Vue.jsというスタックに置き換えました。

また、機能面では、検索実行時に毎回画面遷移していたのを改め、ページ内で再検索が行われるようにしました。

速いJavaScript/Vue.jsアプリケーションを書くための方法については、下記記事に書いた内容を踏襲しているので、省略します。

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

これに加えて、リストページの実装において特徴的なこととして、今回、Vuexは使いませんでした。

Vuexを使わなかった理由は、リストページはデータの流れがシンプル(検索APIからレスポンスを受け取り、描画するだけ)かつ、コンポーネントが素直なピラミッド構造になっていて、props down/events upで必要なデータの受け渡しを全て表現できたためです。また、Vuex Storeのコードは肥大化しがちなため、パフォーマンスの観点からの懸念もありました。

ただし、Vuexを完全に捨てたわけではなく、今後の改修で必要になれば、Vuexを導入する可能性はあります。

この書き直しによって、パフォーマンスは大きく改善しました。

f:id:ryo-utsunomiya:20190227105859p:plain:w480
PageSpeed Insights: リライト後

この時点で、ストレッチゴールの「PageSpeed Insights 65点以上」を達成できました 💪

施策3: 初回検索のAjax化

当初の目標を超えることができましたが、もう一押し改善できそうなポイントが残っていました。

施策2までの段階では、ページ初回表示時の検索処理は、サーバサイドで行っていました。SEOのためのtitleタグ、metaタグや、SNS等で共有する際に必要な情報(twitter card、facebook OGP等)を書くには検索結果を知っている必要があります。また、Botの中にはJavaScriptを実行しないものもあります。したがって、SEO関係のタグは、サーバサイドで書いて、初回レスポンスのHTMLに含める必要がありました。

一方、パフォーマンス改善の実験として、画面の初期表示時にサーバサイドで検索を行わずAjaxで行うようにしたところ、FCP/FMP/Time to Interactiveのそれぞれについて0.5秒程度の改善が見込めることがわかりました。

SEO関係のタグもJavaScriptで書くようにできれば、初回表示時に検索をサーバサイドで行う必要がなくなり、画面の初期表示はさらに速くできます。この問題の解決のためには、2つのアプローチが考えられました。1つはSSR、もう1つはDynamic Renderingです。

SSR(Nuxt.js)を採用しなかった理由

SSRを行って、JSの初回レンダリングの終わったHTMLを返すようにすれば、SEOの問題は解決します。

しかし、結論からいうと、SSR(Nuxt.js)は採用しませんでした。一休.comのアプリケーションの特性を考えると、SSRの導入によって遅くなる可能性が高いと考えたためです。

下記画像は先日Googleが公開したRendering on the Webというドキュメントから抜粋したものです。

f:id:ryo-utsunomiya:20190227112031p:plain
Rendering on the Web

Nuxt.jsを使ったSSRは、この表の「SSR with (Re)hydration」に該当します。一方、現行の実装は「Full CSR」です。この2つを見比べると、「SSR with (Re)hydration」の方がConsが増えているのがわかります。

SSR with (Re)hydrationは、サーバサイドでレンダリングを行い、フロントエンドでも、Full CSRと同等のリソースを読み込んで、状態の引き継ぎ(Rehydration)を行います。単純に考えると、Rehydrationの分、Full CSRと比べて計算量が増えます。

実際には、SSRを入れると遅くなるという単純な話ではなく、SSRでしかできない最適化を入れることで、Full CSRより速くできます。

しかし、「SSR後のHTMLをCDNでキャッシュする」という強力な最適化手法は、一休.comのアプリケーション要件では、あまり効果的ではありません。宿泊日程等の検索条件に応じて細かく画面を出し分ける必要があるため、同じHTMLを返却できるリクエストの数が多くないためです。

アプリケーション要件の見直しを行い、キャッシュフレンドリーな設計にすればSSRを導入する余地があります。しかし、今回のプロジェクトのスコープにはUIの刷新は含まれていません。現状の画面仕様では、Full CSRの方がパフォーマンスが出ると判断しました。

Dynamic Renderingの導入

Dynamic Renderingとは、bot向けに静的HTMLを配信する方法です。これによって、JS描画済みの静的HTMLをGooglebot等が取得するようになるので、metaタグ等をJSで書いても問題なくなります。

詳細は下記ドキュメントを参照してください。

ダイナミック レンダリングの使用方法  |  検索  |  Google Developers

一休では、Rendertronを使ってDynamic Renderingを行っています。 Rendertronの導入にあたっては色々苦労もあったようですが、これについては akasakas さんが書いてくれると思うので、ここでは詳しく触れません。

Dynamic Renderingによって、SEO関係のタグをJSで書く準備が整ったため、初回検索のAjax化に至りました。

この結果、冒頭の「改善結果」で紹介しているパフォーマンスが実現できました。

今後の展望

フロントエンドに関しては、パフォーマンス上のボトルネックのほとんどを解消した状態にもっていきました。一方、サーバサイドの検索APIについては、レスポンス速度がまちまちで、遅いときは1.5秒ほどかかることがあります。さらなる高速化のためには、検索APIの速度改善が必要そうです。

また、スムーズに宿泊施設を探すには、検索導線(トップ・リスト・ホテル)の全体的な回遊性が重要です。検索導線のSPA化等によって、ページ間のスムーズな移動を実現するような施策も検討しています。

We are hiring

hrmos.co

クラウド移行とSREについて講演をしました。

当社のクラウド移行とSREについて講演をしました

2019/1/30にitsearch+様で当社のクラウド移行とSREについて講演をしました。

news.mynavi.jp

発表資料はこちらです。ぜひ、ご覧ください。

speakerdeck.com

昨年11月に書いた以下の記事の内容に具体的な事例を交えつつ、当社のSREの取り組み方について発表をしました。

user-first.ikyu.co.jp

発表にも書きました通り、今後もコンテナ技術等、新しい技術を活用しつつ、ビジネスの成長を支える技術基盤開発、SREを実践していきたいと思います。We are hiring!!

hrmos.co

hrmos.co

おまけ

最近勉強になったSRE関連のリソース


この記事の筆者について

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

Bonfire Frontend #3で「一休.comのフロントエンドパフォーマンス改善」の話をしました

宿泊事業本部でフロントエンド開発をしている宇都宮です。

昨日(2019/1/24)、LODGEで開催された、Bonfire Frontend #3に登壇させていただきました。

Bonfire Frontend #3のテーマは「パフォーマンス改善」で、各社がパフォーマンス改善ネタを持ち寄って発表する会でした。

私は「一休.comのフロントエンドパフォーマンス改善」というタイトルで、フロントエンド全般のパフォーマンス改善についてお話ししました。

slides.com

フロントエンドのパフォーマンス改善においては、デファクトスタンダードな計測ツールであるLighthouseの使い方に習熟することが重要だと思っています。

また、2019年1月現在、Lighthouseを利用した計測環境として最もアクセスしやすいのがPageSpeed Insightsです。ただ、PageSpeed Insightsの計測には前提条件や若干の癖があるため、その辺の話も交えて、PageSpeed Insightsの話を厚めにしました。

その後は一休で行っているパフォーマンス改善の大まかな戦略や、今後のフロントエンドパフォーマンス改善に向けたアーキテクチャの構想などをお話ししました。

具体的なパフォーマンス改善の施策については、先月のIkyu Frontend Meetupで発表した下記スライドもあわせてご覧ください。

slides.com

speakerdeck.com

一休では、使いやすい予約サービスの提供のため、引き続きパフォーマンス改善活動を進めていきたいと思っています。

hrmos.co

一休.comホテルページのスマホ版からjQuery依存を取り除くためにやったこと

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

一休.comでWebフロントエンドを開発している宇都宮です。

先日、一休.comホテルページのスマホ版から、jQueryを取り除きました。jQueryを取り除いた経緯、やったこと、結果について書きます。

ちなみに、ホテルページには以下のURLでアクセスできます(スマホで開くか、PCの場合はUAをスマホに偽装する必要があります)

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

なぜjQueryを取り除いたのか?

JavaScriptサイズの削減のためです。一休.comホテルページは、以前は合計で約300KBのjsファイルを読み込んでいました(300KBはgzip後の転送量なので、実ファイルはもっと大きいです)。

よくいわれる「jsは170KB以内ルール」は、回線速度のベースラインが400Kbpsという前提1です。一休.comの平均的ユーザはもっと良質の回線2を使っているので、170KBまで切り詰めようと思っているわけではありません。

しかし、jQueryで実装されている処理は、最近のDOM APIを使えば代替可能です。ブラウザAPIの統一が進みつつある現在、jQueryを使う理由はないのでは? と考え、jQuery依存を取り除くプロジェクトを進めました。

どうやったのか

jQueryを使用している箇所は多かったため、細かくプルリクエストを切って、都度masterにマージしていく方針で進めました。

結果的に、修正プルリクは12個、総変更行数は±2500行程度になりました。

また、メインプロジェクトと並行して進めていたため、去年の8月頃から着手して、完了は先週でした。約4ヶ月かかった計算になります。

何をやったのか

ここからは、やったことを細かく書いていきます。

jQuery.ajax() => fetch に置き換え

jQuery.ajax() を、ブラウザの標準APIである fetch に置き換えました。fetchが利用可能なのはiOS 10.3以上なので、polyfillも導入しました。

github.com

※ライブラリをバンドルすると、全てのユーザにpolyfillを配信することになります。パフォーマンス観点からは、polyfill.iofetchが使えない場合のみpolyfillを使うのも良いと思います。

基本的には、Promiseを使っているところはそのまま置き換え、コールバックを使っているところはPromiseベースに書き換えました。1カ所だけ同期のajaxを使っているところがあったので、そこは非同期に書き直しました。

$.ajax('https://www.ikyu.com/api/...', {}).then(data => data);

const data = await fetch('https://www.ikyu.com/api/...').then(res => res.json());

fetchのpolyfillを採用した理由

比較したのはXMLHttpRequest(XHR)とaxiosですが、

XHRと比べると、

  • Pros
    • fetchはPromsieベースで、高レベルなAPIになっている
  • Cons
    • iOS 10.2以下ではpolyfillの読み込みが必要

axiosと比べると、

  • Pros
    • fetchはWebの標準APIであるのに対して、axiosはjQuery.ajax風の独自API
    • polyfillなので将来的にライブラリの読み込みをなくせる
    • whatwg-fetchはaxiosよりもサイズが小さい
    • axiosが提供しているような高度な機能(Universal JS、リクエストのキャンセル、transform/intercept等)は今のところ必要ない
  • Cons
    • 機能が少ない(たとえば、タイムアウト機能がない)

という感じかなと思います。

fetchのConsについては、

  • XHRを生で使うのは可読性の観点からはありえない
  • fetchに足りない機能は必要に応じて補うことができる

という理由から実質的に問題ないと考えて、fetchのpolyfillを採用しました。

DOM操作を標準APIに置き換え

jQueryで行っていたDOM操作を、全てブラウザの標準APIに置き換えます。jQuery => DOM APIの置き換えに関する包括的なドキュメントは以下がおすすめです。

github.com

ここでは、今回のプロジェクトで実際に使った置き換えのみ紹介します。

要素の取得

jQueryの $() は単体の取得とリストの取得を透過的に扱えるようになっていますが、DOM APIでは区別が必要です。

$(selector);

// 1個だけ取る
document.querySelector(selector);
// 要素のリストを取る
document.querySelectorAll(selector);
// 要素のリストを取って、一括操作する(NodeList.forEachはiOS 9では使えないので配列化している)
[...document.querySelectorAll(selector)].forEach(/**/);

注意が必要なのは存在しない要素へのクエリです。jQueryは、存在しない要素に対するクエリを発行して、返却されたオブジェクトにメソッドを発行しても、エラーにはなりません。存在したりしなかったりする要素に対する処理をjQueryで行っている場合、DOM APIへの置き換えは一手間必要です。

// エラーにならない
$('こんな要素はない').show();

// document.querySelector()の結果はnullなので、styleへのアクセスでエラー発生
document.querySelector('こんな要素はない').style.display = 'block';

show/hide

$el.show();
$el.hide();
$el.toggle();

el.style.display = '';
el.style.display = 'none';
if (el.ownerDocument.defaultView.getComputedStyle(el, null).display === 'none') {
  el.style.display = '';
} else {
  el.style.display = 'none';
}

実際に使う際は、関数化したほうがよいでしょう。

addClass/removeClass

class操作はclassListで置き換え可能です。IE 10以上対応なので安心。

$el.addClass('class');
$el.removeClass('class');
$el.hasClass('class');

el.classList.add('class');
el.classList.remove('class');
el.classList.contains('class');

html/text

$el.html(html);
$el.text(text);

el.innerHTML = html;
el.textContent = text;

アニメーション

jQueryのアニメーションAPIは手軽に使えて高機能なので、完全な置き換えは難しいです。 ユースケースに合わせて、CSSアニメーションに置き換えていくのがよいでしょう。

これについても https://github.com/nefe/You-Dont-Need-jQuery が参考になります。

You Don't Need jQueryには載っていない、アニメーションを伴うスクロールは以下のように実装しました。

/**
 * 指定した要素までスクロールする
 * @param {string} selector スクロール対象のHTML要素のCSSセレクタ
 * @param {number} step スクロール幅(px)
 * @param {number} timeout スクロールを行う間隔(ms)
 */
export function scrollToElement(
  selector,
  { step = 100, timeout = 16 } = {},
) {
  const target = document.querySelector(selector);
  if (!target) return;

  // 目的地のY座標
  const destY = target.offsetTop;

  // 目的地が現在位置より上にある場合は上(負のstep)、下にある場合は下(正のstep)にスクロール
  const stepWithDirection = destY < window.scrollY ? -step : step;

  const scrollByStep = () => {
    if (Math.abs(window.scrollY - destY) > step) {
      // step よりも距離が開いているときはscrollByで近づく
      window.scrollBy(0, stepWithDirection);
      setTimeout(scrollByStep, timeout);
    } else {
      // step 以下の距離まで近づいたらscrollToでピッタリ移動する
      window.scrollTo(0, destY);
    }
  };

  setTimeout(scrollByStep, timeout);
}

$.ready()

$.readyはブラウザの対応状況にあわせて load と DOMContentLoaded を使い分けてくれます。が、すでにDOMContentLoaded未対応ブラウザ(IE 8以前)は滅びているので、DOMContentLoaded のみでOKでしょう。

$.ready(function() {
  // 処理
});
$(function() {
  // 処理
});

document.addEventListener('DOMContentLoaded', () => {
  // 処理
})

イベントフィルタリング

jQueryだと、「doument配下のclickイベントを全てキャッチし、そのクリック対象、およびクリック対象の親要素が特定の属性をもつ場合にだけハンドラを実行する」という処理が、以下のように簡単に書けます。

$(document).on('click', '[data-xxx]', eventHandler);

これをDOMの標準APIで実装すると、少々面倒です。

function findParentByAttribute(target, attributeName) {
  let el = target;
  while (el.parentNode) {
    if (el.getAttribute(attributeName)) {
      return el;
    }
    el = el.parentNode;
  }
  return null;
}

document.addEventListener('click', event => {
  if (!findParentByAttribute(event.target, 'data-xxx')) return;
  // handle event
});

jQueryの使用を防ぐ目印

jQueryを取り除く作業をしたファイルには、先頭に以下の記述を追加して、jQueryを使ってはダメなことがわかるようにしました。

// このファイルではjQuery使用禁止!
const $ = undefined;

このコードは、ローカル変数の $ を定義して、undefinedで初期化します。これによって、グローバルな $ はローカルの $ でシャドウされます(グローバルな $ は上書きされませんが、シンボルの探索ではローカルの $ が優先されます)。さらに、$ の値はundefined なので、 $() などの呼び出しを行うとエラーが発生します。constなので再代入もできません。

これでも、 window.$window.jQuery 、jqueryのimportなど、jQueryにアクセスする手段は残されています。が、一休.com開発チームの規模やスキルを考えると、この方法で十分と判断しました。

なお、上記コードはES Modules(またはwebpack)環境での動作を前提にしています。ES Modulesはファイル毎のスコープを切ってくれますが、ES Modulesを使っていない場合も即時関数でスコープを切ることで同じことができます。

(function(){ // ファイルの先頭
  // このファイルではjQuery使用禁止!
  const $ = undefined;
...
})(); // ファイルの末尾

jQuery削除の効果

f:id:ryo-utsunomiya:20190117155101p:plain
jQuery削除前

f:id:ryo-utsunomiya:20190117154713p:plain
jQuery削除後

↑は、jQuery削除前後のPageSpeed Insightsのスコアです。どちらも71点。Time To Interactive/First CPU Idleは改善していますが、SpeedIndexは悪化しています。この程度の変動は何も変更しなくても起きるので、スコアが変わるほどのインパクトはなかったということですね。

パフォーマンス改善の観点からは、jQuery削除は、コスパが悪かったという結論になるかと思います。たぶん、同じ時間を別のタスクに使えば、もっと改善できたはず…。

なお、今回この結論に達したのは、既存コードのjQueryへの依存度が高かったからという理由もあります。サクッと取り除けるような状態なら、もっとコスパは良かったと思います。また、一休.comのホテルページスマホ版においては効果がなかったということであり、条件が異なれば、別の結果が得られると思います。

まとめ

パフォーマンスの観点からは、ロードするJSの量を減らすことは重要です。一方で、JSライブラリ30KB程度の削除だと、誤差の範囲程度の改善効果しか得られない、ということもわかりました。塵も積もれば山となるので、無駄ではないと思いますが、もっとコスパの良い改善施策を実施していきたいところです。

告知

Bonfire Frontend #3が、1/24に開催されます。テーマは「パフォーマンス改善」です。今回、Yahooグループのよしみで(?)お声がけいただき、登壇する機会をいただきました。今回の記事のような、一休.comで進めているパフォーマンス改善のお話しをしようと思っているので、是非ご参加ください!(すでに満席ですが、1/18に抽選なので、まだ間に合います)

yj-meetup.connpass.com


  1. http://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/

  2. 一休.comでは回線速度のベースラインを1.4Mbpsで考えています。LighthouseのSlow 4G相当です。

一休の1 to 1マーケティングを支えるプラットフォーム

データサイエンス部・大西 id:ohke です。

一休の1 to 1マーケティングを支えるプラットフォームについてお話したいと思います。

1 to 1マーケティング

一休の主力である宿泊予約サービスは今年で19年目、レストラン予約サービスも13年目を迎え、会員数も800万人を超えました。
一休のサービスを「知らなかった」から「知っている」という成熟フェーズに入ってきますと、集客に加えて、1 to 1マーケティングがより重要になってきてます。

一休の1 to 1マーケティングで大事にしていることは3点です。

  • 施策に必要なデータは全てデータウェアハウス (DWH) へ集約する
  • オートメーションツール (内製のWebアプリケーション) でターゲットの抽出、コンテンツの作成、配信を一元管理する
  • ユーザごとにコンテンツを最適化する (レコメンド)

f:id:ohke:20181228105756p:plain

DWHとETL

1 to 1マーケティングでは、ユーザの過去の行動に基づいて施策を実施していきます。
そのため、予約情報 (サービスのテーブルデータ) 、アクセス情報 (Google Analyticsと内製のリアルタイムログ収集ツール1) 、およびそれらと紐付くマスタデータなどは、全てDWHに集約しています。

DWHには、SQL Server (RDS) を使っています。SQL Server Management StudioやRedashでSQLを書けば、誰でも集計・分析を行えるようになります。

DWHへの日々のETL (抽出・加工・格納) は極めて重要になります。
ETLが失敗・遅延すると、データにアクセスできなくなり、1 to 1マーケティングに支障を来してしまいます。マーケティングの文脈以外にも、DWHは、CEOをはじめ各事業部長や集客チーム・UI/UXチームの定点観測や意思決定に利用されるため、データが無い・間違っていることによる影響は甚大です。

一休ではETLを管理するツールとして、Apache AIrflowを導入・運用してきました

user-first.ikyu.co.jp

日次だけで約400タスク (1タスクが1つのテーブルのエクスポートなどと対応します) 、さらに週毎・時毎などのタスクもあわせると、そこそこ大規模なETLとなります。

f:id:ohke:20181228125559p:plain

DWHとETLを健全な状態に維持し続けるために、以下の改善・運用も行ってます。

  • 社内の各部署から依頼されるDWHへのデータ追加や計算方法変更への対応
  • 各部署で自然発生的に持っていたETL処理もAIrflowへ統合
  • 事業担当者 (マーケタや営業) が実行するクエリのチューニング
  • 滞留クエリの自動検知・除去

オートメーションツール

1 to 1マーケティングでは、その名の通り、ユーザ一人ひとりに寄り添ってなければなりません。しかし一方で、アプローチするユーザ数も減らしたくないので、必然的に、少数のユーザに深くターゲットした施策をたくさん実行する必要があります。

1 to 1マーケティング施策の実行にあたっては、大まかに、ターゲットの抽出、コンテンツの作成、配信という3段階の作業を行います。
従来は他社のマーケティングメール配信サービスを使ってきましたが、数十以上の施策を実施するにあたって、大きな壁にぶつかりました。

  • ターゲットやコンテンツに埋め込む値の抽出は、マーケタの日々の手運用に頼られていたため、スケールしない
    • SQLで抽出したCSVファイルをファイルサーバにアップロードする、ということを施策ごとに行っていました
  • 配信は他社のサービス任せとなる (一休でコントロールできない) ため、事故無く運用するためには習熟が必要

また、メールだけではなく、サイトに来訪しているユーザにもアプローチするために、以下の新しいチャネルも要件として生まれていました。これらのチャネルをサービスに密に組み込んでしまうと、施策のたびにマーケタと事業部のエンジニア・デザイナが協同せざるえないため、クイックに施策を実行できなくなります。

  • サイトメッセージ: サイト上でのお知らせやクーポン配布に使われます
  • ポップアップ: サイトメッセージよりも訴求力が強く、クーポン配布に使われます

そこで、ターゲットの抽出・コンテンツの作成・配信を一元管理するオートメーションツール (Webアプリケーション) を内製することで、上の課題の解決を図りました。

  • ターゲットやコンテンツに埋め込む値の抽出を、DWHへ発行するSQLで記述できるようにする2
  • テキストやHTMLを編集できるようにすることで、自由にデザインを変更・プレビューできるようにする
  • 配信のチャネル (メール or サイト上のメッセージ or サイト上のポップアップ) 、日時、優先度などを確認・変更できるようにする
  • 配信は全てオートメーションツールで行うため、基本的には事業部のエンジニア・デザイナとコミュニケーションする必要が無い

f:id:ohke:20181226165711p:plain

オートメーションツールの導入と、その後のマーケタの細やかな施策設計によって、現在では毎日100近くの施策がこのツールから実行されています。

f:id:ohke:20181226163558p:plain

日々の効果測定は、Google Analyticsなどを使って収集し、Redashでビジュアライズして、Slackに通知するというカジュアルな方法を採ることが多いです。

f:id:ohke:20181227145344p:plain

オートメーションツールそのものは以下のアプリケーション構成となっています。

  • サーバサイドは、Python + Flask + SQL Alchemy
  • フロントエンドは、React
  • CI/CDは、CircleCI + CodeDeploy

オートメーションツールからの配信は、チャネルごとに異なる機構で行っています。

  • メールは、サービスと同等の構成をマーケティング用に構えました3
  • サイトメッセージとポップアップは、RDS (SQL Server) に配信リストを渡し、API (Go) 経由でサービスからアクセスすることで実現しました
  • それぞれの配信結果もDWHへETLすることで、開封率の集計や効果測定を行ってます

f:id:ohke:20181228104528p:plain

レコメンド

いくらオートメーションツールで簡単にユーザへ配信できるようになったとしても、そのユーザにとって嬉しい情報でなければなりません。

ホテルやレストランの予約を提供している一休にとって、「そのユーザがどのホテル or どのレストランをおすすめされると喜ばれるか」というのは、サイトでの検索結果やリマーケティング施策では重要なテーマです。特に、ユーザの直近の行動を反映できないリマーケティング施策 (例えば、数ヶ月前に宿泊したユーザを対象としたメール) では、興味を引くコンテンツとなっていないとユーザの心が離れる (要するにウザいと思われる) 要因となってしまいます。

一休では、サイトでの検索結果やリマーケティングメールに表示されるホテルやレストランのリストは、各ユーザの過去の行動を反映したものとなってます。レコメンドと呼んでいます。
具体的には、ホテルやレストランでの行動 (予約やアクセスなど) を元に、アイテムベースの協調フィルタリングを用いて、類似したホテルやレストランをおすすめするというロジックになっています。これらのロジックもDWH上のデータを使って計算しています。

サイトでの検索結果にも同様のロジックでレコメンドしてますが、リアルタイムな情報を使う必要があること、および、サービスに組み込むために堅牢性が求められることから、APIとして提供してます。
このAPIは以下のような構成となっています。こうしたサービスから利用されるアプリケーションの開発・運用もデータサイエンス部の役割です。

  • 実装はPython + Flaskで、全てオンメモリに展開してリクエスト都度で計算してます
  • インフラは、ECS + ElasticBeanstalk
  • CI/CDは、CircleCI

課題は山積みです

今回お話した中でも、まだまだ取り組めていない課題はたくさんあります。

  • DWH/ETLやオートメーションツールの安定化
    • 営業時間前にデータが出揃ってないと、集計・分析や施策の精度が落ちてしまうので、ETLの品質を担保し続けていかないといけません
    • そこまで熟達していないマーケタがSQLやデザインを自由に記述できるので、大きな事故につながらないようにシステムで細やかに予防する必要があります
      • 「検証配信」や「計算する」(件数チェック) はそのための仕組みとなってます
  • 配信タイミングの最適化
    • このユーザには朝8:00、そのユーザには夜7:00など、ユーザごとにチャネルに触れる時間帯が異なる
    • そのユーザがチャネルに触れる時間帯を狙ってアプローチしたい
  • 協調フィルタリングによって売り上げのリフトに成功していますが、更なるUX向上のために別のアプローチを模索中
    • 協調フィルタリングでは、ライトユーザの場合には情報が不足しているため、レコメンドによるリフトが小さくなってしまいます (いわゆるコールドスタート問題)
    • ヘビーユーザでも、ユーザの嗜好が常に一定では無く、ユーザのコンテキスト (目的、一緒に行く人、気分など) を汲み取る必要があります
      • 以前イタリア料理店に行ったからと言って、未来永劫イタリア料理店に行くとは限りません
    • リアルタイムのユーザ行動を反映しながら、その時点でそのユーザにとって最もフィットするホテル or レストランをおすすめできるようになることを目指してます

上に加えて、データサイエンス部としては他にも様々な課題に取り組んでいます。

  • 急増するレストランへの営業活動を最適化する
  • 抽象的なワードでも良い感じにレストランを検索できるようにする
  • クーポン施策の最適化 (損益分岐点の予測など)
  • などなどなどなど...

データサイエンス部では、こうした取り組みを4人で遂行しています (2018/12現在) 。

一緒に取り組んでいただける仲間を募ってます

DWHやETLの改善に燃える!

一休の経営や施策の根幹を成すデータの安定供給のために、自ら設計・構築・運用できます。

内製アプリケーションをクイックに開発・改善したい!

フロントエンド、バックエンド、インフラ、CI/CDを自分で考えて開発できます。

統計モデリングや機械学習でもっとかっこよく課題を解決したい!

高価格帯のサービスECサイトですので、他のサービスと一線を画したテーマの問題に取り組めます。

自ら考えた施策でもっとユーザに愛されるサービスへ育てたい!

CEO直下で施策の立案・遂行・改善に取り組めます。


  1. 詳しい経緯はこちら → データ分析基盤、その後 - 一休.com Developers Blog

  2. 一休のマーケタはSQLを書けます。

  3. 詳しくはこちら → 新メール配信基盤への移行 /ikyu-mail-platform - Speaker Deck