ぱろっと・すたじお

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

ReactOnRailsとSSRとWebpackerを捨ててWebpackに移行した件

久々に「チェンクロパーティーシミュレーター」(以下「ccps」)のお話です

ccpts.parrot-studio.com

github.com

Rails6のRC2がリリースされたので、夏休みの課題的な感じで、
Rails6への移行を進めようと思ったのですが・・・

・・・なぜかReactOnRailsとWebpackerを捨てて、
ピュアなWebpackに移行していたのですΣ(・ω・ノ)ノ

react_on_railsを破棄し、webpackに移行 · parrot-studio/cc-pt-viewer@592a34d · GitHub

正直、短期間にいろいろやり過ぎて、私の中でも整理しきれていないのですが、
夏休みの課題をレポートとしてまとめておくのも必要なことなので、
覚えている範囲でいろいろ書いていきます...φ(・ω・`)


今回言いたいこと
  • サーバサイドレンダリングSSR)はかなり重たいので、他の部分で最適化できないか検討してみた方が良い
  • Railsの「Webpacker」を理解するには「Webpack」の知識が必要で、WebpackがわかるならWebpackerはむしろ冗長なのでは?
今回の経緯

面倒なので、ざっと経緯をまとめておきます...φ(・ω・`)

  • SSRで運用中にアプリレベル監視を入れたところ、頻繁に警告が来るようになった
    • 雑だった箇所を修正したり、攻撃してきたIPを弾いていった結果、監視そのものが重くする要因と判明
    • 5分に1回監視して落ちるようなサイトって相当に問題では?
    • ReactOnRailsでSSRのキャッシュを使おうと思ったら、有料でないと使えない
  • 結果、どこかのタイミングでSSRをやめて設計変更しようと決める
  • その後、Rails6のRC2がリリースされたことを知る
    • いつものように新規でRails6プロジェクトを立ち上げ、コードを移植していた
  • RailsでjsやcssをまとめていたSprocketsが、Rails6だとデフォルトではないことを知る
  • この時点で、完全にWebpackerに移行すると決め、いい機会なのでReactOnRailsを切り捨てる
  • 試行錯誤の結果、Webpackerのみでdevelopmentの動作を確認する
  • stagingでのデプロイを試そうといじっていて、運用のための要件(例:圧縮等)が満たせない・・・というより、Webpackerの情報が少なすぎて破綻する
  • 「Webpacker」ではなく「Webpack」の情報なら山ほどあり、Webpackerを破棄する事例をみて、Webpackerの破棄を決める
  • Webpackのconfigでかなり苦戦したが、結果的に無駄に含まれていたコードを排除することに成功
    • productionデプロイも圧倒的に高速化
    • js/cssも軽量化
    • SSRしてないのに、思ったより表示が速い
  • SSRもWebpackerもいらなかったんや・・・(´-ω-)」 <- イマココ
SSRの限界と結果的に得られたメリット

