ReactOnRailsとSSRとWebpackerを捨ててWebpackに移行した件
久々に「チェンクロパーティーシミュレーター」(以下「ccps」)のお話です
Rails6のRC2がリリースされたので、夏休みの課題的な感じで、
Rails6への移行を進めようと思ったのですが・・・
・・・なぜかReactOnRailsとWebpackerを捨てて、
ピュアなWebpackに移行していたのですΣ(・ω・ノ)ノ
react_on_railsを破棄し、webpackに移行 · parrot-studio/cc-pt-viewer@592a34d · GitHub
正直、短期間にいろいろやり過ぎて、私の中でも整理しきれていないのですが、
夏休みの課題をレポートとしてまとめておくのも必要なことなので、
覚えている範囲でいろいろ書いていきます...φ(・ω・`)
今回言いたいこと
今回の経緯
面倒なので、ざっと経緯をまとめておきます...φ(・ω・`)
- 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でかなり苦戦したが、結果的に無駄に含まれていたコードを排除することに成功
- 「SSRもWebpackerもいらなかったんや・・・(´-ω-)」 <- イマココ
参考にしたサイト
- TypeScriptで書かれたReactをWebpackerで動かすための土台
- WebpackerからWebpackへの移行
- CSSをjsでimportするのキモいけど、どうにかならんの・・・の解答
- fontawesome5がまるごとimportされるのキモいけど、どうにかならんの・・・の解答
以降は私自身のためのメモ書きです...φ(・ω・`)
SSRの限界と結果的に得られたメリット
ccptsはjQueryとReactで構築されているため、
SSRは難しそうに思いますが、「ブラウザが必要とするコード」と、
「静的なHTMLを生成するためのコード」を分離することで、SSRに成功しました(`・ω・´)
- React+jQuery+RailsのSPAをサーバサイドレンダリングに移行した件(その1:概要編) - ぱろっと・すたじお
- React+jQuery+RailsのSPAをサーバサイドレンダリングに移行した件(その2:ブラウザ依存排除編) - ぱろっと・すたじお
- React+jQuery+RailsのSPAをサーバサイドレンダリングに移行した件(その3:設計変更編) - ぱろっと・すたじお
これ自体は満足していたのですが、問題はやはりサーバの負荷です
ある時期からさくら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─~~
ちなみに、夏休みの課題的な感じで、ccptsのベースをいじっているのだけど、結論からいえばwebpackerに殺意を覚えて切り離すことにしたので、もうちょっとかかりますΣ(゚Д゚)ガーン
— ぱろっと (@parrot_studio) 2019年8月11日
「Webpack」について理解することで、
「WebpackerのDSL」が何をしようとしていたのかは理解しましたが、
Webpackを理解してからだと、あまりに回りくどいです
アップデートも遅くて、Webpackerが依存しているライブラリのために、
GitHubからセキュリティ警告が来ていたりして、
アップデートが来ないとどうにもならなかったりとか
(Webpackに移行して、各ライブラリを最新にしたらもちろん警告はなくなりました)
Railsが「Webサイトに必要な技術的要素」をフルスタックで容易にしているのは利点ですが、
クライアントサイドについてはRailsと無関係なプラクティスがあふれており、
それをRailsの文脈にわざわざ変換させるのは、実際の業務においても面倒だと思います(´-ω-)
「webpack.config.js」を一から理解する
ということで、純粋な「Webpack」について、一から学んでいきました
webpackのTree Shakingを理解して不要なコードがバンドルされるのを防ぐ - Qiita
Webpackも4になってだいぶ使いやすくなったそうで、
modeパラメータを指定することで、
最低限想定した処理をしてくれるとか、簡単になっています
とはいえ、今回はあくまで「Railsの上で動かす」ことが前提であり、
「index.html」からアクセスさせるわけではないので、
一般的なwebサイトとは少々やり方が異なります
ポイントは以下です
- 元のWebpackerにあわせて、「app/javascript/packs」を起点とする
- ビルドしたコードを「public/packs」に吐き出す
- viewから自分で書いたhelperである「javascript_pack_tag」等を呼び出す
- Railsのフロントにあるサーバ(Nginx等)から直に「public/packs」以下を参照させる
1.2.のディレクトリはどこでもいいのですが、引き剥がしたとはいえRailsのプロジェクトなので、
一応Webpackerの作法に合わせて決めてあります
3.が最大のポイントで、Railsが用意していた「javascript_pack_tag」を自分で実装する必要があります
実際にはこんな感じです(´・ω・)っ
- 参考:今日から簡単!Webpacker 完全脱出ガイド - pixiv inside
- 実装:cc-pt-viewer/webpack_helper.rb at master · parrot-studio/cc-pt-viewer · GitHub
結果的に、このようなタグが出力されます
<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を使おうとも思わなかったので、
非常に感謝はしていますが、やっと補助輪が外れて、
「本来の運用」ができるようになった・・・ということですね(`・ω・´)