病院なび AMP化への道

事の発端はGoogleが「2021年6月からCWV(コアウェブバイタル)の評価をランキングシステムに導入する」とアナウンスしたことでした。そのアナウンスにより「病院なび[^1]」でもCWVの向上が優先課題として上がり、その解決策として、病院なびのAMP化を実施致しました。

この記事では、なぜAMPにしたのか、AMPへの切り替えを、どのように進めたのかを記します。
(病院なびではRailsを使っているので、RailsでのAMP化方法となります)

[^1]: 「病院なび」は、全国23万件以上の医療機関・薬局・薬店を検索できる医療機関検索サイトです。https://byoinnavi.jp




# なぜAMP?
まず、AMP(Accelerated Mobile Pages)」とは、Googleが推進しているコンテンツを高速に表示されるための手法で、CWVに優れています。が、その分、制限も多いです。

> AMP はウェブサイトを高速かつユーザーファーストにし、収益化するシンプルかつ堅牢なフォーマットです。AMP は一般的なプラットフォームへの配布を実現し、運用コストと開発コストを削減することで、貴社のウェブ戦略に長期的な成功をもたらします。
引用: https://amp.dev/ja/about/websites/

> AMP Project には、デベロッパーが簡単に Core Web Vitals のしきい値を満たせるようにするという役割があります。
引用: https://developers-jp.googleblog.com/2021/02/core-web-vitals-amp.html


病院なびでのパフォーマンスのボトルネックはフロント側ということは分かっていました。フロント側をより詳細に調べて、CWVを少しづつ改善していていくという方法もありましたが、調査と改善を繰り返すのはどうしても時間がかかります。今回は2021年6月という期限があったのと、一部のページは既にAMPにしていたため、残りの主要ページをAMP化して、CWVの改善を目指すことにしました。


# AMPの制限と課題
上述の通り、AMPではコンテンツを高速で表示するために、いくつかの制限を強いられます。一部列挙すると

* 基本、JavaScriptは利用不可。(amp-scriptでは使えるが、使える機能は限られている)
* CSSはinlineのみ。また、容量も75KBまで。
* imageタグなど特定のHTMLタグは AMP用のタグ(<amp-img>など)に置換する必要がある

また、AMPに切り替えるにしても、一挙にAMPにするのはリスクが高いので、既存ページ(以降、非AMPと記載)とAMPでのABテストを実施しながら、進めることとします。正しくABテストが出来るように非AMP/AMPのレイアウトは可能な限り同じにする必要があります。
つまり、非AMPの見た目のまま、中身はAMPで構成されているHTMLを作成する必要がありました。

そこで、非AMPとAMPを切り分ける仕組み作りから始めました

# 非AMP? それとも AMP?
まず第一歩として、非AMP/AMPの判定処理を作成する必要があります。病院なびでは、POROでAMP判定用Classを作成致しました。


```ruby
class Amp
  def initialize(user, page)
    @user  = user
    @page  = page
  end

  # AMP対象のユーザーか
  def user?
     # 基本はスマホユーザーの場合のみ true を返す
     # それに加えてABテスト時では、cookie による判定も加えて非AMP/AMPの切り分け制御を実施
  end

  # AMP対象のページか
  def page?
    # 基本はAMP対象のページの場合 true を返す
    # それに加えてABテスト時では、東京都のページのみなどの判定を加えて制御する
  end

  # AMPとして表示するか
  def layout?
    user? && page?
  end
end
```

このAMPの判定条件を使って、非AMP/AMPの切り分けの仕組みを作っていきます。切り分けは大きく2パターンあり「テンプレートによる切り分け」　と「メソッドによる切り分け」です。

# テンプレートによる切り分け

テンプレートによる切り分けは簡単で、Railsの機能を使います。

Railsで render 前に以下の実装を加えます

```ruby
lookup_context.formats = %i[amp html] if amp.layout?
```