ccptsはjQueryとReactで構築されているため、
SSRは難しそうに思いますが、「ブラウザが必要とするコード」と、
「静的なHTMLを生成するためのコード」を分離することで、SSRに成功しました(`・ω・´)


これ自体は満足していたのですが、問題はやはりサーバの負荷です
ある時期からさくらVPSの機能で監視をセットしたところ、
監視アクセスの負荷で警告が来るようにΣ(゚Д゚)ガーン

5分に1回程度、TOPにアクセスされる程度で重くなる*1ようでは、
Webサイトとして機能してないですし、
そのためにVPSをバージョンアップするのも何か違います

そもそも、SSR化の記事でも書いたように、SSRは最後の手段であり、
可能であれば最初から適切なファーストビューを組める方がいいのです

ReactOnRailsの切り離し

そもそもReactOnRailsは何をしていたのでしょうか?

  • jsの依存関係解決と統合
  • まとめたjsをRailsのassetsとして扱えるようにする
  • Rubyの文脈で渡したデータを、Reactの初期propsとして渡す
  • まとめたjsからサーバサイドでHTMLを生成する(オプション)

他にも、デプロイ時の「assets:precompile」にフックをかけるとか、
細かいことをやっていますが、大枠ではこんな感じです

しかし、RailsがWebpackerを採用して以降、
Rails側と協調していろいろな機能が整理されていった結果、
「Webpacker+α」くらいの立ち位置になっていました

つまり、クライアントサイドだけを考えるなら大きなメリットはなくて、
SSRまで考える時に楽(になる可能性がある)・・・ということです

で、今回SSRをやめることにしたので、
純粋なWebpackerを使ってもそれほど問題にはなりません

  • jsの依存関係解決と統合 => そもそもWebpackerの仕事
  • まとめたjsをRailsのassetsとして扱えるようにする => Rails側でSprocketsを使わない手法が実装されているので、本質的には不要
  • Rubyの文脈で渡したデータを、Reactの初期propsとして渡す => 初期データの塊をJSONで出力し、Reactをappendする前にparseして渡す

今までReactOnRailsがうまいことやってくれていたWebpackerの仕組みについて、
自分で調べる必要はありましたが、そこまで大きな問題はありませんでした

・・・少なくとも、「ReactOnRailsを取っ払う」という文脈においては(lll゚Д゚)

Webpackerの辛さ

問題が一気に出たのは、何とかdevelopmentで「気持ち悪いけど一応動く」まで到達したので、
stagingで動かしてみよう、言いかえれば、「運用できるレベルで構築しよう」と考えた時です

今まではReactOnRailsがいい感じにコードの圧縮やzip化をやっていてくれていたので、
あまり意識していなかったのですが、Webpackerで自前でやろうとすると、
一気に必要な情報が膨れ上がりましたΣ(゚Д゚;≡;゚д゚)

しかも、「Webpacker」でググっても全然情報が出てこないし、
出てきた情報も「なんでそういう書き方で動くのか?」がわからなくて気持ち悪いわけです

やっと手に入れた情報通りに設定しても動作が想定通りにならず、
これが「新しいWebpackerがリリース前でバグがあるから」なのか、
「新しい書き方をしないとダメ」なのか、全く見えなくなりました(´-ω-)

結論として、この記事の通りです

今日から簡単!Webpacker 完全脱出ガイド - pixiv inside

この記事を読んでめちゃくちゃうなずきまくったので、
Webpackerを捨てる決意をしました( ゚Д゚)y─~~

「Webpack」について理解することで、
「WebpackerのDSL」が何をしようとしていたのかは理解しましたが、
Webpackを理解してからだと、あまりに回りくどいです

アップデートも遅くて、Webpackerが依存しているライブラリのために、
GitHubからセキュリティ警告が来ていたりして、
アップデートが来ないとどうにもならなかったりとか
(Webpackに移行して、各ライブラリを最新にしたらもちろん警告はなくなりました)

Railsが「Webサイトに必要な技術的要素」をフルスタックで容易にしているのは利点ですが、
クライアントサイドについてはRailsと無関係なプラクティスがあふれており、
それをRailsの文脈にわざわざ変換させるのは、実際の業務においても面倒だと思います(´-ω-)


「webpack.config.js」を一から理解する

ということで、純粋な「Webpack」について、一から学んでいきました

webpack 4 入門 - Qiita

webpackのTree Shakingを理解して不要なコードがバンドルされるのを防ぐ - Qiita

Webpackも4になってだいぶ使いやすくなったそうで、
modeパラメータを指定することで、
最低限想定した処理をしてくれるとか、簡単になっています

とはいえ、今回はあくまで「Railsの上で動かす」ことが前提であり、
「index.html」からアクセスさせるわけではないので、
一般的なwebサイトとは少々やり方が異なります

ポイントは以下です

  1. 元のWebpackerにあわせて、「app/javascript/packs」を起点とする
  2. ビルドしたコードを「public/packs」に吐き出す
  3. viewから自分で書いたhelperである「javascript_pack_tag」等を呼び出す
  4. Railsのフロントにあるサーバ(Nginx等)から直に「public/packs」以下を参照させる

1.2.のディレクトリはどこでもいいのですが、引き剥がしたとはいえRailsのプロジェクトなので、
一応Webpackerの作法に合わせて決めてあります

3.が最大のポイントで、Railsが用意していた「javascript_pack_tag」を自分で実装する必要があります

  1. 指定された名前からmanifest.jsonを見て、実体となるpathを取得する
  2. そのpath(あるいはURL)をRailsの文脈で出力する

実際にはこんな感じです(´・ω・)っ

結果的に、このようなタグが出力されます

<script src="/packs/application-d1623c84a0914a0a63d9.js" defer="defer"></script>

Railsのデフォルト設定ではpublic以下に直アクセスさせてくれないので、
Nginx側でこのような設定が必要です*2

location ~ ^/packs/  {
  root /path/to/public;
  gzip_static on;
  expires max;
  add_header Cache-Control public;
}

これ自体は、以前「public/assets」に対して設定したものと全く同じです


その他、気になったこと

「依存するファイルを全て起点から参照させる必要があるのはわかるが、
 CSSまで一緒にされると、jsが解釈されるまでCSSが適用されない」


なんとなくで理解しないWebpackのCSS周辺 - Qiita

GitHub - webpack-contrib/mini-css-extract-plugin: Lightweight CSS extraction plugin


「fontawesome5はwebfontからSVGによる描画に変わったようだけども、
 jsで組み込まれるのでbuildしたファイルが肥大化してやばい」

「React上でiタグによるフォント描画をさせると、
 動的な変更を加えてもにアイコンが固定されて変わらない」

1年後に差がつくFont Awesome5 ~フロントエンド開発(ES6,Webpack4,Babel7)への導入~ - Qiita

GitHub - FortAwesome/react-fontawesome: Font Awesome 5 React component


「productionでもsource-mapが含まれてしまってサイズがでかい」

そもそも、developmentでだけ出力する

const config = {
  // 共通の設定
}

module.exports = (env, argv) => {
  if (argv.mode === 'development') {
    config.devtool = "source-map"; // developmentでだけ

    // webpack-dev-serverの設定とか
  } else if (argv.mode === 'production') {
    // jsとかcssとかzipで圧縮するとか
  }

  return config;
}

この「環境ごとにwebpack.configをいじる」をDSL的にやりたかったのが、
webpackerの「config/webpack」だと思うのですが、
このように書けばいいだけの話なので、やはり冗長だと思います(´-ω-)


「Reactから参照していないが、htmlから参照している画像(例:favicon.ico)が
 Webpackのbuildに含まれない」

最初は起点として設定していたのですが、
要は起点になるjsにimportされていればいいので、雑に追加しました

// appilication.js

import './images/ccpts.png'
import './images/favicon.ico'

これによりmanifest.jsonにも画像が登録されるため、
自前のhelperからいい感じに参照できます

デプロイの修正

今もcapistranoによるデプロイをしており、
以前は「assets:precompile」のタイミングで、
ReactOnRailsがいい感じにbuildをしていてくれました

しかし、もうWebpackerすらも使ってないので、
どうデプロイしたものか悩んだのですが、
要するに「ローカルでbuildして、適切なpathにupload」でいいわけです

# もはや不要
# set :assets_roles, :app
# set :yarn_roles, :app
# set :yarn_flags, '--prefer-offline --silent --no-progress --production'

namespace :deploy do
  namespace :webpack do
    desc 'build packs'
    task :build do
      # 手元でbuildを実行
      run_locally do
        execute "yarn run build:production"
      end
    end

    desc 'upload packs'
    task :upload do
      on roles(:app) do
        within release_path do
          with rails_env: fetch(:rails_env) do
            # リモートにbuildしたファイルをupload
            execute "mkdir #{shared_path}/public/packs/images -p"
  
            files = []
            Dir.glob('public/packs/*').each do |f|
              files << f if File.file?(f)
            end

            images = []
            Dir.glob('public/packs/images/*').each do |f|
              images << f if File.file?(f)
            end

            pack_path = "#{shared_path}/public/packs"
            files.each do |f|
              upload! f, pack_path
            end

            image_path = "#{shared_path}/public/packs/images"
            images.each do |f|
              upload! f, image_path
            end
          end
        end
      end
    end

    desc 'build, upload with webpack'
    task :precompile do
      invoke 'deploy:webpack:build'
      invoke 'deploy:webpack:upload'
    end
  end

  before :migrate, 'webpack:precompile' # migrateの前にwebpack関連を実行
end

今まではサーバでbuildしていたので時間がかかりましたが、
自分のマシンでbuildすれば速いし、
assets関連のタスクも全部削っていいので、デプロイがシンプルになりました

これだとAPサーバにyarnどころかnodeもいらないので、
サーバ構成そのものもシンプルになります(`・ω・´) b


今回のまとめ

これはSSRをやめた時に気づいたことですが、
SSRのためにはサーバサイドで、
画面表示に必要な情報を先に揃える必要がありました

これは当たり前のようですが、以前は画面を表示してから
非同期にデータを取りに行く設計になっていました

これにより、クライアントとサーバサイドの責務を切り離し、
わりと自由にクライアントをいじれるようにしたわけです

しかし、当然ながらこれがクライアントが遅くなっていた原因でもあります

  • データ取得のための通信が数回発生する
  • 取得したデータで画面をレンダリングしなおすので、再描画コストがかかる

