NuxtのSSRモードでメモリリーク?原因はaxios?

はじめに

最近金髪から黒髪に戻して更生しました藤井(フロントエンドエンジニア)でございます。久々にテックブログに貢献させて頂きます!

 

今はメインの施工管理機能の改修の傍ら、新機能開発にも携わっておりまして、フロント側はSSRモードのNuxtを採用しております。さて、そんな中SREチームから「フロントエンドがメモリリークしていませんか?」との通報が入りました。

 

いやいや、そんなハズは・・と思いながらもDatadogを見てみると見事にメモリ消費量が右肩上がりに増えていってます。なぜじゃ・・・。

f:id:yohei-fujii:20200314183203p:plain

調査開始

メモリリークになるような処理を入れた覚えはないぞ!と思いつつも調査開始です。ひとまずChromeのDevtoolでPerformanceやMemoryタブを見ながら確認しますが、これと言って原因が見つかりません。

f:id:yohei-fujii:20200314201317p:plain

 

おかしい・・。なんでだろう。

あれこれ試しましたが解決せず、ほぼ1日消化しました(やってしまった・・)

 

SSRのデバック

そして他の人にも相談しながらやはり気になったのはSSRモードということです。考えてるだけでは何も進まないので、ひとまず調査してみることに。以下の記事を参考にさせていただきました。

qiita.com

ターミナルで以下のコマンドを叩いてNuxtを立ち上げます。

node --inspect node_modules/.bin/nuxt-ts

次に、以下のページをブラウザで開きます。

chrome://inspect/#devices

 

このような画面が開きますので、Inspectを押してChrome Dev Toolsを起動させます

f:id:yohei-fujii:20200314184607p:plain


そして立ち上がったNuxtアプリケーションを確認します。最初にスナップショットを撮って置きます。あとは何度もページ再読み込みしたりなどしてアクセスをかけました。そしてもう一度スナップショットを取得(HEAP SNAPSHOTはChrome DevToolsのMemoryタブから取得できます)

 

カメラで撮影をしている人のイラスト

 

いよいよ取得したスナップショットを確認です。

さてさて、確かにSnapshot 1 から2にかけてメモリ消費量が増えてます。ひとまずShallow Size(オブジェクト自体の大きさを表す)の大きなものからパーッと目を通しました。

 

おぉー、いっぱいあるなぁ。どれどれ。

根気よく中身を確認していきます。

 

f:id:yohei-fujii:20200314185115p:plain

そうしているとArrayの中に気になる部分を見つけました! 

InterceptorManager?はて、こんなものがなぜ出てくるのか・・。

f:id:yohei-fujii:20200314185429p:plain

そしてそれはArrayだけではなく、system / Contextにも同じようなものが出ていることを確認できました。

 

InterceptorManagerと見て、真っ先に思いつくのはHTTPクライアントであるaxiosです。やはりSSRモードだと何か悪さしているのか?と気になりググってみると似たような記事がヒットしました。

 

techblog.timers-inc.com

 

これは、確かに気になるぞということで早速consoleなどを仕込んで確認します。

f:id:yohei-fujii:20200314190007p:plain

 

まず初回アクセス時点でのInterceptorsの中身を確認。

f:id:yohei-fujii:20200314190104p:plain

ターミナルの方でhandlersの中身が空であること、そしてブラウザの方でも初期値はまだセットされておらず空であることを確認できました。

f:id:yohei-fujii:20200314190215p:plain


それでは、何回かアクセスを繰り返してみて確認します。

すると・・

f:id:yohei-fujii:20200314190449p:plain

 

あら、増えている・・。

 

f:id:yohei-fujii:20200314190500p:plain

 

なぜだー!!!お前はなんだ!

どうやらpluginsはSSRモードだとリクエストの度に動くので、そのことが起因して、axiosインスタンスが肥大化して行っているようです。

 

後々気付いたのですが、公式ページにちょこっとだけ書いてありました。

私たちは isomorphic な HTTP リクエストを作るために axios を使っています。私たちはあなたの Nuxt プロジェクトに、私たちの axios module を使うことを強くオススメします。

node_modules 内の axios を直接使用しており、axios.interceptors を使用してデータを処理する場合、interceptors を追加する前にインスタンスを作成してください。そうしなければ、サーバレンダリングされたページをリフレッシュする際に、interceptor が複数追加され、データエラーが発生します。 

ja.nuxtjs.org

いよいよ対応処理

どうするかというと、Interceptorsのhandlerが存在する場合(=2回目以降のplugins呼び出しの場合)に登録実行させなければ、インスタンスの肥大化を防ぐことができるのではないかと考え、判定処理をいれました。

f:id:yohei-fujii:20200314191536p:plain

一度変数interceptorManagerに代入しanyを付与しているのは、型の問題があったからです。AxiosRequestConfigにはhandlersがないので、どうしようかとも考えましたが、ひとまずanyを使ってしのぎました。(どなたかより良い方法あったら教えてくださいませ)

f:id:yohei-fujii:20200314191617p:plain

idがすでに1以上であれば、つまりinterceptorsの登録が既にされていたらreturnするようにしました。

 

そして運命の確認!もう一度ブラウザを開き、何度もアクセスをして見てみました。

f:id:yohei-fujii:20200314191938p:plain

 よし!何度ページを読み直しても増えていません!

 

それでは、念の為今回の対応を取り込み、検証環境で確認してみます。再度Datadogを確認します。

 

f:id:yohei-fujii:20200314200259p:plain

見事メモリ消費量が安定しました!めでたしめでたし!

 

さいごに

思わぬSSRの罠にハマってしまった感があります。日頃から気をつけていても、今回はまさかお前か!みたいな感じでメモリリークを起こしてしまっていました。SSRモードの場合は、pluginsだけではありませんがライフサイクルにはかなり気をつけなければなりませんね。

 

さて、今回の作業のようにパフォーマンス改善調査というのは根気のいる作業ですが、品質を考える上では気をつけて対応していかなければならないところです。まだまだ課題も多いですが、一つ一つ解決していけたらなと思います。興味ある人、ぜひ一緒にやりましょう!