ぱろっと・すたじお

技術メモなどをまったりと / 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の分析だと警告が出まくりますが、そもそも複雑なサイトであるため、ある程度はあきらめてます