SSRをやめても「初期表示に必要な情報を渡す」という設計は維持したため、
クライアントの処理でも以前に比べて格段に高速化されました(`・ω・´) b

(技術的な興味もあったとはいえ)この速度感なら
面倒なSSRにしようとは思わなかったはずで、
一周回ってやっと適切な設計にたどり着いた気がします


そして、Webpackについてですが、
webpack.configについてきっちり理解し、
自動生成されたままだとか、コピペしただけの設定はなくなりました

それぞれが何をしていて、どのタイミングで適用されるのかを理解しているので、
余計な設定はなくなり、全体がスリムで見やすくなりました

その結果、jsやcssにも余計なコードが含まれなくなり、
jsのサイズだけ見ても、1/2以下に減っており、
起動時の待ち時間もだいぶ減りまして、SSRほどではないにしても、十分高速になりました*3

なにより、「Webpackの知識」はRailsと関係なく、
他のアーキテクチャでも応用できる知識であり、
フロントエンド開発フローの構築に役立ちます

ReactOnRailsがなければ、そもそもReactを使おうとも思わなかったので、
非常に感謝はしていますが、やっと補助輪が外れて、
「本来の運用」ができるようになった・・・ということですね(`・ω・´)

*1: 実際には監視の負荷+ちょっと多めのアクセス いずれにせよ、あまり使われていないから問題になってなかった・・・というだけです(´-ω-)

*2: productionのconfigを一箇所書き換えれば可能なのですが、そもそもパフォーマンスのためにそのような設定がデフォルトなので、それに従う方が適切です

*3: Googleの分析だと警告が出まくりますが、そもそも複雑なサイトであるため、ある程度はあきらめてます

async/awaitを投入したら便利だった件(あるいはAWS Lambdaで画像変換しようとしたら罠にはまった件)

今回は完全にメモ書きだし、たいしたことは書いてないのですが、
私自身がこれを書き残さないと後でまたはまりそうなので、
特に読まなくてもOKですΣ(・ω・ノ)ノ

API Gateway + Lambdaの画像変換ではまった話

本当に今さらなんですが、社内インフラで画像変換させるのが重すぎて、
他のサービスにも影響しそうだし、それ用のサーバ立てるのもめんどいってことで、
AWSに流しちゃえって話になったわけです

今だと「Lambda@Edge」の話ばかり出てくるのですが、
これは「CDNからLambdaをキック(して画像を変換する)」って仕組みなので、
社内と連携するなら「API Gateway+Lambda」という旧来の構成がいいかなと

構築したのは、API GatewayAPIとしてパラメータを受け取り、
Lambdaにeventとしてパラメータを流し、レスポンスをAPIとして返し、
エラーだったら500エラーで返す・・・みたいな標準的なやつです

これ自体はググれば山ほどやり方が出てくるので、
今回はどうでもいいのですが、
問題はLambda環境のimagemagickです

サンプルコードからしてJSというかES2016(=ES7)なので、
当然のようにES2016でコードを書いていて、手元の環境で動くことも確認したのに、
なぜかLambdaにデプロイすると動かないという・・・Σ(・ω・ノ)ノ

変換の手前までは動くことはログで確認できたので、
どうもimagemagick自体が動かないんじゃないか・・・と思ったら、
デフォルトのNode.js10系環境にはimagemagickが入ってなかったようで*1

LambdaのNode.jsを8.xにしたらあっさり動いたのですが、
これに気づくまで半日かかったのです
今さらこの仕組みを構築するって方は気をつけていただきたいщ(゚Д゚щ)


なお、API+Lambdaの環境だと、APIタイムアウトが29秒なので、
Lambdaの処理が29秒を超えると、応答がタイムアウトしてしまいます

現状だとAPI Gateway側のタイムアウトをこれ以上伸ばせないので、
結局、S3にアップロードされたことをトリガーにする仕組みも書いたのですが、
こちらはLambda側にテンプレートが用意されているのです

で、その環境のNode.jsが8.x指定なんですよね・・・
検証手順が逆ならば時間を無駄にしなかったのに・・・(´-ω-)

async/awaitが便利って話

ある意味ここまで前置きですΣ(・ω・ノ)ノ

ES6自体は以前からccptsで使っているわけですが・・・

github.com

ccpts.parrot-studio.com

・・・ES2016(かつてES7と呼ばれていたもの)から導入された、
「async/await」を使う機会がありませんでした

「イベントを数珠つなぎに処理する」のではなく、
「各componentがBusからイベントを勝手に受け取って動く」という疎結合な作りなので、
「連続した非同期処理を書く」ということが、あまりなかったのです*2

しかし、今回の画像変換は以下の3つの非同期処理をつなぐ必要があります

  • S3から画像をダウンロードする
  • 画像を加工する
  • 画像をS3にアップロードする

そもそも、Lambdaのテンプレートに、ご丁寧にも「async」が宣言されており、
これはもう使うしかないなと(`・ω・´)

ということで、「async/await」について細かく調べてみたわけですが、
新しいパラダイムを持ち込んだ難しい話ではなく、
「Promiseを使った非同期処理をいい感じに書ける仕様」でしかありません

もちろん、「いい感じに書けること」が最大のポイントでして、
そもそもPromise自体、「callback地獄をいい感じにしたい」から導入されたものですよね

qiita.com

ぶっちゃけ、これを読んでもらえれば、
もう私が書くことはないくらいなのですが、
Promiseの仕組み自体はjQuery全盛の時代から実装されています

  • 非同期処理でちゃんと正常系と異常系を処理したい
  • 非同期処理を連鎖させたい
  • 連鎖の途中でエラーになった場合に特別な制御をしたい

この要件を満たそうと、functionにfunctionを複数渡すことをネストさせた結果、
ぱっと見てわけがわからないコードができあがりました(lll゚Д゚)

それを改善したのがPromiseではあったのですが、
「つーかPromiseってなに? resolveとかrejectとかワカンネ」という、
概念としてやや複雑なものではありました

しかも、結局のところthenの中で別なfunctionを呼んでつなぐ構造は変わっておらず、
それぞれの処理を独立して「認識」できるかというと、結構面倒だったわけです

それが、「await」を使うだけでここまで単純化されます

try{
  // S3からダウンロード
  const image = await download(bucket, origin);
  // 画像変換
  const resized = await resize(image, ext, width, height);
  // S3に書き戻す
  const result = await upload(bucket, resized, image.ContentType, output);
} catch(e) {
  // エラーレスポンスを返す
}

一見、同期的なコードの羅列に見えますが、
裏ではちゃんと非同期的なcallbackで動いてブロックしないようになっており、
まさに「いい感じに書ける」仕様でございます( ゚д゚)o彡゚

要は「Promiseを返すと、resolveかrejectされるまで待って、
resolveした値を返し、rejectされたら例外をあげる」というだけの仕組みです
なので、function側はPromiseを返す仕組みにさえしておけば「いい感じ」になります

しかしですね・・・これだけだと
「そもそもPromiseがわかんないんだよぉぉぉぉっ」って問題を解決できません
そこで出てくるのが「async」です

「async」を宣言したfunctionは、暗黙的にPromiseに変換される・・・と書くと面倒そうですが、
要するにasyncを宣言して「普通に」書けばawaitできる、というだけです

// asyncをつけると、Promiseにラップしてくれる
async function resize(image) {

  // なんかresizeする処理

  if (err) {
    throw err; // 例外はrejectされる
  } else {
    return resizedImage; // 通常のreturnはresolveされる
  }
}

// resizeが完了するまで待って、終わったら結果がresizedに代入され、エラーなら例外
const resized = await resize(image);

Primiseの概念がわからなかったとしても、
「asyncつきでなんか非同期処理を書けば、いい感じにawaitできる」と覚えておけば、
それだけで直感的なコードになるわけです(`・ω・´) b

