Vue は遅延ローディングルートをどのように実現しているのか

マッチングアプリ「CoupLink」の開発担当をしている横山です。

本記事は、Vue公式ドキュメントで紹介されている遅延ローディングルートについて技術的に深掘りしよう、という主旨で執筆したものです。
初学者ゆえ、説明の至らぬ点もあると思いますが、何卒ご容赦ください。

目次

遅延ローディングルートとは

前提として、SPAは初回の通信で全てのページのリソースを取得します。

この時、Vue Routerに登録している全てのルートコンポーネントの読み込みが行われるため、初回読み込みのファイルサイズが大きくなり、レンダリングの速度が遅くなりがちです。

しかし、遅延ローディングルートというテクニックを使うと、各ルートコンポーネントを必要になったタイミングでのみ読み込んでくれるようになり、SPAの抱える問題を解決してくれます。

書き方

import モジュール名 from パス

components: モジュール名

今までこう書いていたのを

components: () => import('モジュール名')

こうするだけ。めっちゃ簡単。

ディレクトリ構成が単純だったり、規則性を持ってる場合はmapを使ってスマートに書くことも出来ます。

const routeOptions = [
  { path: '/', name: 'Home' },
  { path: '/mypage', name: 'MyPage' }
]

const routes = routeOptions.map(route => {
  return {
    ...route,
    component: () => import(`@/components/${route.name}`)
  }
})

では、何故これで動くのか見ていきましょう!

抑えるべき3つのポイント

1. Webpack の Code Splitting

Code Splitting allows you to split your code into various bundles which can then be loaded on demand or in parallel.

https://webpack.js.org/guides/code-splitting/#dynamic-imports

Code Splittingとは、コードを複数のバンドルファイルに分割し、非同期 or 並列で読み込む仕組みのことです。分割された各ファイルのことを、チャンクと呼んだりします。

分割されたファイルの名前は、デフォルトでは0から順に数字が割り当てられるのですが、下記のように明示的に指定することも出来ます。同名のChunkNameを持つモジュールは、1つのバンドルファイルにまとめられます。

components: () => import(/* webpackChunkName: "MyPage" */ '@/components/MyPage')

2. Vue の Async Component

これは名前の通り、非同期でコンポーネントを読み込むための仕組みです。

 Vue allows you to define your component as a factory function that asynchronously resolves your component definition. Vue will only trigger the factory function when the component needs to be rendered .

You can also return a Promise in the factory function, so with Webpack 2 and ES2015 syntax you can make use of dynamic imports.

https://v2.vuejs.org/v2/guide/components-dynamic-async.html#Async-Components

Component(s)オプションに非同期で解決するコールバック関数 or Promiseを返す関数を渡すと、WebpackにCode Splittingを行うよう(= ファイルを分割し非同期で読み込む) 設定をしてくれるということです。

3. JS の dynamic import

そして、実際にPromiseを返す処理をしてくれているのが import( ) です。関数っぽく見えますが、実際はただのキーワードです。

dynamic imports are only evaluated when needed, and permits greater syntactic flexibility.

It returns a promise which fulfills to an object containing all exports from moduleName, with the same shape as a namespace import (import * as name from moduleName)

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import

実証

以下は、’webpack-bundle-analyzer’を使ってCoupLinkのバンドルファイルを図表化したものです。

従来の方法(Static Load)ではrouter配下のモジュール郡が、バンドルファイルの半分以上を占めていました。

しかし、遅延ローディングルートを使用することで、routesモジュールのサイズを1/70ほどに抑えることが出来ています。

余談: SPAのメリットが失われている?

遅延ローディングを行うことにより、ページ遷移の度にJSファイルの読み込みが発生してしまい、SPAのメリットが失われるのではないか、と思われた方もいるのではないでしょうか。

ですが、これもWebpackが解決策を用意してくれています。
Webpackは非同期コンポーネントに対して、prefetchを行うためのlinkタグをindex.htmlファイル内に自動で追加してくれます。

<link href="/js/MyPage.ハッシュ値.js" rel="prefetch">


prefetchとは、ユーザーが次に訪問する可能性が高いページのリソースを前もって取得する仕組みのことです。なので、実運用においてはprefetchが完了してる限り、全てのページ読み込みはcacheが使用されます。

実運用で起こりうるエラー

本番運用において、閲覧中のユーザーがChunkLoadingErrorに遭遇する可能性があります。

これは、読み込み先のバンドルファイルが見つからない場合等に起こるエラーです。

デプロイ時に古いビルドを削除しているような場合、デプロイ直前に利用していたユーザーにおいて発生します。

対策としては、リロードを強制してファイルを更新させるのが良いと思います。

router.onError(error => {
  if (error.name === 'ChunkLoadError') window.location.reload()
})