Polyfil ファイルを Webpack の entry に含める

例えばこのサイトだとIntersectionObserverを使っていて、これはスマホなどでまだ対応しておらず見れないことが多いので含めています。

なぜ Webpack の entry に含めたいのか

<script>での読み込みは Lighthouse のスコアを下げる

HTMLで以下の Polyfil を読み込むことで、IntersectionObserverを対応していないサイトでも使えるようにできます。

<script src="https://polyfill.io/v2/polyfill.min.js?features=IntersectionObserver"></script>

しかしこれは、Minimize Critical Requests Depthとして引っかかってしまいます。

Image from Gyazo

NextJS の entry ファイルのルートで Polyfil を読み込む

上のnpm版である、intersection-observerを使います。

Polyfil は基本エントリーの最初に読み込むファイルの先頭にimportを記載すれば使えるので、_app.[jt]sx?をその形にすれば使えそうです。しかし、この Polyfil は中で window オブジェクトへアクセスしている部分があり NextJS ではその_appファイルも SSR で使われるので、 「windowundefinedだよ」なエラーが起こってしまうのでこれも駄目です。

// _app.tsx
import 'intersection-observer';

NextJS の Webpack-Config の entry に含める

next.config.jsからwebpack設定を自身の設定で上書きする方法です。

next.config.jsのデフォルトentry() => Promise<{[x: string]: string[]}>な形をしていて、このオブジェクトを上書きすれば良さそうです。以下は戻り値の例です。

Promise {
  { 'main.js': [],
    'static/runtime/main.js':
     [ '/Users/nju33/github/blog/node_modules/next/dist/client/next-dev' ],
    'static/development/pages/_app.js': [ './pages/_app.tsx' ],
    'static/development/pages/_error.js':
     [ '/Users/nju33/github/blog/node_modules/next/dist/pages/_error.js' ] } }

このmain.jsへ含めてます。

{
  webpack: config => {
    const originalEntry = config.entry;
    config.entry = () => {
      return originalEntry().then(entry => {
        entry['main.js'] = 'intersection-observer';
        return entry;
      }
    }
  }
}

ただまだこれだと、サーバーサイド用のビルドファイルにもintersection-observer Polyfil が含まれてしまい、上記と同じエラーが起きてしまうので、含めるのは Client 用のビルドファイルだけにする必要があります。
幸いにも、next.config.jswebpack関数は第二引数にoptions.isServerというフラグを渡してくれます。これは Client 用のビルド時にはfalseになってくれるフラグです。

つまり、

  • options.isServertrueのときは、デフォルトのままのentryを返す
  • falseのときは、intersection-observerを含める

とすれば解決できそうです。config.entryを以下のように改良してみます。

{
  webpack: (config, {isServer}) => {
    const originalEntry = config.entry;
    config.entry = () => {
      if (isServer) {
        return originalEntry;
      }
      
      return originalEntry().then(entry => {
        entry['main.js'] = 'intersection-observer';
        return entry;
      }
    }
  }
}

上のように変更したところ Lighthouse のスコアがあがり、かつIntersectionObserverに対応していない端末も対応することができました。