「直感的である」「わかりやすい」ということは、
「間違いを見つけやすくなる(ので品質が上がる)」ということでもあります
なので、積極的に採用していきたいところです

強いていえば、ES2016が動く環境か、
トランスパイラ経由のデプロイが整備された環境でしか使えないので、
サーバサイドエンジニアとしては、そこをどうにか頑張っていきたいところですね(`・ω・´)

おまけ:API GatewayとS3、どちらがトリガーとして最適か?

S3の方がトリガーとしてシンプルだし、Lambda@Edgeもその延長線なのだから、
そっちが本命・・・というのは事実だと思います

ただ、「画像をアップロードした」がトリガーだと、
「どうやって」とか「どこに」という変換の付帯情報が渡せないわけです(´-ω-)

変換処理ごとにLambdaを立てるってのが、
マイクロアーキテクチャ的には適切なのですが、
細かくたくさんの環境を作ると、管理できなくなると思うわけです

その点、APIであれば細かなパラメータを外部からコントロールでき、
Lambdaに情報を引き渡すことができるので、
汎用的に使うのには楽だと思います(`・ω・´)

一方で、わざわざhttpで通信するオーバーヘッドはあるし、
なにより(現時点では)タイムアウトの問題もあるので、
「汎用的な画像変換API」自体が適切か・・・というと、難しいところですね

結局のところ、どういうフローの中でどう使うか・・・なので、
「Lambdaを使えば画像変換が簡単」といっても、
考えることはたくさんありますね(´-ω-)

*1: サンプルコードにあるgmモジュールを、わざわざ手動でuploadしないといけないから、おかしいな・・・と思ってはいたのですが・・・(´-ω-)

*2: 詳しくはFRPについて書いた記事にて(´・ω・)っ https://parrot.hatenadiary.jp/entry/2015/11/29/175113

KotlinでいつものBrainF**kインタプリタを書いてみた件(+学んだ本が良かった件)

以前から話には聞いていたものの、
最近「ことりんはいいぞ・・・!」という話を聞く機会があったので、
試しに本を読んでみたわけです

Kotlinプログラミング

Kotlinプログラミング

この本がKotlin抜きに非常に良い本でして、
Kotlin自体もいい感じだとわかったので、
例によってBrainF**kインタプリタを書いてみました(´・ω・)っ

gist.github.com

これで「現場からは以上です」って感じなのですが、
以下は自分用のメモであり蛇足です...φ(・ω・`)


「今風の言語」とは何か?

1. 「人間」にとって都合の良い言語

仕様を読んでみて、Kotlinは非常に「今風の言語」だと思ったのですが、
何をもって「今風」なのか・・・と考えてみたわけです

そもそも、一番CPUに近い言語は「アセンブラ」であり、
それをもうちょっと人が書きやすくしたのが「C」、
そこにオブジェクト指向パラダイムを付加したのが「C++」や「Java」でした

「プログラムとは、本質的にメモリを操作する仕組みに過ぎない」というのは、
こちらの本にも書かれている本質的な話でして・・・

・・・Javaも極論を言えば
mallocとポインタを言いかえただけ」と見なすことが可能ですΣ(・ω・ノ)ノ

そういったCPUというかアセンブラから派生した言語とはまた別に、
「人にとって都合の良いモデル・処理系」を提唱したのが「Lisp」でして、
その影響を受けたJavaScript*1Rubyは非常に柔軟です

しかし、「人にとって都合がいい」は「CPUにとっては重い」ってことなので、
Rubyも長いこと「遅くて使えない」と言われていたのが、
CPUの高速化によって価値が再認識されたわけです*2

「わかりやすく言いかえる」というのが「人間」にとっては重要で、
オブジェクト指向」も本来的には「現実世界をモデル化する概念」ですし、
「関数型」も「数学の概念のモデル化」にすぎません

まあ、当時の関数型言語ブームは少々アカデミックに走りすぎて、
「なんかよくわからないけど難しい」になってしまっていたのはありますが(´-ω-)

今ではオブジェクト指向とか関数型の概念は、
もはやそのような単語が言語の文脈に出てこない程度には、
「当たり前のもの」として扱われるようになっております

2. 「型」の取り扱いと「設計思想」

さて、RubyJavaも「オブジェクト指向パラダイムを含んだ言語」ですが、
決定的に違うのが「型」の扱い方です

Javaは変数に型を要求するのが当たり前*3であり、
Rubyは「そのメソッドが呼べればOK」というゆるい縛りしかありません
(いわゆる「ダックタイピング」)

Rubyはその表現力ゆえに「Rails」という「実装された設計思想」を生み出し、
現在のWebフレームワークはほぼRailsに影響を受けているわけです

一方で、Webが肥大化するにつれ、
モノリシックなフレームワークで抱えられる規模を超えると、
「自由度が高いこと」は必ずしも利点ではなくなってきます

もちろん、先日書いたようにアーキテクチャ設計でカバーできる場合もありますが・・・

parrot.hatenadiary.jp

・・・ここまでコストを割けるのはかなり限られたケースでしょう

そもそも、どんな「できるエンジニア」であっても、
「人が認識できる規模」にはどうしても限界があり、
それを超えれば確実にミスを生みます

だからこそ、「設計上の制約」を「コードレベルで表現する」のが必要で、
その手段の一つが「型」と言えます

「じゃあJavaにすればいい」というのはよくわかるし、
実際に業務系システムではまだ圧倒的なシェアがあるのですが、
どうしても他の言語に比べて「お堅いので書きづらい」のはあります(´-ω-)

結局は「いちいち型を書く」のが面倒なのであり、もっといえば
「人が見ればわかることをいちいち書かないといけないのが冗長」ということでしょう

String hello = "Hello World!";

要は、「helloに文字列を代入してるんだから、
helloがStringなのわかるやん? わからんの(´・ω・)?」ということで、
いわゆる「型推論」ができないのか・・・ってことです

「人間にわかること」と「CPUにわかること」には乖離がありますが、
CPUの進化により、「現実的な時間」で推論が可能になったため、
最近の言語はだいたい型推論を備えています

最近のJavaにも一応備わっているらしいですが、
「関数型っぽい型=高階関数」までは扱えない・・・か、
扱いづらいか、対応がまだ不十分かのようです(´・ω・`)

