ぱろっと・すたじお

技術メモなどをまったりと / my site : http://parrot-studio.com/

React+jQuery+RailsのSPAをサーバサイドレンダリングに移行した件(その1:概要編)

先日、「チェンクロパーティーシミュレーター」(以下「ccps」)をアップデートしまして、
サーバサイドレンダリング(いわゆる「SSR」)に対応しましたヽ(`・ω・´)ノ

ccpts.parrot-studio.com

github.com

過去の経緯はこちらを見ていただきたいのですが・・・

parrot.hatenadiary.jp

・・・すでに「React+jQuery」で動いており、「TypeScript」で書かれたSPAでございます

しかし、SPAであるがゆえに、大きな問題を抱えておりまして、
それは「とにかく初期表示が遅い」ということです(´-ω-)

これを解決するのが「サーバサイドレンダリングSSR)」なのですが、
いろいろ検索してみると、わりと否定的な意見が多くあります

やってみた上で、私もどちらかといえば否定的なのですが、
要は「どこで使うべきものなのか?」が重要だと思うのですね

SSR自体、わりと手間がかかるし、それ用の設計や手法が必要になるものであり、
私もかなり苦労させられたのですが、そこを紐解いていくためにも、
今回はあえて「当たり前のこと」から考えてみます...φ(・ω・`)

なお、かなり長くなる予定なので、概要編・実戦編と分けた上で、
全部で数回になる予定です


<第2回>
parrot.hatenadiary.jp



そもそもサーバサイドレンダリングSSR)とは何か?

冷静に考えてもらいたいのですが、
そもそも「サーバサイドでHTMLを出力し、クライアントのJavaScriptで制御する」というのは、
Webシステムとしてはごく「普通」の話です

にもかかわらず、わざわざ「SSR」なんて仰々しい言葉を定義するわけですから、
「サーバサイドでHTMLを作ること」だけではないわけです

そもそも、現代的なWebシステムにおいて、サーバサイドがAPI的なものを提供し、
クライアントが(旧来に比べるとややリッチな)jsで制御する・・・というレベルの設計は、
すでに一般的といっていいレベルだと(少なくとも私は)思ってます

その「リッチなjs」が単なる「制御」を超えて、「画面を大幅に書き換える」、
つまりは「view」を直にいじるレベルになってくると、
クライアントのview制御と、サーバサイドのview生成で、ロジックが「二重化」されることになります

これを解消するため、「viewは全部クライアントに寄せる」ことをした上で、
「画面の切り替えのレベルまで全部viewの制御に寄せる」、
「サーバサイドは純粋なAPI」という設計方針をとったのが「SPA」ということですね

しかし、「クライアントでviewを作る」ということは、
「クライアントでjsを解釈しない限り、画面が見えない」という問題があります
(いわゆる「ファーストビューが遅い」問題)

サーバでHTMLを作っていれば、少なくともその範囲ではすぐ表示されるのですが、
SPAの場合はjsの通信速度やクライアントのCPU速度など、
様々な要因が絡むので、相対的には「遅い」わけです(´-ω-)

一般的に、SPAの問題としてあげられるのはこのあたりでしょうか

  • ファーストビューが遅い
  • SEO的な問題
    • クローラーが正しくSPAを解釈できるとは限らない
    • OGPような静的なタグ出力はサーバサイドでしかできない

私のccptsの場合は「ツール」なので、編集モードで多少待たされるのはともかく、
共有されたURLから飛んでくる「PT閲覧モード」で、
編集用のコードを解釈する時間で待たされる・・・というのは、本来よろしくありません

