詳解 AMP Optimizer

この記事では AMP Optimizer v2.6.0 について説明しています。

## AMP Optimizer とは
[AMP Optimizer](https://github.com/ampproject/amp-toolbox/tree/main/packages/optimizer) は AMP ページの作成をシンプルにし、AMP のレンダリングパフォーマンスを最適化するツールです。[AMP Cache](https://developers.google.com/amp/cache?hl=ja) でも行われている AMP のサーバーサイドレンダリングをサポートしています。

npm のパッケージで配布されていますが、単体で使うよりは [Next.js の AMP 機能](https://nextjs.org/docs/api-reference/next/amp)や [WordPress の AMP プラグイン](https://wordpress.org/plugins/amp/)として間接的に使うことの方が多いかと思います。

## 動かし方

### CLI
CLI のコマンド一つで試すことができます。

```sh
$ cat <<EOF > index.html
> Test AMP Optimizer
> EOF
```

1行の html ファイルに以下のコマンドで AMP Optimizer を動かします。

```sh
npm install @ampproject/toolbox-cli -g
amp optimize index.html 
```
amp の文字がたくさん書かれたファイルが出力されます。

CLI では url を指定することもできます。example.com は AMP 準拠サイトではないので、AMP Valid なページは出力されません。

```sh
amp optimize https://example.com/
```

### API

実際に使うのはこちらだと思います。

インストール

```
npm install @ampproject/toolbox-optimizer
```

html を文字列で渡せば最適化した html 文字列が得られます。

```js
const AmpOptimizer = require('@ampproject/toolbox-optimizer');

const ampOptimizer = AmpOptimizer.create();

const originalHtml = `
<!doctype html>
<html ⚡>
  ...
</html>`;

ampOptimizer.transformHtml(originalHtml).then(optimizedHtml => {
  console.log(optimizedHtml); // 最適化された html
});
```

オプションの指定。以降で説明します。

```ｊｓ
const ampOptimizer = AmpOptimizer.create({
  verbose: true
});
```

オプションを指定せずにデフォルトで使ってもいくつかの最適化が行われます。以降で何が行われているのかについて説明します。

## AMP Optimizer は何をやっているのか？

ドキュメントの冒頭にデフォルトで行われる処理が記載されています。

- AMP layout のサーバーサイドレンダリング
- 不足している必須 AMP タグの自動追加
- amp-img、amp-iframe、amp-video、または　amp-video-iframe　からヒーロー画像を自動検出してプリロード
- （可能であれば） AMP Boilerplate の削除
- 不要なホワイトスペースの削除
- CSS キーフレームアニメーションを抽出してページ下部に移動
- AMP フレームワークとカスタムフォントの読み込みを最適化
- インライン化された amp-script 用のCSPを生成

AMP の最適化については以下の記事で説明されています。

-  [AMP をさらに高速化する方法](https://developers-jp.googleblog.com/2018/11/how-to-make-amp-even-faster.html)
-  [新しい AMP Optimizer で楽々 AMP 開発](https://developers-jp.googleblog.com/2020/03/amp-optimizer-amp.html)

## オプション

現時点で使えるオプションについて説明します。ドキュメント+αな内容です。

### `autoAddMandatoryTags`

AMP として足りないマークアップを自動的に追加します。

- name: `autoAddMandatoryTags`
- valid options: `[true|false]`
- default: `true`
- used by: `AddMandatoryTags`

わかりやすいところでいうと、`html` タグの `amp` 属性や AMP 用の script タグ (`v0.js`) が自動的に追加されます。

### `autoExtensionImport`

`amp-carousel` のような AMP 拡張(AMP コンポーネント)の script タグが自動的に追加されます。

- name: `autoExtensionImport`
- valid options: `[true|false]`
- default: `true`
- used by: `AutoExtensionImport`

script タグは 該当の AMP 拡張が使われているときのみに追加されるます。したがって、AMP Optimizer の使用を前提とするなら、手動で script タグを書かない運用も可能です。

### `format`

AMP のフォーマットを指定する。

- name: `format`
- valid options: `[AMP|AMP4EMAIL|AMP4ADS]`
- default: `AMP`
- used by: `AutoExtensionImport`, `AddMandatoryTags`

通常の Web サイトだけなら使うことはない。AMP のEメールや広告のフォーマットでも使用可能。それぞれ必須 AMP コードが異なるので、それらの差分を Optimizer が埋めてくれる。

### `experimentEsm`

AMP ランタイムとコンポーネントで JavaScript モジュールのサポートを有効にする。

```html
<script async nomodule src="https://cdn.ampproject.org/v0.js"></script>
<script async src="https://cdn.ampproject.org/v0.mjs" type="module" crossorigin="anonymous"></script>
```

**Warning: Invalid AMP ページになる**

- name: `experimentEsm`
- valid options: `[true|false]`
- default: `false`
- used by: `RewriteAmpUrls`

### `imageBasePath`

ビルド時に画像パスを解決する際に使用するベースパスを指定します。`(imgSrc, params) => '../img/' + imgSrc` の形式で書く。

- name: `imageBasePath`
- valid options: `STRING|FUNCTION`
- default: `undefined`
- used by: `Markdown`

### `imageOptimizer`

与えられた画像 src に対する srcset を計算する関数を提供して、画像 srcset の自動生成を有効にする。この関数は、srcset を返す。利用可能な画像がない場合は、`falsy` を返す。

ex.

```ｊｓ
const ampOptimizer = AmpOptimizer.create({
  imageOptimizer: (src, width) => `${src}?width=${width}`
});
```

### `lts`

AMP ランタイムとコンポーネントで LTS バージョンを使うことができる。

```
- name: `lts`
- valid options: `[true|false]`
- default: `false`
- used by: `RewriteAmpUrls`
```

AMP は通常2週間おきにリリースされています。ロードしている JavaScript が変わるので、なにもしなくてもバージョンに追従することができます。一方で、バージョンアップ起因の不具合もときどき起こります。通常のリリースサイクルとは別に LTS バージョンも提供されているので、このオプションでより安定な AMP を使うことができます。現在の AMP のバージョンは https://cdn.ampproject.org/rtv/metadata でみることができます。

### `markdown`

このオプションで Markdownファイルから AMP Valid な html を生成することができます。（少し語弊があるので後述）

```
README.md => HTML => AMP Optimizer => valid AMP
```

このオプションは `<img>` タグを `amp-img` または `amp-anim` タグに変換するだけです。実際のファイルから画像の寸法を解決しようとします。320pxより大きい画像は自動的に `intrinsic` レイアウトになります。画像検出を動作させるためには、dependency に `probe-image-size` が必要です。

- name: `markdown`
- valid options: `[true|false]`
- default: `false`
- used by: `Markdown`

実質 Markdown to html 変換器として使えます。

### `minify`
html を minify する。

- name: `minify`
- valid options: `[true|false]`
- default: `true`
- used by: `MinifyHtml`, `SeparateKeyframes`
 
### `preloadHeroImage`

ヒーロー画像の最適化を有効にします。ヒーロー画像は自動検出されるか、`data-hero` 属性で明示的にマークすることもできます。

```
<amp-img data-hero src="foo.jpg" ...>
```

`data-hero` でマークアップできるヒーロー画像の最大数は2枚です。

`data-hero` 属性が存在しない場合、`amp-img`、`amp-iframe`、`amp-video`、または、`amp-video-iframe` から自動検出されます。画像のプリロードリンクは、既に存在しない場合にのみ生成されます。`amp-img` 要素の場合は、`amp-img` 要素内の `img` 要素もサーバーサイドでレンダリングします。これにより、画像のレンダリング性能が大幅に向上し、Core Web Vitals の Largest Contentful Paint (LCP) の改善に寄与します。

- name: `preloadHeroImage`
- valid options: `[true|false]`
- default: `true`
- used by: `PreloadHeroImage`

通常 `amp-img` 内の `img` はクライアントサイドでレンダリングされています。

### `verbose`
ログを出力する

- name: `verbose`
- valid options: `[true|false]`
- default: `false`

## Transformer に踏み込む

実際に html ファイルの変換処理をしているのは Transformer です。前項の各オプションの used by にさりげなく書いてあるやつが Transformer です。[デフォルトで使われる Transformer](https://github.com/ampproject/amp-toolbox/blob/v2.6.0/packages/optimizer/lib/DomTransformer.js#L27-L59) は決まっていますが、`transformations` オプションで自由に指定することもできます。Transformer を自作して使用することもできます。

```js
const AmpOptimizer = require('@ampproject/toolbox-optimizer');
const {createElement, firstChildByTag, appendChild} = AmpOptimizer.NodeUtils;

class CustomTransformer {
  constructor(config) {
    this.log_ = config.log.tag('CUSTOM');
  }
  transform(tree, params) {
    this.log_.info('Running custom transformation for ', params.filePath);
    const html = firstChildByTag(tree, 'html');
    if (!html) return;
    const head = firstChildByTag(html, 'head');
    if (!head) return;
    const desc = createElement('meta', {
      name: 'description',
      content: 'this is just a demo',
    });
    appendChild(head, desc);
  }
}

// it's best to run custom transformers first
const customTransformations = [CustomTransformer, ...AmpOptimizer.TRANSFORMATIONS_AMP_FIRST];

// pass custom transformers when creating the optimizer
const optimizer = AmpOptimizer.create({
  transformations: customTransformations,
});
// you can add custom parameters on a per document basis
const transformedHtml = await optimizer.transformHtml(html, {
  filePath,
});
```
AMP First の場合 v2.6.0 ではデフォルトで使われる Transformer は以下です。Paired AMP の場合は少し異なります。Paired AMP は v2.6.0 時点で experiment feature みたいです。

- `AddMandatoryTags`
- `Markdown`
- `AutoExtensionImporter`
- `OptimizeImages`
- `PreloadHeroImage`
- `ServerSideRendering`
- `AmpBoilerplateTransformer`
- `ReorderHeadTransformer`
- `RewriteAmpUrls`
- `GoogleFontsPreconnect`
- `PruneDuplicateResourceHints`
- `SeparateKeyframes`
- `AddTransformedFlag`
- `MinifyHtml`
- `AmpScriptCsp`

Transformer を読めば実際の変換処理を理解することができます。以降ですべての Transformer について説明します。

### `AddAmpLink`

Valid AMP への参照を link タグを追加する。Paired AMP のデフォルトです。

```
<link rel=amphtml href=${ampUrl}>
```

### `AddBlurryImagePlaceholders`

`amp-img` と `amp-video` のポスター画像のプレースホルダとしてぼかし画像を追加します。jpeg のみ対応です。ぼかし画像の作成は計算コストがかかるのでキャッシュ前提のオプションです。Paired AMP のデフォルトです。

使えるオプションは以下。

- `blurredPlaceholders`: ぼかし画像プレースホルダの有効化。 デフォルトは `false`
- `imageBasePath`: 画像のベースパス
- `maxBlurredPlaceholders`: ぼかし画像の最大数。デフォルトは `5`
- `blurredPlaceholdersCacheSize`: キャッシュするぼかし画像の最大数。キャッシュを無効化する場合は `0`、すべてのプレースホルダをキャッシュする場合は `-1` を指定する。デフォルトは `30`

ex.

```
const optimizer = AmpOptimizer.create({
  // blurry image placeholders are currently not considered valid AMP
  // hence it's recommended to setup paired AMP mode when enabling this feature.
  transformations: AmpOptimizer.TRANSFORMATIONS_PAIRED_AMP,
  blurredPlaceholders: true,
});
```

キャッシュは LRU キャッシュです。

### `AddMandatoryTags`

必須 AMP タグを追加します。[AMP のスタートガイド](https://amp.dev/ja/documentation/guides-and-tutorials/start/create/basic_markup/?format=websites)にあるようなタグが追加されると考えればいいです。後述しますが、あえて Invalid AMP にしたい場合は要注意です。

### `AddTransformedFlag`

html 要素に `transformed="self;v=1"` 属性を追加します。これは AMP Validator に AMP Optimizer で変換した html であることを伝えるためのものです。

AMP Optimizer が登場した当初は AMP Optimizer で処理した html は AMP Valid ではありませんでした。現在は 変換後の html も AMP Valid です。

### `AmpBoilerplateTransformer`

`ServerSideRendering` が適用されている場合、`<style amp-runtime>` が存在する。このタグに `i-amphtml-version` 属性を追加して、タグ内に https://cdn.ampproject.org/v0.css をインライン展開する。

`ServerSideRendering` に依存しているので、Transformer で指定するときは `ServerSideRendering` より後に書く。

### `AmpScriptCsp`

amp-script に CSP を追加する。meta タグが追加される。

### `AutoExtensionImporter`

AMP コンポーネントのタグを走査して、足りてない AMP extension (script タグ)を自動的に追加します。

### `GoogleFontsPreconnect`

Google フォントが使われていたら、preconnect 用のコードを挿入します。`ReorderHeadTransformer` の後に指定する必要があります。

href が https://fonts.googleapis.com ではじまる link タグがあれば、`<link rel="dns-prefetch preconnect" href="https://fonts.gstatic.com" crossorigin="">` を head 内の viewport meta タグの次に挿入します。`https://fonts.gstatic.com` は Google フォントの CSS 内に書かれているフォント URL のオリジンです。

### `Markdown`

Markdown という名前がついていますが、この Transformer は `<img>` タグを `amp-img` または `amp-anim` タグに変換するだけです。`ServerSideRendering` より前に指定します。

典型的変換フローは以下のようになります。

```
README.md => HTML => AMP Optimizer => valid AMP
```

md ファイルから html への変換は [markdown-it](https://github.com/markdown-it/markdown-it) 等を使う必要があります。

### `MinifyHtml`

html の最小化をする。インラインの JSON や amp-script も含めて最小化されます。

### `OptimizeImages`

srcset の変換関数をもとに `amp-img` の srcset 属性を生成します。指定された width や画面幅をもとに srcset を計算しているようです。`PreloadHeroImage` の前に指定する必要があります。

### `PreloadHeroImage`

ヒーロー画像の `img` タグをサーバーサイドレンダリングします。サーバーサイドレンダリングすることにより AMP ランタイムがロードされる前に画像を表示することができます。`img` タグは通常 `amp-img` によりクライアントサイドでレンダリングされます。

ヒーロー画像は自動検出されるか、`data-hero`属性で明示的に指定したものが採用されます。

### `PreloadImages`

`PreloadHeroImage` のプロトタイプみたいな位置づけに見えます。機能が被っているので、積極的に使われてないかも。

### `PruneDuplicateResourceHints`

Resource Hints （`dns-prefetch`、`preconnect`、`prefetch`、`preload`、`prerender`）の重複をなくします。`ReorderHeadTransformer` の後に指定する必要があります。

### `RemoveAmpAttribute`

html タグの AMP 属性 (`amp`、 `⚡`、 `⚡4ads`、 `amp4ads`、 `⚡4email`、 `amp4email`) を削除します。最小で Invalid AMP を作ることができます。限りなく Valid AMP で開発したいが、AMP Cache には載せたくないという用途に使えます。

### `RemoveCspNonce`

script タグの CSP nonce を削除します。

### `ReorderHeadTransformer`

head 内のタグをソートします。`ServerSideRendering` の後に指定します。

以下のようにタグをソートする

- (0) `<meta charset>`
- (1) `<style amp-runtime>`
- (2) `<meta charset>` 以外の `<meta>`
- (3) AMP ランタイム JS (`<script>`)
- (4) render delaying な `<script>`
- (5) extension の `<script>`
- (6) favicon の `<link>`
- (7) resource hints の `<link>`
- (8) `<link rel=stylesheet>`
- (9) `<style amp-custom>`
- (10) AMP に許可されてる他のタグ
- (11) AMP boilerplate

### `RewriteAmpUrls`

AMP ランタイムの URL を書き換える。`ReorderHeadTransformer` の後に指定する。

ex. `https://cdn.ampproject.org/v0.js` を
`https://cdn.ampproject.org/rtv/[ランタイムバージョン]/v0.js` に変換

使えるオプション

- `ampRuntimeVersion`: ランタイムバージョンを指定 ex. `001515617716922`
- `ampUrlPrefix`: ランタイムの URL プレフィックスを指定。ランタイムのセルフホスティングで使える
- `lts`: true で LTS バージョンになる。 `ampRuntimeVersion` と `ampUrlPrefix` とは非互換

### `SeparateKeyframes`

CSS の keyframes、 media、 supports を `amp-custom` の style タグから `amp-keyframes` の style タグに分離する。 `amp-keyframes` は body の最後に追加。

### `ServerSideRendering`

`sizes` 属性をメディアクエリに変換する `ApplyCommonAttributes` と `layout` 属性を処理する `ApplyLayout` があります。 AMP boilerplate を削除する処理もこの Transformer に含まれています。AMP Boilerplate が削除されると html タグに `i-amphtml-no-boilerplate` が付きます。 AMP Boilerplate については後述します。

#### `ApplyCommonAttributes`
`sizes` 属性を CSS のメディアクエリに変換する。

#### `ApplyLayout`

AMP コンポーネントの layout、 width、 および height 属性をもとにレイアウトを確定させます。AMP コンポーネントが現在サポートしている layout 属性は 8種類 (`nodisplay`、 `fixed`、 `fixed-height`、`responsive`、 `container`、 `fill`、 `flex-item`、 `intrinsic`) です。 layout 属性の種類と width、height 属性の値をもとに実際の width、height を計算して、該当要素の style 属性に追加します。このとき layout 属性がある要素に `i-amphtml-layout-[layout の属性値]` 属性がつけれられます。layout 属性値が `fixed`、 `fixed-height`、`responsive`、 `flex-item`、 `intrinsic` の場合、`i-amphtml-layout-size-defined` 属性も付与されます。`nodisplay`、`container`、 `fill` の３種類はサイズを定義する layout ではないので付与されないのだと思います。

## Best Practices

### AMP Boilerplate を削除する

AMP Boilerplate とは `<style amp-boilerplate>` のことです (ref. https://amp.dev/ja/documentation/guides-and-tutorials/start/create/basic_markup/?format=websites )。ここに書かれているスタイルは、最大 8秒間画面を真っ白 (body の visibility を hidden) にするものです。AMP ランタイムである `v0.js` がロードされて、実行されると `visibility: hidden` が解除されて画面が見えるようになります。つまり、ランタイムが実行されるまでは何も表示されないのでパフォーマンス面で懸念があります。しかし、AMP は JavaScript が実行されないと UI のレイアウトが固定されないので必要なスタイルであると言えます。

このパフォーマンス劣化を解決するために、AmpOptimizer のサーバーサイドレンダリングが使えます。 `ServerSideRendering` Transformer を使って、レイアウトをサーバーサイドで確定しておきます。すると、AMP Boilerplate のタグは不要になるので削除することができます。この機能には制限があって、以下のコンポーネントが使われていると AMP Boilerplate を削除することができなくなります。

- `amp-experiment`
- `amp-story`
- `amp-dynamic-css-classes`

### 可能であれば静的ページ生成する

AMP Optimizer の処理は計算コストが高く、リクエストごとに都度最適化していてはサーバーの負荷が高くなり、パフォーマンスも悪くなります。可能であればビルド時に静的ページを生成しておきます。

### CDN でキャッシュする

静的ページ生成とも関連しますが、可能であれば最適化した html を CDN でキャッシュさせます。AMP Cache も同様の方式です。AMP Optimizer は最適でも CDN キャッシュ前提の処理をしていると考えた方がいいと思います。

## Experimental Features

### AMP ランタイムのバージョン固定

`ampRuntimeVersion` オプションでランタイムのバージョンを固定することができます。変換すると以下のコードが、

```
https://cdn.ampproject.org/v0.js
```

以下のように変換されます。

```
https://cdn.ampproject.org/rtv/001515617716922/v0.js
```

AMP の更新の影響を受けたくないときに使えます。`lts` オプションと同時に使うことはできません。

```js
const ampOptimizer = require('@ampproject/toolbox-optimizer');
const ampRuntimeVersion = await runtimeVersion.currentVersion();

// The input string
const originalHtml = `
<!doctype html>
<html ⚡>
...
`

// Additional options can be passed as the second argument
const optimizedHtml = await ampOptimizer.transformHtml(originalHtml, {
  ampUrl: 'canonical.amp.html',
  ampRuntimeVersion: ampRuntimeVersion
});

console.log(optimizedHtml);
```

### AMP コンポーネントのセルフホスティング

AMP フレームワークやコンポーネントのインポートを `cdn.ampproject.org` とは別のドメインに書き換えることが可能です。

```js
const ampOptimizer = require('@ampproject/toolbox-optimizer');

// The input string
const originalHtml = `
<!doctype html>
<html ⚡>
...
`

// Additional options can be passed as the second argument
const optimizedHtml = await ampOptimizer.transformHtml(originalHtml, {
  ampUrl: 'canonical.amp.html',
  // this will rewrite https://cdn.ampproject.org/v0.js to /amp/v0.js
  ampUrlPrefix: '/amp'
});

console.log(optimizedHtml);
```

AMP CDN にも依存しなくなるので、AMP の影響を完全に排除することができそうです。

## さいごに

AMP Optimizer の Transformer コードをみていると、Web のパフォーマンスのベストプラクティスを学べるような気がします。

できればこの記事で AMP Optimizer の更新に対応していきたい。