(Java8で改善されたことは知っているものの、触れる機会がなかったので、
 このあたりの私の理解はあいまいです)

そこで、「JVMで動く関数型言語」として「Scala」が出てきたし、
私も一時期は興味があったわけですが・・・

やはり、先ほども書いたように、HaskellScalaのような言語は、
「純粋な関数型言語」を「アカデミックな思想」で目指しており、
「普通の人には難しい(と見える)」のが問題なのではないかと

Rubyが広まったのはその柔軟性もありますが、
やはり「プログラムを楽しく書きたい」という「思想」の面が強いわけです
良くも悪くも、「現実的な妥協」が入っている

Kotlinは「純粋な関数型言語」ではありませんが、
高階関数」や「再代入不可」のような、
Rubyと同程度以上の関数型言語的なパラダイムを備えています

その上でKotlinの「思想」は「よりベターなJava」であり、
非常に「Javaとの連携」を強く意識しています

Javaのclassとの暗黙的相互運用な可能な点は、
まさに「現場が望んだのだからそうあるべき」という思想を感じます(`・ω・´) b

結局のところ、「今風の言語」の個人的な定義はこうなるのかなと

  • 「思想」が感じられ、「現実的なバランス」に落とし込んでいる
  • 関数型のパラダイムを「現実的に便利な部分だけ」引き継いでいる
  • 過去に存在した言語との相互運用性を意識している

その意味で、JavaScriptに対する「TypeScript」と、
Javaに対する「Kotlin」は同じなんだろうな・・・と*4

考えてみると、TypeScriptもKotlinも(ついでに C# *5 も)「企業」が開発した言語です
それだけ、「現場の声」が反映されていたり、
「現場で使われるIDEとの連携」を意識している・・・ということですね

3. Rubyエンジニアから見たKotlin

Rubyのエンジニアからすると、
Kotlinは「Rubyっぽくて書きやすいJava」ですが、
明確な違いがあります

それは「変更に対してオープンかクローズか」という違いです

Rubyにおいて、あらゆるものは上書き可能であり、
それゆえに「黒魔術」とも呼ばれる柔軟なメタプログラミングが可能です
これを利用してgemにモンキーパッチをあてる・・・というのもよくあります

言いかえると、「誰かが勝手にまずいことをやってしまうことを防げない」ので、
ある人が良かれと思って変えたクラスの変更が、
他に波及して壊れる・・・ということもあります(´-ω-)

モンキーパッチにしても、当時はわかっていてやったとしても、
あとになると忘れてしまい、gemが更新できず、
右往左往する・・・ってことがよくあります
(ゆえに、私はgemやフレームワーク自体にパッチをあてるのは否定的です)

それはプログラマの質とかチームの問題である・・・という「思想」なのですが、
大規模なシステムでそういった「やってほしくないこと」を防ぐのは、
「現実的に考えると」難しいです

その点、Kotlinは「クローズ」がデフォルトで、
「必要な時に明示する」という「思想」です

クラスだけでなく、メソッド単位で宣言が必要なのは「面倒」ですが、
「面倒だから面倒ではない手段を探す」ように誘導しているのはあります
(これはRubyの設計思想も同じ)

また、「良くないことをしている場所」をあとから探すのが容易になるのも利点です
Javaでもアノテーションとかありますが、
「言語仕様で強制」はできませんからね(´-ω-)

だからRubyよりKotlinが優れている・・・ということではなく、
あくまで「適用範囲を良く考えよう」ということです

一般的なWebの文脈においてはRubyが楽だけど、
業務系システムですでにJVMの実績があればKotlinだよね・・・みたいな...φ(・ω・`)

冒頭の本は何がいいのか?

先ほどもちょっと挙げたこちらの本を、私は以前から絶賛しております

parrot.hatenadiary.jp

www.slideshare.net

この本のすばらしい点は、「とにかく実装を進めていって、
必要になったらあたらしい概念を導入する」というのを徹底している点です
なにしろ、「変数」が出てくるのが後半ってレベルです

同様に、先ほどのKotlinの本も、
「Kotlinの機能を必要になったら紹介する(という体で書かれている)」のが重要で、
「この機能はどういう時に使うと適切なのか?」が明確なのです(`・ω・´) b

Kotlinプログラミング

Kotlinプログラミング

なにしろ、「クラス」が出てくるのが後半ってレベルでして、
高階関数をどのタイミングで使えばいいのか?」もわかるようになってます

この本でも一貫して貫かれていたのが「ベターJavaとしてのKotlin」であり、
その意味ではJavaを知らない人には少々難しくなりそうにも思えます

その点、題材を「ゲーム」にすることで敷居を下げており、
最後まで興味を持って読みやすく工夫されてます

正直、この本がなければ、今回の記事は書かなかったとというか、
インタプリタだけ書いて満足したと思います

訳本ではありますが、特に読みづらいところもなく、
コードの間違いが気になる以外は、全体としてよくできた本だと思います
Kotlinに興味がある方だけでなく、プログラム初心者にもお勧めです(´・ω・)っ

*1: 「ブラウザで動くLisp」を実装するつもりが、マーケティングの都合でJavaっぽくなってしまった・・・というのは有名な話ですね(´-ω-) でも、「関数を値として扱える」という本質を譲らなかったおかげで、柔軟性を備えているわけです(`・ω・´) b

*2: JavaScriptだって、初期には原始的なDOMの操作にしか使われてなかったのが、ブラウザの高速化により、今ではゲームすら動かせるわけですよね

*3: 最近は緩和されたようにも思いますが、それは裏に隠したってことであり、基本的には「型があるべき」という「設計思想」だと思います

*4: Rubyも今では独立した言語ですが、かつては「ベターなPerl」を意識していた時代があり、その名残が仕様の随所に見られます・・・が、だいぶ減りましたね

*5: C++が辛いから、もうちょっと書きやすくしつつ、今風のパラダイムを積極的に取り込んだ言語・・・ですよね?

