スマートフォン検索ページがSPAになるまで

稲尾遊氏:こんにちは。最後のセッションを発表させていただきます。タイトルどおりなのですが、「一休.com レストランのスマートフォン検索ページがSPAになりました」と題して発表させていただきます。

まず自己紹介です。

私は稲尾遊と申します。2016年に一休に入社しまして、現在はシステム本部のCTO室で、一休.com レストランのスマートフォン向けサイトのWebアプリ化をやっています。

簡単に一休.com レストランを紹介させていただきます。2006年にリリースされた、高成長を続けるファインダイニングの予約サービスです。

私が一休に入社する時に知り合いに「一休.com レストランで予約したお店なら間違いないよね」という感じで言われまして、「まぁ確かにそうだな」というところがあるので、もしレストラン予約で迷っている方がいらっしゃったら一休.com レストランを使っていただければと思います。

今日はブログで書かせていただいた内容を踏襲するかたちでご説明させていただければと思います。

話の幅が広いので全部話すと時間かかってしまいそうだったので、Universal JavaScriptについてはこのスライドには載せていません。なので、期待してた方はごめんなさい。

検索ページが抱えていた課題

まず、検索ページの課題として、こちらが一休.com レストランのスマートフォン向けの検索ページになります。基本的にこの2つの画面があります。

左側が検索結果で、右側は虫眼鏡をタップすると出てくる、検索条件を絞り込むためのパネル画面になります。この2つの画面はJavaScriptで動的に記述したSPA的な実装になっていました。

この検索ページに対して社内的にも要求はあったんですけど。「もっと使いやすいUIにしたいよね」とか「単純にちょっと遅いよね」という話とか、「検索をダイナミックにしたいね」みたいな話がありました。

「使いやすいUI」というのはちょっとぼんやりしているので、課題感としてまずフォーカスしたところが「なんか遅いよね」というところと「検索をダイナミックにしたいね」というところでした。

以前の開発体制

話は戻り2017年初頭、一休.com レストランは、フロントエンドをモダンな開発にしていく取り組みの中で、webpackを入れたりJavaScriptをESNextで記述するようになりました。

その頃はまだjQuaryを使った実装が主でしたが、そういった頃にCTOのから「フレームワークどうする? ReactとかVue.jsどうしようか?」みたいな話があって、「フロントエンド改善として、なにかフレームワークを入れたいよね」という話が出ました。

一休.com レストランの開発体制はこんな感じでした。

デザイナーとエンジニアがいるんですが、デザイン領域と設計・実装領域の中で、フロントエンドではHTML・CSS・JavaScriptを、程度の差はあれど、デザイナーもエンジニアも触るという環境でした。

とくにCSSに関しては実質的にデザイナーが記述の中心になることがほとんどで、デザイナーさん自身がCSSの中でデザインを調整するようなケースもわりとありました。そんな感じで「デザイナーがコードを書く環境が必要かな」と考えまして、「Vue.jsがいいんじゃないか?」というかたちで検討材料にあがってきました。

Vue.jsのいいところは、やはりHTML・CSS・JavaScriptらしい記述ができる単一ファイルコンポーネントであったり、日本語の公式ドキュメントが整備されていたり、技術的ジャンプを少なくモダン開発が進められそうなところでした。

一休.com レストランのチームはそこまでフロントエンドガチ勢のいるチームではなかったので、導入障壁としてはVue.jsが一番低いんじゃないか、ということでVue.jsを採用しました。

Vue.jsで実装した機能

それでは、今年までにスマートフォン向けのページでどういったものをVue.jsで実装してきたかを簡単にご紹介します。最初のほうに作られたのがこのナビゲーションドロワーです。

ハンバーガーメニューですね。こういったものがVue.jsで作られていたり。

あと、これは店舗紹介ページ、社内では「ガイド」と呼んでいますが、その中で表示する、販売中のプランの詳細を表示するモーダルです。

これは下からヒョイっと出てくる画面なんですけど、こういったものがVue.jsで実装されています。

あとこれは、その店舗紹介ページの中の口コミを表示する部分です。

ここを押すとアコーディオン状に開いたり閉じたりするところなのですが、こういったものもVue.jsで実装しています。

最近では、リアルタイムでキーワードを入力するとサジェスチョンが出て検索に飛べるという、ページ内検索モーダルのようなものもをVue.jsで実装しています。

