SPAをPWA/Universal対応するメリット

Angular、Reactなどを使ったSPA(Single Page Application)開発が一般的になりつつありますが、本記事ではPWA/Universal対応されたSPAがどう動くのかについて整理してみました。

PWA対応SPAの魅力

PWA(Progressive Web Apps)は、2015 年の Chrome Dev Summit でGoogleから発表されたテクノロジーで、3つの特徴が挙げられています

  • 信頼性(Reliable):Service Workerのバックグラウンド同期によってWebアプリをキャッシュし、オフラインでも利用可能にする
  • 早い(Fast):起動や挙動も速く、ユーザー体験を阻害しない
  • つながり(Engaging):PWAはアプリストアを経由せずにインストールが可能で、ホーム画面上にアイコンとして配置できる。また、Service Workerのプッシュ通知機能で直接、ユーザーに通知することができます。

ユースケースはこうです。「ブラウザを起動し、ググって、ウェブサイトにアクセスする」と、PWAで作られたサイトが出てきます。そこで表示される「ホーム画面に追加」を押すとアイコンがホーム画面に配置されます。これで次回からは「アイコンを起動」するだけでフルスクリーンでサイトが起動するようになります。アイコンになればプッシュ通知も可能なので、通知メールなどを介さずに直接やり取りをすることも可能です。

  • 左:Instagramにブラウザでアクセスすると「ホーム画面に追加」が表示される
  • 中:アイコンとして登録される。左隣はネイティブアプリのアイコン
  • 右:ネイティブアプリと似た画面。機能的には劣る部分もある

従来、モバイルアプリを使ってもらうには「ウェブサイトでアプリストアへのリンクをクリック」「アプリストアでインストール」ということが必要です。PWAにすればアプリストアを介することなくWebサイトからスムーズにアイコン化することができ、ユーザー体験の向上が期待できます。SPAはネイティブアプリに比べて限界がありますが、ギャップが小さくなってきており、ある程度のサイトであればPWAだけで十分対応できそうです。

ただし、現状のiOSではWeb Push通知などの一部の機能が動かないため「PWAだけ」という判断はしにくい状況です。これはAppleが「アプリストアを経由しない(ので、統制も効かず、手数料も取れない)」という特徴を嫌がっているからだ、と言われています。iOSが対応するか、というのが今後のPWAの運命を決めそうですが、一方で、世の中の流れはPWAになってきており、あとはAppleが対応するだけ、とも見れます。

Universal JavaScript

そんなSPAですが課題もあります。それはSEOが苦手、という点です。SPAではHTMLがブラウザ内部でしかレンダリングされません。一方、サーチエンジンのクローラーはサーバにアクセスしてHTMLを解析します[1]。つまり、サーバ側にクローラーが読み込めんでインデクシングできるHTMLが存在しないのです。サーチエンジンに引っかからなければサイトにたどり着けませんから、PWAのメリットも意味が無くなります。そこで「クローラーに食わせるためのページをサーバーサイドで生成する」ことが必要です。とはいえ、SPAとは別にサーバサイドのアプリケーションを開発しては本末転倒です。

そこで考えられたのが「Universal JavaScript」です。「Universal JavaScript」は「クライアントで動くJavaScriptを、サーバサイドでも動かす」というものです(参考記事:Universal JavaScript)。つまり「SPAをサーバサイドでも動かして、ブラウザ内部でレンダリングが完了されたものと同じHTMLを出力」して、それをクローラーに食わせればいい。さらに、そのサーバサイドで出力されたHTMLをブラウザがロードすれば、ブラウザにSPAをロードすることなく初期表示が可能になります。UniversalにはPWA対応SPAの課題であったSEO対策に対応するとともに、Webアプリのロードを不要に初期表示を高速化させる効果があります。

PWAとUniversalの仕組み

PWAとUniversal JavaScriptを組み合わせる場合の挙動について理解するために、単純なコンテンツ表示アプリを想定します。このWebアプリはBlogのように記事を表示するページを提供します。記事のページは、指定されたコンテンツIDを使ってコンテンツAPIから記事のタイトルや本文を取得し、テンプレートに埋め込んで記事が含まれたHTMLをレンダリングします。

まず、アプリケーションがデプロイされた状態から、①初回アクセスがあります。レンダリングサーバは、デプロイされたWebアプリコードをロードし、サーバ側で起動します。WebアプリはコンテンツAPIを叩いて記事を取得し、記事ページをレンダリングします。レンダリングサーバは、その結果を返却します。この際、アクセス元がクローラーであればサーチエンジンにインデクシングされることになります。

これがブラウザの場合、②非同期にService Wokerが動き、Webアプリのコードを取得するリクエストを発行し、ブラウザ内でキャッシュします。この場合、レンダリングサーバは単純にリソースとしてのWebアプリコードを返却するだけです。

そして、③2回目にアプリを起動すると、ブラウザはSerivce Workerにキャッシュされたアプリコードを利用してWebアプリを起動します。WebアプリはコンテンツAPIを叩いて記事を取得し、記事ページをレンダリングします。

こうした作りによるメリットは次の通りです。

  1. 初回アクセスはサーバサイドでレンダリングされた結果を使うため、Webアプリ本体をロードする必要がなく、初期表示が可能
  2. 2回目はSerivce Workerが非同期にキャッシュしておいたWebアプリを起動するため、今回もロードコストをかけることなく初期表示が可能
  3. SEO対策ができたSPAが単一コードで実現

Universalによるサーバサイドレンダリングと、PWAによるクライアントサイドキャッシュを有効に活用することで、単一コードでユーザー体験を向上させているのです。ただし、Universal対応するには制約もあります。当然ですが「クライアントとサーバで同じコードが動く」という点です。わかりやすいのはサーバ側で以下のようなコードは動きません。

  • DOM API全般
  • window、documentといったブラウザに依存するオブジェクト操作
  • IndexedDB、WebStorageといったブラウザ固有の機能

多くのSPAフレームワークでは上記に依存しないテンプレートエンジンが用意されているため問題は起きにくいはずです。ただし、jQueryなどを使うと制約にひっかかることになります。

Angular PWAとAngular Universal

Universalはアイデアとして素晴らしいですが、実現する仕組みは複雑になるため、専用ライブラリを使うことが推奨されます。Graat(グラーツ)ではAngular PWAAngular Universalを活用しています。Angular PWAはAngular CLIに組み込まれており、

ng add @angular/pwa --project projectname

とするだけで、簡単にAngularアプリのService Woker対応ができます。2つ目のAngular Universalはサーバサイドとクライアントサイドの仕組みを提供しています。サーバサイドとしては、Angularアプリをサーバサイドで動かすAngular Express Engineを提供しています(Node.jsとASP.Net Core上で稼働)。一方、クライアントサイドは、サーバサイドでレンダリングされたHTMLからAngular本体への切り替えがスムーズに行うための仕組みが用意されています。

実は、この記事が表示されているサイトもAngular PWAとAngular Universalで作られています。次の記事では、その中身を紹介したいと思います。