RACCOON TECH BLOG

株式会社ラクーンホールディングスのエンジニア/デザイナーから技術情報をはじめ、世の中のためになることや社内のことなどを発信してます。

rel=”preload”を極めるために必要な2種類のプリロード機能

こんにちは、羽山です。
本稿はWebパフォーマンス Advent Calendar 2019の13日目に参加させていただきました。

フロントエンドのパフォーマンスチューニングのシリーズ記事で、今回は2種類のプリロード機能に焦点を当ててリソース読み込みの最適化について解説します。

はじめに

まずはブラウザが持つ2種類のプリロード機能をおさらいしましょう。

1つ目はHTMLの仕様として定義されている <link rel="preload"> です。近い将来必要になるリソースを明示することでブラウザは事前に該当リソースを読み込みます。おそらくWebプログラマがプリロード機能と聞いてまず思い浮かべるのはこれだと思います。2019年12月時点では Safari, Chrome, Opera など、一部のブラウザが対応しています。以降 rel="preload" と記載します。

2つ目はHTMLの仕様として定義されたものではなく、ブラウザに実装された暗黙で動作するパフォーマンス最適化の機能です。多くのウェブページはこの機能のおかげでパフォーマンスが知らず知らずのうちに最適化されていますが、あくまでブラウザ内の機能なので明確な呼び名はなく、Lookahead Downloader や Preload Scanner などと呼ばれたりします。以降 Preload Scanner と記載します。

rel="preload" についての解説ページは数多くありますが、実際にどういう場面で使えば効果的なのかを理解できなかったことはありませんか?それは rel="preload" とは切っても切り離せない機能である Preload Scanner の存在を踏まえずに書かれていることが多いためです。そして rel="preload" は理解不足のまま利用すると逆にパフォーマンスが低下する恐れもあるので、その機能に頼りすぎず、 Preload Scanner の有効活用を学ぶことも本稿の主題です。

なお、 Preload Scanner についてはフロントエンドのパフォーマンスチューニング記事のリソースのプリロード機能によるパフォーマンスへの影響の章でも解説しています。一読しておくと理解が深まりますが、読んでいない方でも分かるように構成しています。

Preload Scannerとは

Preload Scanner がどういう機能かを簡単に説明します。すでに分かっている方は次の章まで読み飛ばしても問題ありません。

HTML文書がネットワークから転送されるとパースされてDOM構築が始まります。DOM構築中に発見したリソースはブラウザが決定する優先度に基づいてネットワークから読み込まれます。しかしDOM構築は以下の状況ではブロックされて停止します。

この状況で活躍するのが Preload Scanner で、軽量パーサーがDOM未構築の部分のHTMLをパースして読み込むべきリソースを発見して事前に読み込んでおいてくれます。

わかりやすい例を1つ挙げます。

<script src="path_to_1.js"></script>
<script src="path_to_2.js"></script>
<link href="path_to.css" rel="stylesheet">

上記のような async ではない <script> タグがあると、 path_to_1.js の読み込みでDOM構築がブロックされます。
そのDOM構築が停止している間にpath_to_2.jspath_to.css を読み込んでおいてくれるのが Preload Scanner です。

しかし Preload Scanner の軽量パーサーには制限があって発見できないリソースがいくつもあります。それが本稿の主題である2つのプリロード機能の使い分けです。

Preload Scanner が読み込めないリソース

Preload Scanner はHTML文書をパースしますが、 JavaScriptコードを実行しません 。そのため <script><link> タグを動的に生成する Script-Inject や CSS-Inject という手法を利用したリソースは読み込めません。(以下例)

<script>
(function() {
  var tag = document.createElement('script');
  tag.src = 'path_to.js';
  document.head.appendChild(tag);
})();
</script>

JavaScriptコードを実行すれば path_to.js が必要だと分かりますが、Preload Scanner では発見できません。

ブラウザごとの対応状況

Preload Scanner はブラウザやバージョンによって動作が異なるので、主要ブラウザでそれぞれ読み込めるリソースを調べてみました。

試してみたのは以下、10種類のリソースです。

  1. <script src="path_to_script">
    • Preload Scanner によって同期スクリプトが読み込まれるかの確認
  2. <script src="path_to_script" async>
    • Preload Scanner によって非同期スクリプトが読み込まれるかの確認
  3. <link rel="stylesheet" href="path_to_style">
    • Preload Scanner によって <link>タグのスタイルシートが読み込まれるかの確認
  4. <style> @import url('path_to_style'); </style>
    • HTMLドキュメントの <style> タグ内に直接指定した @import が Preload Scanner によって読み込まれるかの確認
  5. <style> @import url('path_to_style_1'); @import url('path_to_style_2') </style>
    • HTMLドキュメントの <style> タグ内に直接指定した複数の @import が Preload Scanner によってすべて読み込まれるかの確認
  6. <link rel="stylesheet" href="path_to_style"> - @import url('path_to_nested_style');
    • 外部スタイルシート内の @import が Preload Scanner によって読み込まれるかの確認
  7. <link rel="preload" href="path_to_style" as="style">
    • DOM構築に時間がかかる位置に rel="preload" を入れて Preload Scanner によって読み込まれるかの確認(rel=“preload” への対応も必要)
  8. <img src="path_to_image">
    • Preload Scanner によって画像ファイルが読み込まれるかの確認
  9. <iframe src="path_to_html">
    • Preload Scanner によって <iframe> のドキュメントが読み込まれるかの確認
  10. <style> @font-face { src: url('path_to_font') } </style>
    • Preload Scanner によって @font-face のWebフォントが読み込まれるかの確認