これからの中規模分割型アーキテクチャを考えよう

さんざん言及されている資料であり、
これさえ読んでもらえれば、これ以降の文章は不要ですΣ(・ω・ノ)ノ

speakerdeck.com

個人的に、モノリシックアーキテクチャと、マイクロアーキテクチャも、
どちらも「それだけでは辛い」ものであり、
どこかでバランスを取らないといけない・・・と、考えていました

それに対する明確な回答がこの資料であり、
少なくとも現時点においては最適解だと思っているので、
これ以降はただの自分用メモです...φ(・ω・`)


Case1:業務系CMSAPI

私が最初に「API状のもの」を作った時*1アーキテクチャがこちら*2です(´・ω・)っ

f:id:parrot_studio:20190403111453j:plain


当時、私は関数型言語に興味があったので、
アーキテクチャの設計そのものにも関数型的概念を突っ込んでいました

www.slideshare.net

API層が「副作用がある(Store)」と「副作用がない(Logic)」に明確に分かれており、
Perlで表現されたViewがLogicのAPIのみを触るという、
「ちゃんと分離されている感のある」のが非常に良かったと思います

問題は、Logic層がViewに強く依存しているところです

そもそも、Perlで書く量を限界まで減らそうとした結果、
Logicに業務ロジックのほとんどが移動した・・・というのが真相なので、
Logicにエラー文言のようなViewに関わる情報まで含んでいるのが問題です(´-ω-)

とはいえ、Store層はそこそこリソース単位のAPIとして成立しており、
Logic層も今回の機能に特化したAPI群である・・・と考えると、
(Viewの情報さえ分離できれば)構造そのものは不適切でもなかったと思います

Case2:スマホゲーのAPI

急にシステムが飛びますが、次に設計したのがスマホゲーのサーバですΣ(・ω・ノ)ノ

スマホゲーももちろんWebシステムではあるのですが、
通常のWebシステムに比べて、とがった部分が多々あります

  • セキュリティレベルを相当高くする必要がある(例:通信データ自体を暗号化)
  • 応答を少しでも早くしないといけない
  • ユーザーデータが莫大で、単一スキーマに存在することが必須(意味で分離できない)
  • 業務ロジック(=ゲームルール)が複雑で、多数のリソースを同一トランザクションで処理する必要がある
  • 通信のリトライを雑にすることが許されない
  • etc...

「ま、ここはいっか」って妥協が許されないのがゲームサーバです(lll゚Д゚)
(ユーザーが不正を働くモチベーションが非常に高いため)

にもかかわらず、「ゲームの売り上げ」と「サーバの精度」はあまり相関がなく、
結局のところ別な要因で決まるのも辛いところで、だからこそサーバエンジニアは、
企画やクライアントエンジニアが動きやすい環境を作るのが仕事になります

そんな当時のアーキテクチャは、こんな感じでした(´・ω・)っ

f:id:parrot_studio:20190403111503j:plain


莫大なユーザーデータが単一のスキーマに存在するのが必須なのですが、
その代わりユーザー(のID)で水平分割はしやすいです
(その代わり、「ユーザーをまたいだ処理」*3で地獄を見ます)

また、ゲームロジックが複雑で、処理が多岐に渡るため、
そのロジックをどこに置くかが問題で、当時はmodelに置いていました
その結果、modelが複雑で面倒な構造になってました(lll゚Д゚)

また、管理画面も同一のソースで管理していたのですが、
これは管理画面からの操作でも、ユーザーデータのバリデーション*4などは同一のため、
開発効率を考えると仕方なかった部分はあります

今考えるともうちょっとやり方はあったと思いますが、
小人数で面倒なシステム開発を進めるためにも、
ある程度モノリシックなシステムになってしまうのも仕方ないのかな・・・と

あと、これは当時の会社の方針ですが、
どうしても「アーキテクチャの設計」ができるエンジニアは限られるため、
ある程度プロジェクトが軌道に乗った段階で、別の新規プロジェクトに回されました*5

そのため、「最初から最後まで」見ていたシステムはなくて、
こはちょっと残念です・・・が、
私のようなタイプが「運用フェーズ」に向いてないのも事実なので、合理的です

Case3:モノリシックなAPIとView

そして現状いじっている業務システムです
Case1とCase3の中間を取ったようなアーキテクチャになってます

f:id:parrot_studio:20190403111514j:plain

以前に比べるとAPIRailsに関する理解も深まったので、
かなりリソース志向のAPIになっており、
さまざまなViewでそれを使う設計になってます

いわば、リソースAPIがいろいろなシステムのデータ管理層になっており、
View層がロジカルな部分を担当し、画面を作る・・・という形です

最初はReadがメインだったので問題なかったのですが、
Writeが発生したあたりでだんだん粗が見えてきて、
Viewがどんどん膨らんでしまいました(lll゚Д゚)

また、API側にも業務ロジックが一部入り込んでしまったりと、
Case1のLogicにあたる「業務ロジックを管理する層」を明確にしなかったのが失敗です

今にして思えば、私はCase1を「いまいち」だと思っていたのですが、
その理由を明確に分析してなかったのかな・・・と
実はCase1を整理したバージョンが正解に近かったのですが・・・(´・ω・`)

未来の話:これからの理想的アーキテクチャ

冒頭のスライドの内容を抜粋すると、こうなります

  • Railsはリソース志向に特化したフレームワークである
    • 結果的に小規模のリソース群に対しての開発効率がダントツである
  • しかし、業務ロジック層を無理矢理入れようとした結果、システムが複雑化する
    • 先の例ならmodelがファットになる
    • 最近の例だとService層を導入する
  • Railsの良さを生かしつつ、全体のアーキテクチャでカバーできないのか?

これに加えて、最近のマイクロアーキテクチャが、
「細かすぎてわからなくなってしまう」という問題を抱えているのもあります

「巨大なシステム」も「数が多すぎるシステム」も、
結局のところ「人間の認識限界を超える」のが問題なのです
設計の肝は「人間の認識できる規模にインターフェースを整理すること」ですからね

うまく多層化されたシステムや、うまく構造化されたコードは、
ある処理を考える時に、「その先」のことを考えず、
「今見ている処理」を小さく考えることができます

関数型言語(的な設計)の肝はそこにあるのですが、
本質的な話ばかりが先行した結果、
非常に扱いづらい印象になってしまったのがとても残念です(´-ω-)

先のスライドをふまえつつ、私が理解した範疇で、
もし「次」を考えるとすると、こうなるのかなと

f:id:parrot_studio:20190403111531j:plain

  • View-Logic(Service)-Resource-Dataに層を分ける
    • View層は「表現」で分割
    • Logic(Service)層は「業務」で分割
    • Data層は「トランザクション」で分割
    • Resource層はData層と1対1で、Logic層に対するAPIを提供