話は戻り検索ページの課題についてです。「なんか遅いよね」というのと「検索をダイナミックにしたいね」という部分に関して、どれぐらい遅かったかという部分、メトリクスで出すとだいたいこんな感じでした。

Time to First Byteだけで4秒ぐらいかかっていて、Time to Interactiveに至るまでだいたい10秒ぐらい。これはキャリブレのLighthouseのスコアなんですが、3G回線でこのような状況でした。まぁ「けっこう遅いよね」というような感じでした。

数字的にも遅いですが、けっこうUX的に課題感がありました。パネルから検索条件を変更すると、SPAなんですがなぜかページリロードから始まって、そのあとページが読み込まれたあとに動的に検索結果をJavaScriptで描画するという、「SPAなのに毎回ページリロードするの無駄じゃない?」という実装になっていました。

なので、これをあるべき姿のちゃんとしたSPAにしようということで、検索ページの課題解決というかたちで始めていくことになったのが、2017年末のことです。CTO室が誕生したのがちょうどその頃だったんですが、CTO室の課題としてこれを解決することになりました。

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

技術的にどのように進めていこうかということで、昨今流行りのコンポーネント指向みたいのが出てきますが、その前に「“ちゃんとした”SPAとはなにか?」という部分です。SPAとは、みなさんご存じのようにSingle Page Applicationのことです。

最近、デザイン部門でも「越境」というキーワードがわりと流行っているのかなと思っています。「Beyond “the page” metaphor」というところで、Brad Frostの『Atomic Design』の本の中でこんな記述がありました。

「理解したり活用するときに、名前のインパクトはすごい大きいよね」という話が出てきました。

これはなにかというと、Single Page Applicationなんだけど、「僕らが作りたいのって、いわゆるWebページではなくて、Webアプリケーションだよね?」というところで、「それなら、いわゆるWebページを制作するフローに囚われていてはいけないんじゃないか?」という話が『Atomic Design』の序章のにも書いてあり、「なるほどな」と思いました。

Webページ開発における関心の分離というのは、いわゆるこういう感じなのではないかと考えています。

サーバサイドでHTMLを描画して、クライアント側でCSS・JavaScriptを実行してスタイルを当てて動的な動きをつけるというものかと思います。

一方で、Webアプリケーションとなったときに、サーバ側の役割というのはもっとロバストなものになるのかなと思っています。

単純に必要なデータを返す。クライアントサイドでViewとなるHTMLやCSS、JavaScriptを構築する。このときに、1つ新たなレイヤーとして登場するのが「コンポーネント」という概念です。

そんなコンポーネント指向設計のようなことをやるにあたって、Vue.jsのSingle File Componentを中心にフロントエンドを設計していくのが技術的には正しいアプローチではないかと考えました。

そうは言いつつも、一休.com レストランには以前からあるWebページも当然存在しているので、「その中に組み込むとしたらどんな感じになるか?」と考えたのがこちらです。

サーバサイドでHTMLを描画して、旧来どおりCSS・JavaScriptをクライアントを実行しつつ、コンポーネント指向を取り入れる部分だけクライアントでコンポーネント指向を実装します。

こういうのももちろん可能かとは思いますが、積極的にやりたい構成ではありません。それをやるならもっとやりやすい方法として、Vueの場合はNuxt.jsがいいのではないかと考えました。

NuxtのメリットはVue.jsのコンテキストでコンポーネント指向開発ができて、Vue.jsをベースに技術的ジャンプを抑えてアプリケーション開発というのものに移行できるところです。

あと、もちろんBFFやサーバサイドレンダリングの機能も提供していますし、フレームワークなので「Convention over Configuration」的な考え方で実装もしていけるという感じで、「わりとNuxt.jsいいんじゃないかな?」というのが徐々に自分の中で高まっていきました。

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

また話は変わります。一休.com レストランは高い成長率を維持するサービスなんですが、その成長率に寄与する大きな要素がSEOです。

SEOの取り組みで目立ったところとしては、例えば、2017年に一休.comの店舗ページがAMP対応しまして、いち早くGoogle AMPに対応することで検索結果への露出を高めるような取り組みもしました。

こういった取り組みをかなり速いスピード感で一休.com レストランの中でやっていますが、そこは強力なデジタルマーケティングチームがいて、売上を牽引しているということが、可能にしています。