動作確認をしたブラウザは以下です。

Preload Scanner の対応状況(2019年12月 調査)

Safari13 IE11 Edge44 Firefox70 Firefox71 Chrome78 Opera65
<script>
<script async>
<link rel="stylesheet">
<style> @import × ×
<style> @import @import × × × ×
<link rel="stylesheet"> @import × × × × ×
<link rel="preload"> × × × ×
<img> × ×
<iframe> × × × × × × ×
<style> @font-face × × × × × × ×

Safari, Chrome はデスクトップ版とスマホ版で確認しましたが共に動作の違いはありませんでした。

Firefox のバージョンが2つあるのは本稿執筆中にバージョンアップがあったので試してみたところ、タイムリーに @import の Preload Scanner に機能改善が見られたため参考までに両方載せました。

項目数で最も多くの種類のリソースを Preload Scanner が読み込めたのは Safari で、次いで IE11, Chrome, Firefox71, Opera が並ぶ結果となりました。IE11は意外な健闘を見せていますが、Preload Scanner にあたる機能を最初に導入したブラウザがIE8である事を考えると妥当な結果とも言えます。

どのブラウザも共通して対応しているのは <script> <script async> <link rel="stylesheet"> の3種類です。しかし、@import を完全に読み込めるブラウザは半数ほどだったり、Firefox が画像に対応していないなど意外と細かい罠が多いようです。

また、 <iframe><style> @font-face に対応しているブラウザは現時点ではありませんでした。

余談ですが、IE11はF5などでリロードしてページを開くと Preload Scanner が完全に無効化されるようです。今となっては必要性の低い配慮ですが、Preload Scanner を初めて導入した際に互換性の問題でうまく動作しなくなるサイトを考慮した結果かと推測されます。

今回それぞれブラウザの動作を調べるにあたって、Preload Scanner がそれぞれどんな種類のリソースに対応しているかを確認できる簡易ツールを作って検証しました。Githubに公開しているので興味がある方は参考にしてください。

2種類のプリロード機能の使い分け

まず結論を先に書きます。

パフォーマンス改善についてはプログレッシブエンハンスメントを検討することをお勧めします。

Preload Scanner や rel="preload" は対応するリソースの種類・ブラウザの対応可否の組合せが多岐にわたります。
全ての環境に最適化することを目指すのではなく、対応ブラウザで閲覧した場合は読み込みが速くなり、それ以外ではページの動作には影響ないものの読み込みが多少遅くなるという考え方で、無理に独自チューニングを実装するよりもブラウザの進化に任せた方がベターなことが多いです。
実際に本稿執筆中にも Firefox の Preload Scanner に機能改善がみられたように進化の速い分野なので近い将来、自動的に対応ブラウザが増えていくはずです。

ではどういうシーンで Preload Scanner や rel="preload" を利用したら良いのか具体的なケースで考えてみましょう。

@import の利用を控える

@import に関連する Preload Scanner のテストは以下の3パターンを試しました。

これらを全て問題なく読み込めるブラウザは、確認した中では Firefox71 だけでした。
@import の利用はなるべく控えて <link rel="stylesheet"> とすれば、より多くの環境で Preload Scanner が読み込んでくれるようになります。

Google Tag Manager

<!-- Google Tag Manager -->
<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
})(window,document,'script','dataLayer','GTM-XXXXXX');</script>
<!-- End Google Tag Manager -->

Google Tag Manager などの外部サービスを利用する場合はコードスニペットの形で提供されるケースが多々あります。これは動的に <script> タグを生成するいわゆる Script-Inject のJavaScriptコードで、実行の結果 <script async src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXX"></script> というタグが生成されます。

このケースでは以下の rel="preload" タグを設置することで、JavaScriptコードの実行前に読み込みを開始できます。

<link rel="preload" as="script" href="https://www.googletagmanager.com/gtm.js?id=GTM-XXXXXX">

Script-Injectコード

Google Tag Manager のケースを汎化して書くと、以下のような Script-Inject のコードとなります。