Data-Resouceの層を組むのにRailsは便利で、
その範疇であればRailsが破綻しないのであれば、
こういうやり方になるのかな・・・と...φ(・ω・`)

本来、全てのデータ処理に対してトランザクションを張りたいところですが、
それでは(Case3のような)巨大なスキーマができあがり、
管理できなくなるので、ある程度整合性をあきらめて分割する部分も出てくるのかなと*6

まとめ

個人的に「モノリシックなアーキテクチャ」も、「マイクロアーキテクチャ」も、
どちらもどこかで破綻すると思っていて、
「ちょうどいい落としどころ」がどこかにあるだろうな・・・と思ってました

その指針を明確に示しているのが冒頭のスライドであり、
非常に価値があると思います
ぜひ皆さんも読んでくださいね(`・ω・´)

*1: 厳密にはその手前があるのですが、ここに残すような精度のものではないし、結局リリースされていない=運用されてないので、価値はありませんΣ(・ω・ノ)ノ

*2: どこかで似たような図を書いて発表した記憶があるのですが、プライベートな勉強会で資料をUPしなかった気がするので、作り直しました(´-ω-)

*3: 例:フレンド申請・レイド戦・マルチプレイ

*4: しかもめっちゃ複雑(lll゚Д゚) 「お金を増やす」だけでも多数のバリデーションが必要ですからね・・・

*5: 別な人に開発を引き継ぐのが前提だったため、「できるだけわかりやすい、Railsの標準的な設計」を心がけたので、ここでRailsに対する習熟度が上がったと思ってます

*6: もちろん、Case2のようなゲームサーバの場合、ユーザーデータは単一トランザクションでないとダメなので、このような設計は採りづらいと思います。業務系寄りの話ですね

React+jQuery+RailsのSPAをサーバサイドレンダリングに移行した件(その3:設計変更編)

2回に渡って書いてきたSSR化のお話も、今回でラストです(`・ω・´)


<前回>
parrot.hatenadiary.jp

parrot.hatenadiary.jp

<サイト>
ccpts.parrot-studio.com

<修正したコード>
github.com


正直、前回の話で「SSRの設計における一番の肝」は書いているのですが、
今日の件もそれはそれで重要なポイントではあるので、
頑張って書いていきます...φ(・ω・`)

前回の話が「SSRに関するわりと一般的な話」だったのに比べ、
今回の話は「ccptsの仕様やアーキテクチャに依存する話」が多いので、
一つの参考事例として捉えていただければ

もう一度段取りを復習しておくと、こんな感じでした

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

今回は残りのSTEP2とSTEP3のお話です

STEP2:SSRを前提にした設計の変更

STEP1の時点で「とりあえずの表示」はできているのですが、
当然、アプリの仕様として問題のある箇所が山ほどあります

仕様に依存する部分だったり、以前の手抜きの修正だったりするので、
あまり一般的ではないかもしれませんが、一つずつやったことを見ていきます

(1) Cookie周りの設計変更

前回は「parseした塊を丸ごと渡す」みたいな雑な実装をしましたが、
もちろんそれではダメ・・・ってことはないですが、
「最初の描画を全てSSR化する」という文脈ではよろしくありません

Cookieが管理しているデータをparseし、
それに対応するデータを取得するところまでサーバでやる必要があるからです


<今まで>

  1. クライアントの初期処理でCookieをparse
  2. 必要なデータをサーバAPIに問い合わせ
  3. 問い合わせのあったクエリに対するデータを取得
  4. APIの応答としてリストを返す
  5. クライアントの操作によりCookieを更新

<新しいやり方>

  1. サーバ側でCookieをparse
  2. Cookieに含まれる情報に対応するデータを取得
  3. Reactの初期値としてリストを渡す
  4. クライアントの操作によりCookieを更新


後から操作によって動的に変わる部分は今まで通りのやり方なのですが、
最初にレンダリングする情報だけ、サーバのデータ処理を先行した形です

ただまあ、ここに関しては、本質的に大きな設計変更で
「クライアントだけが知っていれば良かった情報を、サーバも知る必要がある」、
言いかえれば、「サーバ・クライアント間の線引きが崩れる」って話なんですよね・・・

そもそも、「サーバはAPIを提供し、クライアントで主要なロジックを組む」という設計で、
Cookieが管理する情報はまさに「クライアントだけが知っている情報」だったのです*1
(例:「お気に入り」「PT構成保存」等)

本当はうまく疎な関係にできればいいと思うし、
やり方はあると思うのですが、
今回の目的である「完全SSR化」を優先しました

(2) イベントの見直し(componentDidMountの排除)

jQuery.ready()」の時代からコードを積み足してきたのもあって、
componentの初期処理がcomponentDidMountに書かれていたケースが多数ありました
(例:componentDidMountの中で初期データをBacon.Busに流す)

これはcomponent同士を疎にするためには都合が良かったのですが、
サーバサイドではcomponentDidMountを初めとした「イベント」は動作しません
つまり、このままでは初期化処理が走りません(´-ω-)

要は、「全てをSSR化する」ためには、
「最初に表示したいデータ」は全てcomponentのpropsに渡すしかないわけです
(個人的には、ベストではないけど、目的のためにはベターくらいの感じで)

  constructor(props: AppViewProps) {
    super(props)

    // ...

    // constructorでサーバから受け取ったqueryStringをmodelに変換
    // queryStringもブラウザの概念なので、サーバから明示的にクライアントに渡す必要あり
    this.query = Query.parse(this.props.queryString)

    // ...
  }

  private renderConditionView(): JSX.Element | null {
    if (!this.state.showConditionArea) {
      return null
    }

    return (
      // クエリmodelをフォームに渡して描画
      // 以前だと Bacon.Bus を経由してデータを送っていたので、propsで渡す必要がなかった
      <ConditionView
        originTitle={this.props.originTitle}
        query={this.query}
        switchMainMode={this.switchMainMode.bind(this)}
      />
    )
  }

まあ、初期表示以降は今まで通り、
Bacon.Bus経由でストリームとしてデータを流しているので、
純粋すぎた部分と現実的な部分で落としどころを見つけられたのかな・・・としておきましょう

(3) 状態管理の手抜きを改善

このあたりまでくると、SSRに関係ない話なのですが・・・

jQuery.ready()」の時代からコードを積み足してきたのもあって、
あらかじめ全ての構造を描画しておいて、
初期処理や何らかのイベントでhide/showする・・・という設計がかなり残ってました

後からの制御はともかく、
初期処理(=componentDidMount)で表示を管理するとSSRでおかしくなるので、
厳密にprops/stateに依存する管理に全て移行させました

