はじめに
ウェブページをブラウザで表示する際、背後では複雑なレンダリングプロセスが実行されています。
今回は、レンダリングプロセスの中の最初の工程である Loading (リソースの読み込み) のパフォーマンスを向上するためのテクニックについて、記事を書いていこうと思います。
関連記事
本記事に関連した記事も投稿したので、ぜひ読んでいただけると幸いです!
参考書
リソース取得の最適化が重要な理由
リソース読み込みの流れとして、ブラウザの URL アドレス欄に URL を入力すると、ブラウザは指定された URL の HTML を HTTP を通じて取得しパースします。
さらにパースされた HTML のドキュメント内に宣言されている画像や JavaScript ファイルや CSS ファイルなどのリソースも取得します。
たとえば外部 CSS ファイルは head 要素内に link
要素で、外部 JavaScript ファイルは script
要素で、画像なら img 要素で次のように宣言されます。
<link href="style.css" rel="stylesheet">
<script src="foobar.js"></script>
<img src="image.png" width="34" height="34">
このようにブラウザは、HTML ファイルから始まるリソースのグラフを構築しつつ、ネットワークプロトコルを通じてリソースを取得・パースしていきます。
そして、描画に必要なリソースが揃うと、後続のレンダリングプロセスが開始されます。
つまり、いかにリソースの取得を最適化できるかが重要です。
リソース読み込みの最適化のテクニック
ただ闇雲にパフォーマンスを向上させるための取り組みを行なっても、大した効果を得ることはできません。
まずは、「どのような方針で最適化するべきなのか」をお伝えしたのち、その方針に則った最適化テクニックをお伝えできればと思います。
修正方針
最適化をするにあたっての、念頭に置いておきたい観点(修正方針)は下記です。
- 読み込むリソースの大きさと数を減らす
- レンダリングをブロックする読み込みを減らす
- ブラウザとサーバー間の遅延を減らす
- ブラウザのキャッシュ機能を活用する
上記の観点に沿って、リソースの読み込みパフォーマンスを最適化するテクニックを紹介していきたいと思います。
1. HTML / CSS / JavaScript を最小化する
HTML, CSS, JS などのファイルは、通常改行やタブなどの不要なバイト(文字)を含みます。
これらの余計なバイト列は、専用のツールを利用することで取り除くことができ、結果的にリソースファイルサイズやウェブサーバーからのダウンロード時間を減らすことができます。
例えば、ビルドツールである Vite はプロダクションビルド (vite build
コマンド) を実行する際に、デフォルトで JavaScript、CSS、HTML などを最小化します。
ぜひ、そのほかにもリソースファイルを最小化するツールは存在するので、調べてみてください。
2. 適切な画像形式を選択する
画像ファイルには、その形式ごとに適した用途があります。
それぞれの用途に最適な画像を用いることでファイルサイズを削減できる可能性があります。
最近だと、AVIF や SVG が採用される機会が多く見受けられます。
反面で、GIF は、動画形式におき買われることが多くなっているため、今回は省いています。
形式 | 可逆 | 色数 | 透過 | 用途と特徴 |
---|---|---|---|---|
JPEG | 不可逆 | 約1670万 | 不可 | 写真、圧縮率が高く、広く使われる。 |
PNG-8 | 可逆 | 256 | 可 | 色数の少ないアイコン、ロゴなど。 |
PNG-24 | 可逆 | 約1670万 | 不可 | 透過を必要としないイラスト、画像など。 |
PNG-32 | 可逆 | 約1670万 | 可 | 透過を必要とするイラスト、画像など。 |
WEBP | 可逆・不可逆 | 約1670万 | 可 | ファイルサイズが小さく、透過が利用できる。最近のウェブで広く使われる。 |
AVIF | 可逆・不可逆 | 約1670万 | 可 | 次世代の画像フォーマット。高圧縮率で高品質な画像を提供し、透過も可能。 |
SVG | 可逆 | 無限 | 可 | ベクター形式。解像度に依存しないため、アイコンやロゴなどで多用される。 |
3. CSS の @import
を避ける
CSS では、外部 CSS ファイルを読み込みのに @import
文を利用できますが、読み込みのパフォーマンスを最適する場合は、使用を避けた方が良いです。
import を避けるべき理由
1. HTTP リクエストが増加して、パフォーマンス低下につながる
@import
文を使用すると、ブラウザはスタイルシートを順次読み込むため、HTTP リクエストが増加します。
例えば、style.css
に @import
文で他のスタイルシートを読み込むと、まず style.css
が読み込まれ、その後に @import
文で指定されたスタイルシートが個別にリクエストされます。
この順次読み込みは、ページのレンダリングを遅らせ、ユーザーに見えるコンテンツが表示されるまでの時間を延長する原因になります。
2. 最適化ツールの効果が減少
多くのビルドツールや最適化ツール(例: Webpack、Vite)は、CSS ファイルをまとめて最適化することを前提としていますが、@import
文を使うとこれらのツールが効果的に機能しない場合があります。(外部リソースに依存した動作となってしまうため)
これにより、最終的な CSS ファイルが大きくなったりすることで、パフォーマンスの悪い状態になりかねません。
解決策
@import
文を使うのではなく、ビルドプロセスで CSS を一つにまとめる方法がおすすめです。
例えば、Sass
などの CSS プリプロセッサを使用することで、複数の CSS ファイルを一つに結合し、@import
文を避けることができます。
※ Vite は、デフォルトで Sass をサポートしているので、特段の設定は不要(エントリーポイントを読み込むことで、自動的に一つの CSS ファイルに読み込まれる)
4. JavaScript の同期的な読み込みを避ける
通常の script
要素による JavaScript ファイルの読み込みは、ドキュメントのパースをブロックします。
JavaScript を読み込んだ後、そのコードの実行が終わって初めてドキュメントのパースが再開されます。
これは script
要素の中にインラインでコードを記述する場合でも、外部 JavaScript ファイルを読み込む場合でも同様です。
このブロッキングを避けるためには、JavaScript を非同期で読み込む必要(それ以外にも方法はあるが、推奨されている)があります。
非同期での読み込みを実現するには、defer
または async
属性を script
タグに付与します。
<script defer src="foo.js"></script>
<script async src="bar.js"></script>
非同期属性の使い分け
-
defer属性
- スクリプトは、HTML の解析が完了するまで実行が遅延されます
- そのため、DOM 構造が完全に構築された後にスクリプトが実行されます
-
async属性
- スクリプトは、他のファイルの読み込みや HTML の解析と並行して読み込まれ、ダウンロードが完了したらすぐに実行されます
5. リソースを事前読み込みしておく
link
要素は、ブラウザに対してあらかじめまだ読み込んでいないリソースに対して、前処理で読み込んでおくように指示を出すことができます。
読み込み時間そのものの短縮には大きく寄与しないかもしれませんが、ページ遷移の速度を改善するといった効果は見込めます。
リソースの事前読み込みの手段としては、以下がありますが、今回はプリレンダリングに関してご紹介しようと思います。
- DNS プリフェッチ
- リソースのプリフェッチ
- コネクションの接続
- プリレンダリング
- 今回は、こちらを紹介
プリレンダリングとは
プリレンダリングとは、ユーザーが次にアクセスする可能性のあるページを事前にレンダリングしておく技術です。
プリレンダリングのメリット
- 高速なページ遷移
- ユーザーが次にアクセスするページは、すでにレンダリングされているため、瞬時に表示されます
- SEOへの高評価(ユーザー観点でのパフォーマンス向上とは別ではありますが)
- 特に、検索エンジンのクローラがページをインデックスする際、すでにレンダリングされたページを取得するため、インデックス速度の向上につながります
Nuxt.js でのプリレンダリング設定
Nuxt.js では、プリレンダリングを<link rel="prerender">
タグを使用することで、次のページを指定して、事前にそのページをレンダリングすることができます。
6. リダイレクトを避ける
リダイレクトは、 HTTP リクエストを余分にやり取りする必要があります。
そのため、思いがけないリソースの取得が発生してしまう可能性があり、結果パフォーマンスに悪影響を与える可能性があります。
7. ブラウザのキャッシュを活用する
ブラウザのキャッシュを活用することで、 HTTP リクエストの数を減らしたり、 HTTP レスポンスのサイズを小さくできたりします。
つまり、上手に活用することで二回目以降の読み込み時間を大幅に減らします。
キャッシュには、「強いキャッシュ」と「弱いキャッシュ」の二つがあり、今回は下記を例に解説します。
- 強いキャッシュ
-
Cache-Control
ヘッダー
-
- 弱いキャッシュ
-
Etag
ヘッダー
-
Cache-Control
ヘッダー
このヘッダーを設定してキャッシュを一度有効にすることで、ブラウザにキャッシュが存在する場合には、そのリソースへの HTTP リクエストは送信されないようになります。
設定方法
設定方法としては、下記のように max-age パラメーターでキャッシュの期限を秒数で指定できます。
// URL のリソースが取得されてから 600 秒間はブラウザのマシン内にキャッシュされる
Cache-Control:max-age=600
なぜ「強いキャッシュ」と呼ばれるのか
キャッシュの期限が切れるかキャッシュファイルが削除されるまで HTTP リクエストを全く送信しなくなるためです。
この性質はキャッシュするファイルの内容が全く変わらない場合には有効ですが、内容が変化したり更新されたりする可能性のあるリソースファイルをあつかうのには不都合です。
ブラウザのマシン側に一度ファイルがキャッシュされれば、サーバー側からそれをクリアする方法がありません。
ウェブサーバーに新しい内容のリソースがあったとしても、すでにキャッシュを持っているマシンからは古い内容のリソースが取得されます。
Etag
ヘッダー
このヘッダーを設定することで、条件付き GET リクエストを用いたキャッシュを適用することができます。
これにより、エンティティタグを用いて、変更されたリソースに対してのみ、リソース取得を行うようにできます。
設定方法
Etag ヘッダーには、その HTTP リクエストで取得する URL のリソースのバージョン(エンティティタグ)を設定することができます。
例えば、ブラウザがあるリソースに対して初めてアクセスするとき、HTTP サーバーはレスポンスヘッダーの中に次のような Etag
ヘッダーを返送します。
Etag: "H2LKSHIos08HOHoi348FIOUHfhia3hOF"
ブラウザは、取得したファイルと Etag
の値を保存しておきます。
次に同じ URL のリソースに対して HTTP リクエストを送信するとき、ブラウザは HTTP リクエストヘッダーの中に下記のような if-None-Match
ヘッダーを送信します。(先程の Etag
の値)
if-None-Match: "H2LKSHIos08HOHoi348FIOUHfhia3hOF"
HTTP サーバーは、 HTTP クライアントからこの If-None-Match
ヘッダーを受け取ると、リソースの現在の ETag
の値と比較します。
これらが同じであれば、リソースは変わっていないことになるので、 HTTP サーバーは 304 Not Modified
という、リソースを含まないレスポンスヘッダーだけの HTTP レスポンスを返します。
ETag
値が一致していない場合には、対応するキャッシュがブラウザ側にはないということなので通常のHTTPレスポンスを返します。
単語の補足
条件付き GET リクエストとは
HTTP/1.1 で導入されたリクエストです。
クライアントが送信する HTTP リクエスト内に含まれている条件に合致すれば、 HTTP サーバーは 304 Not Modified
レスポンスを返します。
そうすることで、不要なデータの送信を抑えることができるメリットがあります。
まとめ
今回の記事を通して、レンダリングプロセスの中の最初の工程である Loading (リソースの読み込み) のパフォーマンスを向上するためのテクニックについて少しでも知見が広がれば幸いです。
個人的には、パフォーマンス改善という大枠の理解を 1 つずつ言語化して理解することができました。
例)普段開発で使用している Vite のプロダクションビルドが存在している理由がざっくりとパフォーマンス改善という龍度だった理解を、より解像度の高い「レンダリング工程の Loading 部分の HTML, CSS などのリソースファイルの最小化によるパフォーマンス改善のため」のような粒度小さく理解できた。