rehype-plugin-auto-resolve-layout-shift を利用した Markdown ドキュメントにおける Layout Shift の解消

このブログは @nuxt/content で運用しており、執筆環境としては非常に快適に利用できている一方で、Markdown ベースの宿命として、Markdown 記法で画像を貼り付けている限り Layout Shift が発生するという課題が存在していました。

暫定的な対処としてしばらく <img> で置き換えてきましたが、根本的に対処するため、連休を活かして rehype-plugin-auto-resolve-layout-shift を開発しました。

この記事では、かんたんにその報告と紹介をしたいと思います。

Markdown の Layout Shift について

Markdown は # で h1~h6 を表現できたり、 [text](href) でリンクを貼れたり、 ![alt](src) で画像が表示できたり何かと便利なフォーマットである一方、そのパーサやトランスパイラには、現代的な設定がデフォルトではついていないことがほとんどです。

例えば今なら <a> リンクの href が外部なら自動的に rel="noopener" が付与されてほしいですし、<img> には自動で loading="lazy" が付与されていてほしいところではないでしょうか。

以前もこういった課題を解消するために rehype-plugin-image-native-lazy-loading を開発して Lazy Loading を実現するなどしていましたが、冒頭に言及したとおり、 Layout Shift は改善できていませんでした。

Layout Shift の解消は Google の Core Web Vitals にも含まれている重要指標の一つですし、可能な限り対処しておくべき要件です。

ただ一方で、 Markdown 記法の気軽さも失いたくはないため、 Node.js のデファクトの Markdown/HTML ライブラリである rehype のプラグインとして開発・提供することとしました。

rehype-plugin-auto-resolve-layout-shift について

https://github.com/potato4d/rehype-plugin-auto-resolve-layout-shift

rehype-plugin-image-native-lazy-loading は、事前に画像のバイナリを取得することによって、画像の width/height をビルド時に計算するライブラリです。

Markdown が HTML に変換されるタイミングで画像のの指し示す先へとアクセスし、取得した結果に応じた width/height 属性を付与した <img> へと変換します。

rehype プラグインであるため、remark/rehype を利用しているエコシステム(や、それを間接的に利用している markdown-it などのエコシステム)であれば利用できるため、Nuxt.js や Gatsby などの一般的な SSG 全般で利用可能です。

なお、現在このブログ( https://d.potato4d.me/ )では全ての記事においてこのプラグインが適用されており、適切な width/height が設定されている状態となっています。

利用方法

とりあえずで導入できる ratio モードと、最大のパフォーマンスを得るための maxWidth モードがあります。ratio モードの場合は設定が不要であるため、ここでは推奨かつひと手間必要である maxWidth モードのみ紹介します。

maxWidth モードは、利用先のコンテナ幅を明示的に指定することで、アスペクト比を維持したままベストな解像度で画像を表示します。

有効化する際は、maxWidth にはコンテナ幅を設定し、type に 'maxWidth' を設定します。@nuxt/content の場合、 nuxt.config.js に以下のように記述します。

const rehypePlugins = [
  // plugins config,
]

if (process.env.NODE_ENV === 'production') {
  rehypePlugins.push(
    ['rehype-plugin-auto-resolve-layout-shift', { type: 'maxWidth', maxWidth: 768 }]
  )
}

export default {
  // ...
  content: {
    markdown: {
      rehypePlugins
    }
  },
  // ...
}

このように設定した場合、このプラグインは以下のような処理を行います。

1. width <= maxWidth の場合

この場合小さな画像を表示しているなどのケースが考えられるため、単純に元々の画像の width/height を尊重します。

例えば width=200,height=150 の場合などがこれに該当します。

2. width > maxWidth の場合

本題です。

Web の世界では基本的にコンテナ幅の width が指定されているはずであるため、このシチュエーションが意味することは、つまりはコンテナ幅を超過している状態であるということです。

例えば width=1600,height=900 の 16:9 の画像を想定します。

maxWidth は maxWidth/width を割り出して、 width/height にそのレートをかけます。つまりは width/height は以下の形となります。

  1. width = width * (maxWidth/width)
  2. height = height * (maxWidth/width)

今回の場合、 maxWidth/width = 0.48 なので、 width は 768 に、height は 432 となり、アスペクト比を維持した状態で最適なサイズへと変換されます。

実際に 768 での設定はこのブログで行っており、以下のように自動算出された値が属性値として挿入されています。

Image from Gyazo

利用上の注意

  1. 内部的にはネットワークアクセスを介して画像を取得しているため、多少処理に時間がかかります。

そのため、 minify などと同様に、プロダクションビルド時に限定して有効化することを推奨します。

  1. 対応フォーマットについて

内部的に Jimp に依存しているため、現状 Jimp の対応フォーマットに依存しています。需要があれば、より広いフォーマットの対応も行う予定です。

おわりに

Markdown 製のブログや SSG が身の回りで更に増えているので、この辺りのプラグインがより有効に使われると良いなと。

余談ですが、このプラグインを作るきっかけになったのは、以下のツイートがきっかけでした。

Image from Gyazo

Special thanks! @yamanoku さん