Predictive Prefetching、PrefetchとGuess.js、時々、Angular

こんにちは、川上です。

今年もAngularアドベントカレンダー盛り上がっていますね！
僕もAngularカレンダーに参加することができてたいへん興奮しております。

## この記事で紹介したいこと
　Predictive PrefetchingライブラリであるGuess.jsをAngularアプリに導入するまでを紹介いたします。しかし、そもそもPredictive Prefetchingがわからないので、その前提にある仕様から紹介していこうと思います！
　それでは少しの購読時間、あずからせていただきます！

> 記事のタイトルについて

　タイトルに深い意味はなくて記事を書き進めていくうちにキーワードが多くなってしまい。。記事タイトルを悩んでいた僕へは某映画のタイトルしか降ってきませんでした。


## Resource Hintと４つのAPI
　Webの仕様の中には、リソースを事前に取得してページの読み込みを向上させるための仕様があります。それは[Resource Hints](https://triple-underscore.github.io/resource-hints-ja.html)というもので、それに沿ったAPIが４つの存在します。順番に紹介していきます。

### [dns-prefetch](https://triple-underscore.github.io/resource-hints-ja.html#dns-prefetch)

　ドメインの名前解決を事前に解決するAPIになります。外部サイトのリソース（JS、CSS、画像など）に適用すると効果的です。

**書き方**

```html
<link rel="dns-prefetch" href="//example.com">
```

それでは、実際の効果の程を[WEBPAGETEST](https://www.webpagetest.org/)を使ってみてみましょう。
![Image from Gyazo](https://i.gyazo.com/e391bf9d6dc7ee23bfd7fd3fe52151e8.png)
　dns-prefetchはなるべく早く解決したほうが効果的となるため、Elementの設置場所は、`<head>`内のなるべくはじめの方に書くと良いでしょう（Content-typeやTitleの直後など）
　※ちなみにアクセスしているサイト（この場合でいうとkawakami.dev）に対してdns-prefetchを指定しても何も変化はありません、サイトにアクセスした時点で、すでに解決されているためになります。

***

### [preconnect](https://triple-underscore.github.io/resource-hints-ja.html#preconnect)
　preconnectは、dns-prefetchの処理に加えTCPのハンドシェイク、TLSのネゴシエーションも事前に行うAPIです

**書き方**

```html
<link rel="preconnect" href="//example.com">
```

効果を[WEBPAGETEST](https://www.webpagetest.org/)で見てみましょう。
![Image from Gyazo](https://i.gyazo.com/e4d92e9816c8f606258d9ff08a938df2.png)
※ preconnectは[IE](https://ja.wikipedia.org/wiki/Internet_Explorer#/media/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB:Internet_Explorer_10+11_logo.svg)では対応されておりません。
以下のように書くと、IEであってもdns-prefetchまでは行ってくれるようになります。

**IEを意識した書き方**

```html
<link rel="preconnect dns-prefetch" href="//example.com">
```


>ドメインを指定するのは一苦労

　dns-prefetch、preconnectで指定するURLは、そのページで指定する外部ドメインすべてを指定するのが望ましいです。しかし、そのドメインはHTMLにあったり、CSSにあったり、JSにあったりで調べるのが一苦労です。
　そこでおすすめは、[WEBPAGETEST](https://www.webpagetest.org/)にはDomainsというタブがありまして、そこを見ていただくとそのサイトがアクセスしたドメインが一覧できるので、こちらからドメインを取得するのがおすすめです。

[![Image from Gyazo](https://i.gyazo.com/068e06c8fdab8051c5b1d350f0c89f68.png)](https://gyazo.com/068e06c8fdab8051c5b1d350f0c89f68)

### [prefetch](https://triple-underscore.github.io/resource-hints-ja.html#prefetch)

　ここまで紹介してきたAPI（dns-prefetch,preconnect）は、アクセスしているページで使用する外部リソースのためのAPIでした。ここからはアクセスしていない別ページのリソースを扱うAPIになります。
　prefetchは、別ページのリソースを取得するためのAPIになり、これから訪れるであろうページのリソースを先に読み込んでおくために使います

**書き方**

```html
<link rel="prefetch" href="second.html" as="document">
<link rel="prefetch" href="assets/js/second.js" as="script">
<link rel="prefetch" href="assets/styles/second.css" as="style">
<link rel="prefetch" href="assets/images/pic03.jpg" as="image">
```

[as属性](https://triple-underscore.github.io/HTML-metadata-ja.html#attr-link-as)は任意で指定することができまして、リソースに応じたコンテキストを指定することができます。as属性の一覧は[コチラ](https://fetch.spec.whatwg.org/#concept-request-destination)

それではその効果を[WEBPAGETEST](https://www.webpagetest.org/)でみてみましょう。

[![Image from Gyazo](https://i.gyazo.com/064e0ce5849feef0b830b8e01a33ab0e.png)](https://gyazo.com/064e0ce5849feef0b830b8e01a33ab0e)
※またcrossorigin属性も任意でつけることができまして、CORSポリシーを設定することができます。

```html
<link rel="prefetch" href="//example.com/next-page.html" as="document" crossorigin="use-credentials">
```

### [prerender](https://triple-underscore.github.io/resource-hints-ja.html#prerender)

prerenderは、preconnectに加えて読み込んだリソースをキャッシュ上にレンダリングまで行ってしまいます。それにより、後からそのページにアクセスしたときに素早くページを表示することができます。

```html
<link rel="prerender" href="second.html">
```

それでは動きを[WEBPAGETEST](https://www.webpagetest.org/)を使ってみてみましょう。
[![Image from Gyazo](https://i.gyazo.com/9f7c72fa584462c756bd68b6e02b920f.png)](https://gyazo.com/9f7c72fa584462c756bd68b6e02b920f)

魅力的なAPIですが、prerenderは１つしか設置することはできません

### [preload](https://triple-underscore.github.io/preload-ja.html)の紹介
　Resource Hintsの仕様には含まれないのですが、preloadというprefetchに似たAPIもありますので紹介します。
　preloadはprefetchによく似ているのですが、prefetchは外部リソースの先読みに用いてたのに対して、preloadは現在アクセスしているページのリソースをなるべく早く読み込ませるために使用するAPIです。
　Web Fontsなどなるべく早く読み込ませたほうが良いリソースに使うのがおすすめです。

```html
<link rel="preload" as="font" type="font/woff2" href="//fonts.gstatic.com/s/pacifico/v16/FwZY7-Qmy14u9lezJ-6I6MmBp0u-zK4.woff2" crossorigin>
```
動きを[WEBPAGETEST](https://www.webpagetest.org/)でみてみましょう。

[![Image from Gyazo](https://i.gyazo.com/81cdcd06e58d5b51739ed012b792d206.png)](https://gyazo.com/81cdcd06e58d5b51739ed012b792d206)

　いかがでしたでしょうか。
　以上でAPIの紹介は終わりになります


## prefetch、prerender指定の難しさ
　Resource Hintsの中でも、prefetch、prerenderは魅力的なAPIですが、サイトの全てのリソースを指定しておけば良いというわけではありません。（prerenderはそもそも１つしか指定できない制約があります）、ユーザーが次に訪れるであろうページを予測して読み込ませて置く必要があります。
　リンク先数が少ないページであれば予測もしやすいですが、リンク先が多いページでは予測するのも困難です。また、予測が行なえても数日後もその予測が当たっているという事はまず無いでしょう。

## Predictive Prefetching
　このようにメンテナンスが難しいprefetch、prerenderを、Analyticsデータと機械学習を使って解決してくれるライブラリがあります。それが[guess.js](https://github.com/guess-js)になります。
　作者であるMinkoは、この予測してprefetchする方式をPredictive Prefetchingと命名し[解説](https://web.dev/predictive-prefetching/)しております

### Minko Gechevの紹介
![Image from Github]
(https://avatars2.githubusercontent.com/u/455023?s=200&v=4)
Angular Team / Senior developer program engineer at Google
[Twitter](https://twitter.com/mgechev)
[Github](https://github.com/mgechev)

　今年の夏に[ng-japan主催のイベント](https://www.meetup.com/ja-JP/ng-japan-tokyo/events/263547856/)や[Bulderscon](https://builderscon.io/builderscon/tokyo/2019/session/9129b348-fcb3-41a6-8833-addf7f3e7fa9)で登壇されたりしていました。人柄も気さくで私のつたない英語の質問にも真摯に聞いてくれて大変うれしかったです。

## Yutubeビデオの紹介
　今年のChrome Dev Summitにはセッションはありませんでしたが、後日YoutubeのGoogle Chrome Developersチャンネルに`Predictive Prefetching`というタイトルで紹介しております。
[![Image from Gyazo](https://i.gyazo.com/88b8fa962577f687aa49660eece718ed.jpg)](https://youtu.be/FS5NYN_n7_k)
　このビデオではPredictive Prefetchingの紹介や事例などが紹介されております。なんとMicrosoftやYoutubeもPredictive Prefetchingを使ってパフォーマンス改善を行っているそうです。
　試してみたくなってきました！！

## Giess.jsをwebpackに導入してみる
それではguess.jsをサイトに導入するまでを紹介いたします。まずはAngularアプリへではなく、Webpackを使ったサイトへの導入の仕方を紹介します。

### install
```sh
npm i guess-webpack --save-dev
```

### webpackへGuessPluginの設定
webpack configで読み込みます

```webpack.config.js
const { GuessPlugin } = require('guess-webpack');

module.exports = (env, argv) => {
    ...
```
webpackのplugin内にて、GussPluginを設定します

```js:webpack.config
plugins: [
            new GuessPlugin({
                GA: '自身のView IDを指定してください',  
                debug: true,
            })
```
　View IDというのは、Google AnalyticsのIDとは違うものです。[コチラ](https://ga-dev-tools.appspot.com/query-explorer/)のリンク先から取得できます。
　GuessPluginにはその他にAdvance Optionがあります。詳しくは[コチラ](https://github.com/guess-js/guess/tree/master/packages/guess-webpack#advanced-usage)からご確認ください。
　ここでwebpackを動かすとbuild前にブラウザが立ち上がりGoogle Analyticsへのアクセス許可が求められます。ここで許可することでGuess.jsがGoogle Analyticsのデータを使用できるようになります。
　しかし、build時に毎回この認証が動いてしまうので、production build時にのみ動くように設定を変更してあげるのが良いでしょう。


### フロント側の設定
フロント側の設定では、以下のようにguessをimportして実行するだけです。

```index.js
import {guess} from 'guess-webpack/api';

guess()
```

しかし、guess()はPredictした候補のオブジェクトを返すだけなので、それからLink Elementを作成するなどの処理が必要となります。サンプルとしては以下のようになります。

```index.js
import {guess} from 'guess-webpack/api';

window.addEventListener('DOMContentLoaded', () => {
    addPrefetchToHead();
});

function addPrefetchToHead() {
    if (typeof window !== 'undefined') {
        for (const url of Object.keys(guess())) {
            let hint = document.createElement('link');
            hint.rel = 'prefetch';
            hint.href = url;
            hint.as = 'html';
            hint.crossorigin = 'use-credentials';
            document.head.appendChild(hint);
        }
    }
}
```

## AngularにGuess.jsを導入してみる

いよいよ、ここからはAngularにGuess.jsを導入するまでを紹介したいと思います。
Angular CLIは入っている前提になります

### Projectの作成
とりあえずプロジェクトを新規作成します。また、今回はルーターを使うのでそのように設定しておきます

```console
$ ng new guess-angular
? Would you like to add Angular routing? Yes
```

### コンポーネントを追加
３ページ新たに追加したいので、コンポーネントを３つ追加します

```console
$ ng g component one
$ ng g component two
$ ng g component three
```

### ルーティングを設定します
ページ移動ができるようにルーティングを設定します

```src/app/app-routing.module.ts
const routes: Routes = [
  { path: '', component: OneComponent},
  { path: 'two', component: TwoComponent},
  { path: 'three', component: ThreeComponent}
];
```

### 予測ファイルを作成します
今回はGoogle Analyticsの代わりに自分で作成した予測ファイルを使用したいと思います。
プロジェクトのルートディレクトリに`routes.json`を作成します

```/routes.json
{
  "/": {
    "/two": 80,
    "/three": 20
  },
  "/two": {
    "/": 20,
    "/three": 80
  },
  "/three": {
    "/": 80,
    "/two": 20
  }
}
```

### webpackを拡張できるようライブラリを追加

```sh
npm i -D @angular-builders/custom-webpack @angular-devkit/build-angular
```

### angular.jsonの変更
追加したライブラリを使うように`angular.json`を下記の通り変更します

```angular.json
...
      "architect": {
        "build": {
          "builder": "@angular-builders/custom-webpack:browser", // 変更
          "options": {
            ...
            "customWebpackConfig": { // 追加
              "path": "./extend.webpack.config.js"
            }
...
```

### Guess.jsを追加

```sh
npm i -D guess-webpack guess-parser
```

### Guess.jsの設定
Guess.jsをbuild時に動かすための設定をwebpack上に書きます。そのために、まず`extend.webpack.config.js`ファイルをプロジェクトのルートディレクトリに作成します。そして、以下のように設定を書きます

```extend.webpack.config.js
const { GuessPlugin } = require('guess-webpack');
const { parseRoutes } = require('guess-parser');

module.exports = {
  plugins: [
    new GuessPlugin({
      // Alternatively you can provide a Google Analytics View ID
      // GA: 'XXXXXX',
      reportProvider() {
        return Promise.resolve(JSON.parse(require('fs').readFileSync('./routes.json')));
      },
      runtime: {
        delegate: false
      },
      routeProvider() {
        return parseRoutes('.');
      }
    })
  ]
};
```

### build
そして、buildを実行します。

```sh
npm run build
```

### アプリの実行
ビルド後Guess.jsが適応されたファイルは、distディレクトリに出力されますので実際に起動する際には、`angular-http-server`などのライブラリを使用して`dist/[プロジェクト名]`をWeb Rootに立ち上げる必要があります。

```console
$ cd dist/guess-angular
$ angular-http-serve
```

### Angular 参考URL
ここまでの内容は、[コチラ](https://guess-js.github.io/docs/angular)のページを参考にしております

## その他のサンプル
Guess.jsでは、他にもたくさんの[サンプル](https://github.com/guess-js/guess/tree/master/packages/guess-webpack#demos)がありますので是非チェックしてみてください

## まとめ
- Resource Hintをうまく活用してサイトのパフォーマンス向上を計画してみてください。ただし注意が必要でprefetchはユーザーが意図していないリソースをダウンロードさせる行為です。そのため無闇矢鱈とprefetchを設定するのではなく必要最小限に設定するのが良いでしょう。
- Prefetch、Prerenderはメンテナンスが大変です。そこでサイトのAnalyticsデータなどを活用して自動的にメンテナンスが行えることを検討しましょう。またそれに適したライブラリGuess.jsがあります
- Guess.jsはWebpack版やAngular版も用意されていてきっとあなたの環境へも導入出来るはずです
- 明日のアドベントカレンダーは、noxi515さんの担当です！　お楽しみに！