同様に、例えばBlogやニュース記事のような、
「ユーザーがURLから飛んできて、ただ見たいだけのサイト」は、
基本的にSPAに向いてません(´・ω・`)

そういったサイトは、普通にサーバサイドでHTMLを出力すればいいわけで、
そもそも「SSR」を検討する以前の問題です

言いかえれば、まずSPAにすべきかの検討があって、
SPAが必要だけど、それでは遅いという場合に、
初めてSSRが検討される
・・・というのが本筋かと

じゃあ、SPAにできればSSRは簡単なのかというと、
そんなことはないわけで、めっちゃコストがかかります

Webで良く見る「SSR不要論」は、
「理想的ではあるけど、移行するコストに見合わない」ということだと思ってます

SSRの問題点はこんな感じかと
(あとで出てきますが、この「SSRの問題点」は、
 「SSRに移行するための段取り」と関連してきます)

  • (SPAとして完成していたとしても)実装コストがかかる
    • 「ブラウザ」で動作することを前提に、暗黙的に書かれた仕様を全て見直す必要がある
      • 例:windowオブジェクトへの参照・暗黙的なイベントへの依存
  • クライアントとサーバサイドの担当範囲があいまいになる
    • クライアントだけが知っていればよかったことを、サーバサイドでも考慮しないといけない
  • (通常のHTML出力に比べて)処理が重い
    • 例えばreact_on_railsの場合、Rubyのプロセスでjsの実行系を走らせないといけない
    • サーバサイド自体がNodeのようなjs実行系で動いている場合のコストは不明

これらのメリットとデメリットをふまえた上で、
今作ろうとしているシステムに対し、「普通のHTML出力+js」がいいのか、
「SPAのSSRがいいのか」を判断が必要になってきます

<参考>

www.publickey1.jp


STEP0:SPAをSSRにするための概要設計

(1) SSRを阻害する要因

今回のSSR移行に関しては、以前から使っていた「React on Rails」の機能を利用しています

github.com

以前は「SPAをRailsに絡めて簡単にデプロイできるツール」的に使っていたのですが、
元々SSRの機能がついてまして、SSRを試すだけならprerenderフラグをtrueにするだけです *1

<%= react_component("HelloWorld", props: @some_props, prerender: true) %>

もちろん、「それだけ」でSSRが動いてくれるなら、
React on Railsを開発している会社のコンサル業は成り立たないわけで、
そんな単純な話ではありません(´・ω・`)

「サーバサイドでjsを動かす」というのは、
Railsの文脈においては「execjs」のことを指します*2
(一般的にはNode.jsのサーバサイドプロセス・・・ですかね)

execjsはサーバサイドでのjs実行系なので、「ブラウザ」の概念がありません
SSRを試して、おそらく最初に目にするエラーは、
「windowオブジェクトがない」というものでしょう

windowと書いてなくても、例えば「location」や「confirm」のように、
暗黙的にwindowオブジェクトを参照しているものはたくさんあります
そういったものは全て、サーバサイドjsでは動作しないことになります

React世代のライブラリはともかく、
旧世代のライブラリはほぼ確実にwindowを触っているはずで、
その代表格はもちろんjQueryということになります

私のccptsにおいて、jQueryは仕様上必須です
しかし、jQueryに依存している限り、実行系がエラーを吐いてしまいます

ここが、今回の設計における最大の肝になります

(2) SSRに使うjsの制約と、その対処

ところで、「React on Railsが実行しているjsファイル」ってどれなんでしょう?
configを見ると、こんな設定項目があります

# config/initializers/react_on_rails.rb から抜粋

ReactOnRails.configure do |config|
  # buildに使うコマンド
  config.build_production_command = 'RAILS_ENV=production bin/webpack'

  # SSRで実行するjs名
  config.server_bundle_js_file = 'ccpts.js'
end

ここで指定している「ccpts.js」がどこにあるかというと、
エラーメッセージから追った結果、「public/packs/ccpts.js」にありますが、
これは「app/javascript」以下のjsをwebpackで固めたファイルになります

React on Railsに限らず、SSRしようとした場合、
webpack等で固めた単一のjsを、クライアントとサーバで共有し、
サーバサイドでも実行する・・・という流れになります

そして・・・ここがポイントなのですが・・・

裏を返せば、「サーバサイドで実行するjsにwindowオブジェクト等は含まれず、
クライアントで実行する際には含まれる」
という状況が作れれば解決するはずなのです

実際、これを試したことでSSRに成功したのですが、
概要を図にまとめるとこうなります

f:id:parrot_studio:20190124110650p:plain
ccptsのJS概要図

解説については次回以降やっていきますが、
私が立てたSSR化の段取りは以下のようになります
(まさに、先ほど挙げた「SSRの問題点」と対応しております)

  1. ブラウザ系オブジェクトの排除(とりあえず実行時エラーを消す)
  2. 「仕様上の正しい動作」になるように修正
  3. インフラの調整

次回は「実践編」として、先ほどの図の解説をしながら、
どのように先ほどの問題を解決したのか・・・を書いていきます...φ(・ω・`)


<続き>

parrot.hatenadiary.jp

*1: しかも、サーバで出力したReactのHTMLと、クライアントのReactが管理するDOMで、いい感じに制御してくれるΣ(・ω・ノ)ノ

*2: 厳密にいえば、execjsはRubyプロセスでjsの実行系を動かすためのインターフェースであり、実際の実行系はインストールしてあるものから選択される