そういったチームがフロントエンド開発にどんなものを求めているかというと、サーバサイドレンダリングです。ページ上の重要なコンテンツは静的コンテンツであることが求められました。

これは僕の心の声なんですが、当時SEOチームにいて、Vue.jsでUIのリッチ化をしたいと思いつつ、でも、事業インパクトに関わる部分は動的コンテンツができないので、すこし葛藤がありました。

そのため、現実逃避で「本当にSSRしかないのかな?」ということを時々調べたりもしました。例えば、2017年の8月ぐらいにGoogleのAdvocateの方がツイートしていたこちらのツイートや。

Understand rendering on Google Search」という記事が公開されました。

どんなことを言っているかというと、Googlebotの仕様を明文化してくれたドキュメントだと思いますが、「Chromeのバージョン41でレンダリングされているよ」とか、「基本的にはChrome 41なので、41が対応している機能はサポートしている」とか、「ES6はサポートしてないけど、JavaScriptは実行できるよ」ということが書いてありました。

加えて、10月頃には「Chrome Dev Summit 2017」が開催されて、WebコンポーネントのPolymerのセッションの最後のほうで、botに対してはrendered HTML、「静的なHTMLを返してね」という、今で言うDynamic Renderingにあたるようなヒントが公開されたりしました。

そういうわけで、動的なコンテンツをSEOフレンドリーにするというような意味でのブレークスルーがありそうでしたが、そうは言っても「やっぱりSSRが安心確実だよね」という流れは社内の中で大きかったです。

一方でVue.jsの実装も徐々に社内では広がりを見せており、SEOを担当するチームでも使ってもらっているような状況でした。

そんな時、たまたまSEOチームのデザイナーから「Vue.jsってサーバサイドレンダリングできないのかな?」みたいな話がありました。

それで「おっ?」と思って、Nuxt.jsを思い出しました。

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

こういった流れの中で、「コンポーネント指向開発をしたい」という技術的アプローチと「サーバサイドレンダリング」という業務課題、この2つを解決する方法として「Nuxt.jsはわりといいのではないか?」と社内で認知されるようになってきました。

話は戻り「ちゃんとしたSPAにしよう!」ということで、「ちゃんとしたSPAとは、こんな感じがいいんじゃないか?」ということで、ちょっと説明させていただきます。

いま、新しいバックエンドとしてPythonにリプレイスするという取り組みが進んでいます。 これをバックエンドとして、Nuxt.jsを使うためにNode.jsのBFFをやって、クライアントにはVue.js、Nuxt.jsで実装されたコンポーネントを返却するという構成にするのがいいんじゃないかという機運が高まり、検索ページのSPA化にNuxt.jsを採用することを決断しました。

コンポーネント指向での設計

では、実際にどうやって実装していくかということで、コンポーネント指向設計の話をします。

コンポーネントはこのようなイメージで定義しました。

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

「すべてをコンポーネントとして捉えたときに、どんな種類のコンポーネントが必要なのか?」ということが浮き彫りになってきました。プログラム上で必要な粒度としては、2種類に分けてざっくりこんな感じかなと思います。

「グローバルなデータ」は定数やグローバル変数みたいなもの。「型定義」はTypeScriptを徐々に取り入れているので、ここで定義するような列挙型とかモデルみたいなもの。

「共通もしくはグローバルロジック」はUIコンポーネント間で共通化したいロジックや、パフォーマンス上グローバル化したいロジック。それに加えて、そのViewとなる「UIコンポーネント」が必要だと考えました。

さらに、スタイル上必要な粒度がこちらです。

「グローバルデータ」としてはCSS変数。「フィルター」にあたるようなCSSのミックスインですね。あとは「グローバルスタイル」は、要素型セレクタへ指定するようなスタイルとか。「UIコンポーネント」はCSSで言うと、クラスセレクタに対してどういうスタイルを定義するかという部分になります。こういった分類が必要だと考えました。

ITCSSとは何か?

これらを適当にレイヤー化できる仕組みってないのかな? と考えていた時にたまたま目についたのが、CSSのアーキテクチャのITCSSです。

簡単に言ってしまうと「柔軟で移行しやすい構成」「BEMなどのCSS方法論との互換性」「コンポーネント指向のCSSアーキテクチャ」といった特徴がありました。

