Nuxt SSG(Full Static) で外部 css/js を一度だけ Lazy Load する

Published: 2022/6/25


問題

例えば Katex の css であったり、 gtag.js などのように、一度だけ <head> に記述して外部からロードする前提となっているようなライブラリないしリソースは、割と存在する。

そのようなリソースを読み込みたいとなったとき、単純な実装方法としては、 nuxt.config.tshead にその読み込みを記述することではあるが、これをやってしまうとすべてのページにおいてこのスクリプトがロードされることになり、これは SEO ないし UX 的な観点では好ましくない。

// nuxt.config.ts

export default {
  head: {
    script: [
      { src: 'https://examle.com/foo.js', async: true }
    ]
  }
}

そのリソースを実際に利用するコンポーネントの head にこれを移動するという手もあるが、その場合は、記述された <script> は vue の reactivity による管理の下に入るため、コンポーネントが unmount されたタイミングで head から再度消えてしまい、次にコンポーネントが mount されるタイミングでまた head に追加される、という処理になる。

上記のページにある通り、 <script> は document に attach されるたびにその中身が評価されるので、単純にその評価に使う計算資源が無駄であるということと、ロード対象がリソースが二度実行されることを想定した作りになっていないかもしれないということから、この実装はあまり好ましいとは言えない。

また、ページの表示速度もろもろを高速化することを考えると、 initial render のタイミング、つまりサーバーから html として送られてきたタイミングで既に、 <head> にその読み込み記述があることが望ましい。

まとめると、外部 js や css は、以下の要件を満たすように <script><link> として <head> の下に記述したいことになる。

  1. それを必要としないページでは追加されない
  2. ただし、一度ロードしたならば、それを再度ロードする処理は無駄なので、それ以降はロードされた状態になっている(<head> に追加された <script> ないし <link> は、それ以降はそのままになる)
  3. さらに、もしそのページにて必要なリソースであったならば、 SSR 時点の html に <head> の中で読み込む対象として記述される。

これを Nuxt.js の、特の Full Static (SSG) の場合ではどのように実現するべきかについて、調べた際の記録をまとめる。

回答: store でフラグ管理し、 created でそのフラグを true にする

まず、 Nuxt は SSR つまりサーバー側の記述においては、以下の制約がある。

  1. beforeMountmounted のフックは実行されない。
  2. reactivity は動作しない

また、 SSG におけるクライアント側でのページ遷移では、 nuxt generate 時の asyncData の戻り値が payload.js としてダウンロードされ、それが直でコンポーネントに代入されることになる。

これらの制約を考えると、以下の方針に行きつく。

  1. lazy load する外部 css ないし js は、 layouts/default.vue など、すべてのページで利用されるコンポーネントの head の値として、記述する。
  2. ただし、読み込む必要がない場合は読み込みを行いたくないため、それを行うかどうかを store の中でフラグとして管理し、その値は一度 true になったならば、それ以降はそのままにする。

それを記述して、例えば以下。

<!-- layouts/default.vue -->

<template>
  <Nuxt />
</template>

<script lang="ts">
import Vue from "vue";
import { useStore } from "~/store/some-resource"

export default Vue.extend({
  head() {
    const store = useStore(this.$pinia)
    return store.load ? {
      script: [{ src: 'htts://example.com/foo.js, async: true }]
    } : {}
  }
})
</script>

そして、どこで loaded を true にする処理を記述するかというと、これにはそのリソースを利用するコンポーネントの、 created が利用できる。

// 例
import Vue from "vue";
import { useStore } from "~/store/some-resource.ts"

export default Vue.extend({
  created() {
    const store = useStore(this.$pinia)
    store.load = true
  }
})

というのも、 SSR では reactivity がたしかに動作しないが、 head() は、実装としては computed が利用されていて、かつ実際にそれが呼び出されるのは、ページコンポーネントの render が完了しすべての vm が生成された後に行われる、head の要素の計算を行うタイミングにおいてであるので、コンポーネントが created の中 store の値を書き換えていたとすると、それはもれなく head の計算に反映されることになる。

また、 asyncData とは違いこのコードはクライアント側でも実行されるので、対象のリソースを必要とするコンポーネントが render されることになったならば、そのタイミングでこのリソースは <head> に reactivity によって追加されていくことが期待できる。

まとめ

外部リソースをただ一度だけロードしながらも、そのロードを必要になるまで遅延したいとなったときには、以下の実装を行えば良い。

  1. 対象外部リソースをロードするかどうかを store にてフラグで管理する。
  2. 共通レイアウトの head() にて、 store のフラグの値を見てロードするかどうかを決める
  3. そのリソースを必要とするコンポーネントが、 created() のタイミングで store のフラグをオンにする
  4. store のフラグは一度オンになったならば、それ以降はオンのままになるようにする

Tags: nuxt

関連記事