これはもう、React化した時のやり残しというか手抜きのフォローに近いのですが、
雑とはいえ問題なく動いていたものであっても、
SSR化する際には厳密に書かないとダメってことです(´-ω-)

結果的に、componentDidMount等に残ったコードは、
(前回の)Browserに依存するコードだけになりました(見た目とか、イベントハンドラのセットとか)
つまり、ブラウザでしか動作しなくても問題ない・・・ということです

export default abstract class ArcanaRenderer<T> extends React.Component<T> {

  protected div: HTMLDivElement | null = null

  public componentDidMount(): void {
    if (this.div) {
      Browser.hide(this.div)
      Browser.fadeIn(this.div)
    }
  }

  public componentWillUpdate(): void {
    // 更新される時はいったん消す
    if (this.div) {
      Browser.hide(this.div)
    }
  }

  public componentDidUpdate(): void {
    // 再マウントされる時にフェードイン
    if (this.div) {
      Browser.fadeIn(this.div)
    }
  }

// ...

}

一応フォローしておくと、STEP1の時の判断と同じで、
以前は「今回の目的はとにかくReactで動作するようにすることで、
細かい設計の粗は気にしない」というポリシーだったのです

「厳密にやる」ってのは相応のコストがかかる話なので、
「目的に合わせて現実的なコストに落とし込む」ってのも、
「運用」していくという観点では必要なのかなと

(4) 完全なレスポンシブ化

これもある意味手抜きを直しただけなのですが・・・

今まで、初期処理としてwindowサイズを計算し、
一定ラインを超えたら「携帯モード」で表示する、ってのをやってました
windowsサイズを元にフラグで管理)

当然、これはwindowオブジェクトに触れないSSRでは通用しないのですが、
とりあえずPC/タブレット用に描画して、
ブラウザ側の初期処理で修正すれば・・・と思ったら、うまくいかずΣ(゚Д゚)ガーン

SSRで吐き出したDOMと、後から読み込んだReact(on Rails)が処理するDOMで、
差分があるとwarningが出るのですが、携帯サイズだと大量のwarningが出るし、
そもそも挙動もおかしくなるのです

結局、正しい形でBootStrapのレスポンシブ機能を適用しました

  private renderMember(): JSX.Element {
    const m = this.props.member

    // 両方出力して、bootstrapのhiddenクラスに制御を任せる
    if (!m) {
      return (
        <div>
          <div className="none hidden-sm hidden-md hidden-lg summary-size arcana" />
          <div className="none hidden-xs full-size arcana" />
        </div>
      )
    }

  // ...
  }

もっと力技のところもありますが、基本的な方針はこうなってます

継ぎ足されたコードの弊害といってみればそれまでですが、
「ブラウザという環境」を、いかに暗黙的に、無意識に、
前提に置いていたか・・・ということでもあります(´-ω-)

STEP3:Webサーバのチューニング

ここまででコードレベルではだいたいリリース水準に達したので、
問題ないことを確認するため、staging環境にデプロイしたのですが、
やはりいろいろ問題が出ましたΣ(゚Д゚)ガーン

(1) Nginxのキャッシュ

一回目の表示は問題ないのに、二回目以降にエラーが出る問題が発生し、
エラーメッセージでググったところ、こんな話が

osa.hatenablog.com

SSRで巨大なHTMLをproxy先が吐き出したため、
キャッシュが使われるようになったが、
パーミッションに問題があったということです

つまり、staging環境で今までディスクキャッシュは使ってなかったのに、
SSR化してHTMLが大きくなったら、キャッシュに逃がす必要が出た、ということです

本番のパーミッションは特に問題なかったとはいえ、
そもそもストレージへの書き込みが発生する時点でまずいので、
キャッシュを大きめに設定しなおしました

(2) CPUの負荷(未解決・先送り)

前項の問題がもう出ないことを確認するのと、
ちょうど業務でパフォーマンス的な問題が出たりってのがあったので、
ついでにstaging環境でベンチマークをとってみました

すると、RubyのプロセスのCPU使用量がすごい勢いで上がっていき、
何度か実行していると、レスポンスが大幅に遅延したり、
詰まってエラーになってしまうことも(lll゚Д゚)

かなり無茶な叩き方をしたことは事実ですが、
思ったより「Rubyプロセスの中でexecjsの処理を実行する」のが重いようです

こればっかりは仕組みの問題であり、
そもそも最初からRubyでviewを作れば問題ないわけですが、
今回はSSR化自体が目的で、しかもサイトの訪問は皆無なので、気にしないことに

ある程度アクセスが多いようならば、
viewレベルのキャッシュを入れていく必要があると思いますが、
最終的にexecjsにHTML出力処理を投げる関係で、Railsのキャッシュが生かしづらい構造です

まさに「作って運用しようとしたからわかる問題」であり、
それがわかっただけでも収穫なのですが、
やっぱりSSRはいろいろ難しいですね・・・(´-ω-)

まあ、現実にはそこまで大きなアクセスがないサイトですし、
今回は特に何もしませんでした

(3) 通信量が大きすぎる

問題が解決した(あるいは先送りした)ので、今度は本番にデプロイして、
Googleのモバイルスピードテストサイトで確認したところ、
めっちゃ遅いという判定にΣ(゚Д゚)ガーン

developers.google.com

SSRで速くしたはずなのに、なんでや・・・と思ったら、
どうも通信量が大きすぎるという指摘でした

単純にNginxがgzip圧縮することで解決しましたが、
もっと早くやっておけって話ではありますよ(´・ω・)(・ω・`)ネー

あとはまだ、packs/ccpts.jsがminifyされてないって問題があるのですが、
いまいちうまくいかないので、いったん保留しております

まとめ

ということで、長々と書いてきましたが、最後にまとめです...φ(・ω・`)

  • SSRの大前提を忘れてはいけない
    • SPAがあってのSSRである
    • 最初からサーバサイドでviewを作る設計との比較が必要
  • 暗黙的に「ブラウザ」に依存している部分の排除が大変
    • JavaScriptで書かれたコードではあるが、「サーバサイドで動かす」のを意識しないとダメ
    • ccpts程度の規模でもこれだけ大変なので、業務レベルのアプリではかなり面倒
  • (個人的に)「うまい設計」がまだ見えない
    • 全部サーバサイドはなんか違和感があるので、うまくクライアントと切り離したい

今までがそうであったように、今回の修正もこれが完成ってわけではないので、
また何か新しい技術を使って整理できるといいかなと思います

*1: とはいえ、Cookieが扱うのは「データ」であって「ロジック」ではないので、制御のコードはまだクライアントにあり、その意味では設計が維持されています