この記事は ESM Advent Calendar 2021 - Adventar の21日目の記事です。
こんにちは、swamp09です。
とあるRailsアプリケーション開発プロジェクトで、フロントエンドのパフォーマンスを計測するのに Lighthouse を使いました。パフォーマンスの項目を見てみると「レンダリングを妨げるリソースの除外」が指摘されており中身をみるとapplication.jsで多くの無駄が発生しているようです。Railsアプリケーションでは application.js が肥大化してしまうケースがたまに見られますね。 Eliminate render-blocking resources を読むと、まずはカバレッジを見て必要なリソースだけを読み込むこと、次いでスクリプトがレンダリングブロックしないようにするとのことです。ある画面ではGoogle ChromeのDevToolsのカバレッジを見るとapplication.jsは91%使用していないという結果になっていました、なんと…。未使用のコードが多すぎるという問題は画面ごとに必要なJSだけ読み込むように地道にリファクタリングしていくしかないですね…。
レンダリングブロックを除く
スクリプトがレンダリングブロックしないようにするには、application.jsを読み込みをasync
かdefer
にします。 これらがないとJavaScriptがHTMLパーサーをブロックしてしまうのでパフォーマンスに悪影響があります。async
とdefer
の違いは、 ブラウザの仕組み: 最新ウェブブラウザの内部構造 の スクリプトとスタイル シートの処理順序 によると、async
がHTMLのパースとは別スレッドで処理され、defer
はHTMLのパースが終わってから順番に処理されるということのようです。application.jsではライブラリの読み込みなどを行っており、読み込む順序を意識する必要があったのでdefer
を使うことにして、application.jsの読み込みを javascript_include_tag 'application', defer: true
のように変更しました。
ちなみにパーサーブロックとレンダリングブロックは微妙に違うようなのですが、MDNではパーサーブロックと書いてあり Eliminate render-blocking resources ではレンダリングブロックと書いてあります。あんまり気にしなくて良いのかな…。
インラインスクリプトの対応が必要だった
さておき、よしこれでパーサー|レンダリング ブロックしなくなって良くなったな!!と思いきや問題が起き、application.jsで読み込んでいるライブラリに依存しているコードがインラインスクリプトにありそれらが動作しなくなってしまいました。じゃあインラインじゃなくして全部JSファイルにしてdefer
で読み込めばいいか!!!と思ったのですがそんなことはなく、レンダリングを妨げる JavaScript を削除する によると、小さいコードならインラインが最適なケースがありなんでもかんでもJSファイルに切り出すのは良くないようです。
インラインスクリプトのままでJSファイルにあるライブラリ読み込みの遅延を待つために、DOMContentLoaded
でライブラリの読み込みを待ったあとで処理を開始するようにします。defer
によると「defer 属性の付いたスクリプトは、スクリプトが読み込まれて評価が完了するまで、 DOMContentLoaded イベントの発生が抑制されます。」とのことなのでdefer
でライブラリを読み込み DOMContentLoaded
イベントを待てば安心ですね。
おまけ
余談なのですが、最近リリースされたRails 7.0ではデフォルトでimportmap を使用しています。importmapはES Modulesを読み込むもので、ES moduleの読み込みはクラシックスクリプトとは別でモジュールスクリプトとなっており、defer
は「defer 属性はモジュールスクリプトには効果がありません。」とのことなのでasync
しか使えないようです。defer
との違いを意識していないとimportmapを使うとき処理順序が起因のエラーが起きるのかもしれません。