逆三角形の中に図示された抽象化の中で、適切にレイヤー化していこうという、アーキテクチャです。

そのレイヤーの分類としては、ITCSSはこういったレイヤー分けを提唱しています。

「Settings」はCSS変数を管理するレイヤー、「Tools」がCSSミックスインを管理するレイヤー、「Generic」がいわゆるリセットCSSを管理するレイヤー。「Elements」は要素型セレクタへのスタイルを管理するレイヤーです。

「Objects」がOOCSSとかで見られるMedia Object系のUIコンポーネントのスタイルを管理するレイヤー。「Components」がユーザーに機能を提供するUIコンポーネントのスタイルを管理するレイヤー。「Trumps」というのが最後にあって、これは例外的な用法で使えるレイヤーです。

こうした7つのレイヤーがデフォルトで提唱されています。

ITCSSを一休.com レストランに当てはめる

このレイヤードアーキテクチャにさっきの一休.com レストランの構成を当てはめてみると、このようになりました。

「Settings」レイヤーでは、スタイルに関してはCSS変数、プログラム上のものに関しては定数などのデータを扱い、「Tools」レイヤーでは、CSSはミックスイン、フィルター。もしくはプログラム上の話では、バリューオブジェクトとかDTOみたいなアプリケーション上の型となる定義を扱い、「Generic」では、グローバルスタイル、もしくはアプリケーション上共通化されたロジックとかグローバルなビジネスロジックを扱います。

「Elements」では、Atomic DesignにおけるAtomsのようなプリミティブなコンポーネントを扱い、「Objects」ではMoleculesのようなコンポーネントを扱い、「Components」レイヤーではOrganismのようなコンポーネントを扱う、というようなかたちで定義してみました。

意外とありなんじゃないかと思います。ただ、Atomic Designはこの手のレイヤードアーキテクチャの中では有名ですが、なぜAtomic Designを使わないのかを対照的に考えてみると、Atomic Designの場合はAtomsレイヤーにあたる抽象度がITCSSより粗いというか、細かく分類ができなさそうだと考えました。

その結果、一休.com レストランのフロントエンドのレイヤードアーキテクチャとしてITCSSを採用するに至りました。

Storybookでコンポーネントをホスティング

このように、レイヤードアーキテクチャによってコンポーネントを分類したので、それに沿って実装していきました。もちろんコンポーネントはUIなので、見た目としてどんなものが作られているのかわからないとつらいので、同時にStorybookでコンポーネントをホスティングするようにしました。

例えば「Settings」レイヤーではCSS変数を格納しているんですが、デザイナーと調整しながら、一休.com レストランのトンマナをCSS変数として定義して、それをStorybookで表示できるようにしました。

あとは「Elements」層では、わりとよく使うようなカルーセルの実装をコンポーネント化したりとか。

「Object」の層ではいわゆるMolecules的なイメージのコンポーネントを実装したり。

「Components」層では単一機能となるような1つの機能を提供するコンポーネントの実装をしたり。

このように、徐々にコンポーネントを増やしていきながら検索ページのリニューアルを行っていきました。

リニューアルの成果とこれから

結果としてどういったものが得られたのかというところでいうと、検索条件を変更するたびに起こっていたページリロードはせずに、もちろん直接、動的に検索結果を描画できるようになりました。

また、スコアとしてもだいぶ改善できました。

Time to First Byteがもともと4秒だったところが400ms。この前測ったらだいぶ短くなって、自分でもびっくりしました。バックエンドをPythonにしたことも大きいとは思いますが、こういったかたちで改善ができました。

Time to Interactiveに至るまでは10秒だったのが8秒というところで、やはりSPAでJavaScriptがヘビーになった部分があり、そこまで劇的には変化が見られなかった部分ではありますが、スピード改善もできました。

Lighthouseを使ったツールでの計測と合わせて、RUM-SpeedIndexというツールを使ってSpeed Indexを計測しました。

青色のグラフがそうなんですけど、この値もリニューアルを契機に、検索結果ページのSpeed Indexも改善できました。

このように、まだまだ道半ばな部分もありますが、一休.com レストランはWebらしく、プログレッシブに改善を続けていければと思います。

以上です。ご清聴ありがとうございました。

(会場拍手)