<script>
(function() {
  var tag = document.createElement('script');
  tag.src = 'path_to.js';
  document.head.appendChild(tag);
})();
</script>

これらは可能なら Preload Scanner で読み込める形にしましょう。
つまり、まず検討するのは <script async src="path_to.js"> のような非同期タグへの変換で、そうすれば Preload Scanner に任せられます。
それが難しい場合は <link rel="preload" href="path_to.js" as="script"> を追加することで事前に読み込むことができます。

Webフォント

CSSの font-display によってWebフォントの読み込み過程の制御が可能になりつつありますが、Webフォントの読み込みがボトルネックになりやすいことには変わりがありません。できれば Preload Scanner が読み込んでくれれば良いのですが、残念ながら @font-face に対応したブラウザは現時点ではありません。

@font-face {
    font-family: "original_font";
    src: url('font/trim_4s.woff2') format('woff2'),
        url('font/trim_4s.woff')  format('woff'),
        url('font/trim_4s.ttf')   format('truetype'),
        url('font/trim_4s.eot')  format('embedded-opentype');
}

一見すると Preload Scanner が @font-face を探して読み込むことは可能そうにも思えますが、Webフォントは実際に該当フォントを利用したタイミングで読み込まれるのでDOMとCSSOM構築が不可欠です。そのためDOM構築を行わない Preload Scanner が発見することは不可能ですし、今後も難しいでしょう。

Webフォントはなにもしなければ後半に読み込まれがちなので rel="preload" で事前に読み込みましょう。
この場合は <link rel="preload" href="font/trim_4s.woff2" as="font" type="font/woff2" crossorigin> を追加します。

ただしWebフォントの rel="preload" について注意点が2つあります。

1点目は同一オリジンに配置したフォントデータの読み込みでも rel="preload" に crossorigin または crossorigin="anonymous" を指定する必要があります。詳しい説明は省きますがWebフォントは必ずCORS(Cross-Origin Resource Sharing)の匿名モードで取得するよう仕様で定められていて、それは同一オリジンの場合も含むためです。crossorigin を省略したり crossorigin="use-credentials" を指定したら rel="preload" で先読み自体はされるものの、そのデータは利用されずに二重で取得してしまいます。

2点目はブラウザによって対応しているフォント形式が異なる点です。@font-face src には複数形式のフォントデータを指定してブラウザは先頭から順番に対応している形式を探します。woff2, woff の順番で指定した場合に woff形式のフォントデータを先読みしていても woff2形式に対応しているブラウザは woff2形式のフォントデータを改めて取得します。逆もしかりで woff2形式に対応していないブラウザに woff2形式のフォントデータを先読みさせても転送されたフォントデータは使われません。rel="preload" に対応しているブラウザはほとんど woff2形式に対応しているため type="font/woff2" 属性を指定するのと、@font-face src では woff2形式を優先して指定しましょう。

rel="preload" を利用すべきでないケース

いくつか rel="preload" を利用するシーンを挙げましたが、逆に利用するべきでないケースを考えてみます。

rel="preload" は preconnectprefetch などのリソースヒントと異なり、ブラウザに対する強制的な指示として作用するので、ページのレンダリングに影響のないリソースでも rel="preload" で指定されると優先して取得してしまいます。

クライアント側のCPUやネットワークリソースには限りがあるので、結果としてより優先度が高いリソースの処理を遅らせる可能性があります。

例えば Google Analytics のコードスニペットを考えてみます。そのサイトにとってはいち早く Google Analytics へデータ送信することが重要なら rel="preload" を利用しても良いのですが、通常は Google Analytics を優先して読み込む理由はありません。

このように rel="preload" は強力な指示なので、ページ内のリソースを何でもかんでも指定したら良いわけではありません。無理に rel="preload" を利用するのではなく、Preload Scanner が読み込める書き方にして適切な読み込み優先度をブラウザに判断してもらった方が良い結果になることが多いと言えます。

まとめ

今回は2種類のプリロードを深掘りして考えてみました。

仕様化されている rel="preload" はご存じの方が多いと思われますが、動作やソースコードから観測するしかない Preload Scanner は働き者のわりには決して認知度が高くないのではないでしょうか。
しかし Preload Scanner の動作をしっかり把握していないと、不適切な rel="preload" 指定で逆にパフォーマンスに悪影響となることもあります。しっかり動作を把握してブラウザが効率的なリソース読み込みをできるサイトを提供しましょう。

また Preload Scanner は時代と共にどんどん進化するので、定期的にその時の常識をアップデートする必要があります。今回はPreload Scanner の動作確認ツールも用意しているので。状況のキャッチアップに役に立てば幸いです。

一緒にラクーンのサービスを作りませんか? 採用情報を詳しく見る

関連記事

運営会社:株式会社ラクーンホールディングス(c)2000 RACCOON HOLDINGS, Inc