このようにすれば、AMPの場合のみ、Railsがテンプレートを探す際に、まずは`*.amp.haml`を検索し、なければ、`*.html.haml`を使うという動作が可能となります。

制限の箇所で記述した通り、AMPは外部からのCSSは読み込めないので、以下のようにテンプレートを分けます

**非AMP**

```haml:_css.html.haml
= stylesheet_link_tag(*stylesheets)
```
**AMP**


```haml:_css.amp.haml
-#
  実際の記述を説明部分だけ抽出して分かりやすく書き直してます
  以下の実装は、定型だったり、 Sassのコンパイルを実施しているので、実際はキャッシュしています
:ruby
  # AMP に必要な Boilerplate
  # https://github.com/ampproject/amphtml/blob/master/spec/amp-boilerplate.md
  style = <<~STYLE.lines.map(&:strip).join
    body{-webkit-animation:-amp-start 8s steps(1,end) 0s 1 normal both;
         -moz-animation:-amp-start 8s steps(1,end) 0s 1 normal both;
         -ms-animation:-amp-start 8s steps(1,end) 0s 1 normal both;
         animation:-amp-start 8s steps(1,end) 0s 1 normal both}
    @-webkit-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
    @-moz-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
    @-ms-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
    @-o-keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
    @keyframes -amp-start{from{visibility:hidden}to{visibility:visible}}
  STYLE

%style{ "amp-boilerplate": "" }= style
%noscript
  %style{ "amp-boilerplate": "" }
    body{-webkit-animation:none;-moz-animation:none;-ms-animation:none;animation:none}

-# AMPではCSSはインラインでしか受け付けていないため、対象CSSを header に書き込む
%style{ "amp-custom": "" }
  - stylesheets.each do |stylesheet|
    -#
      SCSS gemを利用して compress 化する
      compress は BOM が付与されているので外す
    !=  SassC::Engine.new(stylesheet, style: :compressed).render.remove_bom
```

呼び出し元では非AMP/AMPは気にする必要がなく、以下のようになります

```haml:application.html.haml
-# 非AMPの場合は _css.html.haml を AMPの場合は _css.amp.haml を読み込む
= render("layouts/css")
```

このように「テンプレートによる切り分け」は簡単なのですが、「メソッドによる切り分け
」はRailsを頼れないため、独自実装する必要があります。

# メソッドによる切り分け
病院なびでは`alias_method_suffix`という独自メソッドがあります。ここでは詳しく記載しないですが、昔、Railsにもあった、`alias_method_chain`( [Railsの実装参考](https://github.com/rails/rails/pull/27035/commits/7c848e6dd493ff236d33a0410a92f4c3e5cc3c7f))と類似したメソッドです。実態は以下のように`suffixed?`の結果により呼び出すメソッドを切り分けできます。

```ruby
class Foo
  class_attribute :suffixed, default: false

  def foo
    :foo
  end
  alias_method_suffix :foo, :bar, if: :suffixed?

  def foo_with_bar
    # _without_{suffix} で元のメソッドを呼べる
    :"#{foo_without_bar}_with_bar"
  end
end

Foo.new.foo #=> :foo

foo = Foo.new
foo.suffixed = true
foo.foo #=> :foo_with_bar
```

これを用いると、良い感じに非AMP/AMPのメソッドを切り分けできそうです。
(`amp_layout?`は`amp.layout?`のエイリアスメソッドです)

```ruby
# amp_layout? が true の場合、*_with_amp メソッドを呼び出す
module AmpMethod
  def amp_method(target_method)
    alias_method_suffix(target_method, :amp, if: :amp_layout?)
  end
end
```
はい、出来ました。では、これをどのように使うかを`image_tag`を用いて見ていきます。AMPの場合、通常の`<img>`は使えず`<amp-img>`を使う必要がありました。これを対応した実装は以下の通りです。

```ruby
module ActionView
   module Helpers
     module AssetTagHelper
      # amp_method が使えるようにモジュールを extend する
      extend AmpMethod

      amp_method :image_tag # amp の場合は image_tag_with_amp を呼ぶ

      # AMP用
      def image_tag_with_amp(source, options = {})
        # 幅・高さが未指定の場合は scriptエラーとなるため、未指定でも可能な layout='fill' とする
        options[:layout] = :fill unless options[:width] || options[:height]
        # 空タグ <amp-img /> ではなく、内容の無いタグ <amp-img></amp-img> にするために tag.public_send している
        # https://amp.dev/ja/documentation/components/amp-img/
        tag.public_send('amp-img', nil, options.merge(src: source))
      end
    end
  end
end
```

こちらも「テンプレートによる切り分け」と同様に呼び出し元では非AMP/AMPを意識する必要はありません。

```ruby
# 非AMPの場合は <img> で、AMPの場合は <amp-img> で作成される
= image_tag("amp.jpg", alt: "amp")
```

# そしてAMP化へ

仕組みができたので、もう勝ったも同然です。AMPのページでは自動で、CSSはインラインとなり、画像は`amp-img`で作成されます。呼び出し側は何も変える必要はありません。

AMP化したいページを `amp.layout? => true`として、ページ表示してみましょう。URLに `#development=1` を付与すると、ページロードと共にAMPの`validation`が実行されます。ブラウザのコンソールに`AMP validation successful.`と出れば、AMP化成功となります。

ただ、現状ではエラーが出まくると思います。JSを使っていたり、他の制限に引っかかっていることもあります。ただ、仕組みは出来上がっているので、あとは粛々と対応していくだけです。

まずは、エラーとなっている箇所を判定し、それを「テンプレートによる切り分け」or「メソッドによる切り分け」で切り分けます。この時、AMP側は何も返さないとします。(テンプレートであれば、空ファイル、メソッドであれば`nil`を返す)

これを繰り返し、`AMP validation successful.`となる状態に持っていきます。

その後に先ほど切り出したAMP側を今度は一つづつ対応していきます。対応ごとに`AMP validation successful.`となることを確認して下さい。全て対応すれば、晴れてAMP化達成となります。

この進め方の良い所は一度切り分けを作ってしまえば、あとは自動で切り替わるところです。
病院なびでは、「詳細ページ」と「口コミページ」という構成は似ているが、表示している内容が異なるページがあります。まずは詳細ページをAMP化したのですが、その後の口コミページのAMP化は `amp.layout?`を`true`にすれば、AMP化することができました。

# AMP化してみて

当初の目的としてのCWVの改善は叶ったかと思います。6月に主要ページはAMPにして、Google Search Console上での不良もしくは要改善のページは、ほぼ良好なページとなりました。2021年12月では96.5%が良好となっています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1265134/e26b4934-6f92-baba-e9a8-38608778af32.png)

ただ、やはり制限が厳しいのは事実です。現状でも「その仕様はAMPでは実現が難しい」という場面があります。特にJSが使えないので、Google以外のツール(アナリティクスなど)を使っている人は、それがAMPに対応しているかの確認が必要です。ちなみに`GoogleAnalytics4`は2021年12月時点でも、AMP非対応です。
参考: https://github.com/ampproject/amphtml/issues/24621

また、開発が活発に行われているReactとかと比較すると、開発スピードがどうしても気になります。私は東京で開催されたAMPカンファレンス `AMP Conf 2019` に参加したのですが、その時からAMPの新機能はそれほど増えていないように思います。そのカンファレンスで Bento AMP を鋭利作成中と発表しておりました。これはAMP以外でもAMP のコンポーネントが使えるというもので、最近、正式公開になったと知りました。カンファレンスから2年で、大きな機能リリースは Bento AMP のみと思います。
参考: https://www.suzukikenichi.com/blog/google-officially-launches-bento-amp/

病院なびでは当初のCWV改善という目的は達成できましたが、AMPの制限が足枷になっているのも事実です。また、CWVに関しても、まだ、改善の余地はありますので、他のツールを検討しながらも、よりよりサービスを目指していければと思います。