HTML
CSS
JavaScript

WEB ページの読み込み時間を短くしよう

More than 3 years have passed since last update.

まずは遅くなる原因を取り除こう

この記事ではクライアントサイドに焦点を当てて紹介しますので、PHP や Ruby などサーバーサイドのプログラミングに関することは一切出てきませんのでご了承ください(o*。_。)oペコッ

サイトの読み込みが遅い場合、大抵はまずいやり方をしている一部分が足を引っ張っていることが多いと思います。
手当たり次第に最適化を試す前に「なぜ遅いのか?」問題の切り分けをしっかりやってから対応を考えましょう。

原因はどうやって特定するの?

PageSpeed Insights (developers.google.com) を使ってみる

ブラウザ搭載のデバッガで調査するのが王道だけど、お手軽に調べるのであれば PageSpeed Insights がキャッチーでオススメです。
最低限のお作法について指摘してくれるので、指摘事項を直していけば割と解決します。(原因が曖昧なままですけどね)
下の画像は最近、私が作ったサイトの結果ですが、こんな感じで問題点を指摘してくれます。
親切に修正方法まで案内してくれるので、この記事よりも当てになるかもしれません(゚∀。)

PageSpeed-Insights.png

あくまでも目安なので、満点を取ろうとして無理に修正する必要はありません。
下の画像の「JavaScript を縮小する」では、圧縮済みの jQuery Mobile が指摘されていますが、これはソースの先頭に書いてある Copyright のコメントなので、そこは削除しちゃダメですからこの指摘は無視します。

2014-09-09_052941.png

ちなみにQiitaのトップページを評価してみたら赤点でした( ̄▽ ̄;)
Qiita 運営さん頑張って!(`・ω・´)b

GTmetrix (YSlow のオンライン版?) を使ってみる

英語でもよければ GTmetrix というサイトもあります。
以下は Qiita のトップページをチェックした結果です。

2014-09-10_003830.png

リソースごとのタイムラインも見られるのがいいですね。
タイムラインのクリームっぽい色の部分は blocking 、つまりダウンロードの順番が来るまでの待機時間を表しています。
2本の長いバーはどちらも「はてな」のボタン関係でしたが、順番待ちしているだけですから遅い訳ではありませんよ。
バーの長さだけではなく、タイムラインの詳細をよく確認しましょう( ´∀`)

原因ごとの対処方法

サーバーのレスポンスが悪い

サーバーの性能不足(処理能力 / 帯域)、物理的な距離が遠いことなどが可能性としてありますね。
サイト作成のお仕事でサーバーを選べる人は幸せだなーって思いますけど、そんな幸運な環境なら、最低でも国内にあるサーバーを選びましょう。
アメリカにあるサーバーに日本からアクセスすると1往復で100ミリ秒は掛かります。
HTML に画像を2つ貼り付けたページを読み込むとしたら、HTTP の GET リクエストで1回、画像2つは同時並行でダウンロードするから2往復で200ミリ秒かな・・・と考えてしまいそうですが、ファイルサイズが大きいと実際には予想以上の往復が発生して1往復の100ミリ秒が重くのしかかってきます。
この1往復をラウンドトリップといいます。
1回のラウンドトリップで受信出来るデータ量はウィンドウサイズというもので決まっているようです。
TCP に詳しい人からすると、鼻水吹き出すような大雑把な説明ですけど許して(; ´Д`)

容量が大きなファイルが多くてダウンロードに時間が掛かる

一番多いパターンは画像ファイルを使いすぎて重くなるケースかなぁ。
そんなとき、まずは画像を使わずに CSS で頑張ってみて、無理なら画像を最適化しましょう。
大きな実写画像を使う場合は、JPEG で保存して減色してから最適化すると小さなサイズで保存できます。
ボタンなどの小物はアルファチャンネルを使うことも多いですから、PNG で作成して、CSS スプライトを使うのがオススメ。(CSS スプライトは次の節で紹介します)
JPEG の最適化は jpegtran
PNG の最適化は OptiPNG
オンラインなら Compress JPEG Images Online(PNG もあるよ)

HTML ファイルから参照しているファイルが多すぎる

小さな画像が多い場合は CSS スプライトで1つの画像にまとめる。
CSS や JS ファイルが多い場合は、可能な限り1つのファイルにまとめてしまいましょう。
Sass 導入しているなら、コンパイル時に結合とか出来るのかな。
CSS ファイルで @import すると style タグを並べるよりも遅くなりますが、直列で複数つなげるようなことをしなければ、あまり影響はないので気にしなくてもいいと思います。

Instant Sprite というサイトで、簡単に CSS スプライトを作成できます。
CSS スプライトでまとめる画像を、ブラウザにドラック&ドロップするだけで画像を登録できるので大変便利です。

スクリプトの処理が重い

JavaScript の最適化にはブラウザの理解が必要です。
ブラウザでWebページにアクセスすると、以下の流れで表示されます。

2014-09-08_015353.png

このように パース(緑)> レイアウト(赤)> リペイント(青)という流れで処理されます。
Chrome や Firefox などのメジャーなオープンソースブラウザの実装がこうなっているだけで、絶対ではないことを頭に入れておいてください。
ちなみに Firefox ではレイアウトのことを リフローといいます。

スクリプトによってページが変更されたときに、どこまで遡って再処理されるかによって実行速度が変わってきます。
そんなに神経質になる必要はありませんが、処理の書き方次第で1度で済むはずのレイアウトが何度も発生してしまうことがあるので、どのようなときにレイアウトが発生するのか理解しておくと役に立ちます。

Chrome 拡張の Speed Tracer (By Google) はレイアウトやリペイントの発生を詳しく確認出来るので、自分の書いたスクリプトで何度レイアウトが発生しているのかを知りたいときに試してみるといいですよ。

レイアウトでやっていることはノードの位置とサイズの決定ですから、それを変更するとレイアウトが発生します。
スクリプトの処理中に位置やサイズが変更されると、その情報はキューイングされて、スクリプトが終了した直後に1回のレイアウトとリペイントによってすべての変更が反映されます。

Win32 アプリの描画処理も、たしかこんなノリですよね。
無効領域(再描画が必要な領域)を追加していって、スレッドがアイドルになったところで一気に描画するっていう感じ。
大雑把すぎますか?( ̄▽ ̄;)

Speed Tracer でレイアウトの発生を確認する

Chrome 拡張の Speed Tracer をインストールすると Chrome の右上にツールボタンが表示されます。
検証したいページを表示してボタンを押すと以下のようなウィンドウが表示されます。
なんか上部のボタンが上にはみ出てますけどね( ̄▽ ̄;)

2014-09-10_031011.png

左のフィルターボタンを押すと黄色いバーが出てくるので、Minimum duration の数値をゼロにしましょう。
そして左上のレコード開始ボタンを押すとタイムラインの記録が始まるので、JavaScript を動作させると、発生したイベントや処理に掛かった時間などを見ることが出来ます。
以下のソースでレイアウトがどのように記録されるか確認してみます。

<script>
function test() {
  var node = document.querySelector("#box2");
  node.style.color = 'red';
}
</script>
<div id="box1">
  <div id="box2">box2</div>
  <div id="box3">box3</div>
</div>
<div id="btn" onclick="test();">run</div>

2014-09-10_042609.png

赤枠の中がクリック時に起こった処理ですが、レイアウトは見当たらないですね。
クリックイベントの次には Style Recalcalation と Paint あります。
Style Recalcalation はボックスの文字色を変更したためでしょうね。

では次は以下のソースに書き換えて実行してみましょう。

function test() {
  var node = document.querySelector("#box2");
  node.style.width = '50px';
}

2014-09-10_045631.png

赤枠のところに注目です。
Layout と表示されていますから、この処理でレイアウトが1回実行されたことが分かります。
ではサイズの変更を2回やったらどうなるでしょうか。

function test() {
  var node = document.querySelector("#box2");
  node.style.width = '40px';
  node.style.width = '60px';
}

2014-09-10_050448.png

2度実行してもレイアウトは1度しか発生していません。
このように、JavaScript の実行中にサイズを連続で変えてもレイアウトは1回にまとめられることが確かめられました。
では以下のようにレンダーツリーが持っている値を参照するコードを挟むとどうなるでしょうか。

function test() {
  var node = document.querySelector("#box2");
  node.style.width = '40px';
  var w = document.querySelector("#box3").offsetWidth;
  node.style.width = '60px';
}

2014-09-10_051339.png

onclick の実行中にレイアウトが発生しています。
そして JavaScript の処理が終ったあとにもレイアウトが発生していますね。
このように、レンダーツリーの内容を変更してから、レンダーツリーの値を参照するコードを書くと、値を取得するためにレイアウトが実行されてしまいます。
もしも、offsetWidth の取得を一番最初にやっていれば、レイアウト済みのレンダーツリーから値を取得できるため、レイアウトは JavaScript が終了したあとの1回だけに抑えることが出来たはずです。

逆に考えれば、レンダーツリーにアクセスするコードを書くことで、意図的にレイアウトを発生させられるということです。
IE8 でコンボボックス(SELECT タグ)に現在の幅を超える長い文字列のアイテムを追加しても、ある条件下で横幅が広がらないという不具合があります。
この不具合は、コンボボックスを変更してもレイアウトの対象に追加されていないために、コンボボックスの幅が広がらずに文字が切れてしまうようです。
そこでコンボボックスにアイテムを追加したあとに、コンボボックスの offsetWidth を参照すると、レイアウトが実行されて幅が調整されます。

レイアウトの影響を小さくする

DOM ツリーの末端のノードを変更すると、発生するレイアウトがレンダーツリーの広範囲に及ばないことが多いので、レイアウトが発生しても影響を小さく抑えられます。
でもまあレイアウトの発生回数を減らすことに比べると、効果は小さいので出来たら気を付けようね、という程度でおっけーだと思います。
ブロック要素を position:absolute や position:fixed にすることで、サイズの変更が他のノードに連鎖しないので、レイアウトの処理が軽くなります。
ただし、position:fixed はそれ自体が重たいプロパティなので、使用は最低限に控えるべきです。
テーブルタグは、どこかのセルの内容が変わったり、サイズが変更になると、テーブル全体のレイアウトがやり直しになるので、なるべく使わないようにしましょう。

タグに直接スタイルを記述すると、レイアウトが発生しやすくなるので、スタイルはすべて CSS に書くようにしましょう。
保守の観点から見てもそのほうがベターですよね。

レイアウトまとめ

変更するとレイアウトの予定がキューイングされるプロパティ一覧

width height margin padding
display position border border-width
top bottom left right
float clear text-align vertical-align
font-size font-family font-weight white-space
overflow overflow-y line-height min-height



参照するとレイアウトが即座に発生するプロパティ一覧

offsetWidth offsetHeight offsetTop offsetLeft
clientWidth clientHeight clientTop clientLeft
scrollWidth scrollHeight scrollTop scrollLeft
scrollX scrollY scrollBy() scrollTo()
currentStyle getComputedStyle()

ヘッダのないテーブルってマークダウンで作れないのかな?(´・ω・`)

積極的に最適化する

同時並行でダウンロード出来る数を増やすために CDN を利用する

主要のブラウザの同時接続数は 6~8 のようです。
IE11 が載ってなかったけど Opera のように 6 に戻ってるのかなぁ。

ブラウザ 同時接続数 ブラウザ 同時接続数
IE6~7 2 Chrome1~2 6
IE8~9 6 Chrome3 4
IE10 8 Chrome4~ 6
Firefix2 2 Opera9.63 4
Firefix3~ 6 Opera10 8
Safari3~4 4 Opera11~ 6

引用元: Professional Website Performance (PDF 15ページ)

同時接続数はサーバーごとの最大値なので CDN を利用することで改善できる可能性があります。
ただし jQuery などスクリプトのダウンロードは、他のファイルのダウンロードや描画処理をブロックしてしまうので、効果がありそうなのは CSS くらいでしょうか。
サイトを設置したサーバーが国内にあり、利用する CDN が米国にあるような場合だと、ラウンドトリップに時間を取られるので意味がないか逆に遅くなってしまうケースもあるので注意。
むしろ世界中に展開するサイトならば、各国にサーバーを持つ商用の CDN を使うといいんじゃないかと思いますが、商用 CDN はよく知らないので各自で調べてください( ̄▽ ̄;)

プリレンダを使用する

プリレンダは次にユーザーが訪れる可能性の高いページをバックグラウンドで読み込んでレイアウトのステップまで完了させておくことで、実際にユーザーが移動したときに瞬時にページを表示する手法です。
ブラウザはページを読み込み終ったあとに、リンクのラベル(次へなど)やページ構成を頼りに、次に移動しそうなページを予測してプリレンダします。
これを Chrome では予測プリレンダ、IE では自動ページ予測と呼んでいます。
次のページが明確な場合はブラウザの予測に頼らずに、HEAD セクションで指定することも可能です。
指定できるページは1つだけです。

<link rel="prerender" href="http://example.com/page2.html">

そういえば以前、Yahooモバゲーで遊んでいた頃、いちいちログアウトしなかったんですけど、ググってたら検索結果に誰かのプロフィールページがヒットして、予測プリレンダで知らない人に足あと残ったことあります(; ´Д`)
積極的に、おまんちんを踏んでいくスタイル!

プリフェッチを使用する

プリフェッチはページ単位ではなく、リソース単位でバックグラウンドで読み込んでキャッシュしておく手法です。
次にユーザーが移動するページの予測が難しかったり、ユーザー入力のパラメーターを必要とするページなどプリレンダできない場合に、移動先のページ候補に共通する重たいファイルを指定しておくと、実際にユーザーが移動したときに、キャッシュが効いて素早く表示出来ます。
プリレンダと同じように HEAD セクションで指定することが出来ます。

<link rel="prefetch" href="/images/gigantic.jpeg">

DNS プリフェッチを使用する

DNS プリフェッチは、外部ドメインへのリンクやページ内に挿入した広告やブログパーツなど、異なるドメインの名前解決に掛かる時間を短縮するために、事前に実行しておく手法です。
ホスト名の名前解決は時間が掛かることがあるので、この指定は効果的だと思います。

<meta http-equiv="x-dns-prefetch-control" content="on">
<link rel="dns-prefetch" href="//www.example.com">

非同期スクリプトを使用する

通常スクリプトファイルの読み込み時には、他のダウンロードやページの解析はすべて停止するため、ページの表示を遅らせる原因の一つですが、script タグに属性を付けることで非同期で読み込まれます。
これって使いどころが難しいですよね。

スクリプトを非同期にダウンロードし、スクリプトは直ちに実行されます。
<script async src="hoge.js"></script>
スクリプトを非同期にダウンロードし、スクリプトはページの解析が終了したら実行されます。
<script defer src="hoge.js"></script>
async として解釈される。ただし async に未対応のブラウザは defer と解釈する。両方に対応していなければ指定は無視される。
<script async defer src="hoge.js"></script>

遅延読み込みで見える部分を優先する

折りたたみメニューや、スクロールしなければ見えない部分に隠れている画像の読み込みを後回しにして、ユーザーに見える部分を優先的に読み込むことで体感的に速く見せるのは効果的です。

IE11 限定ですが・・・

まだ実験的な実装段階ですが、IE11 には以下のような記述で優先度の低い画像を指定することが出来ます。

<imt src="/images/logo.png" lazyload />

img タグに lazyload 属性を追加するだけなので簡単ですね( ´∀`)
あくまでも優先度が下がるだけなので、この画像が読み込まれないと onload イベントは発生しないんじゃないかと思います。(未確認)
現状は IE11 独自仕様みたいですけど、未対応のブラウザに副作用がないから、おまじないのつもりで付けてみてもいいかもしれませんね。
ちなみに、何も指定しなかった場合の優先度の高い方から次のようになります。

  • HEAD セクションに定義されている CSS や JavaScript
  • JavaScript による同期通信
  • BODY セクションに定義されている画像やスクリプトなどのファイル
  • iframe 内のページ(以降 iframe 内にもこのルールが再帰的に適用される)

GTmetrix で Qiita トップページを解析したときに、はてなボタンの blocking 時間が長かった原因は優先度のせいです。
はてなボタンは iframe 内にあるため、ページ内のすべてのコンテンツが読み込み終って、同時接続数に空きが出来るまで待たされていたんですね。
つまり早く表示したいコンテンツは、iframe 内に入れてはいけないってことになります。

本気で対策するなら・・・

Lazy Load Plugin for jQuery がオススメです。
大量の写真を掲載しているようなページでは、最適な方法だと思います。

<html>
<hrad>
<meta charset="utf-8" />
<style>
.lazyload-img { width: 300px; height: 200px; }
</style>
<script src="/js/jquery.min.js"></script>
<script src="/js/jquery.lazyload.min.js"></script>
<script>
$(document).ready(function(){
    $(".lazyload-img").lazyload({
        effect : "fadeIn"
    });
});
</script>
</head>
<body>
<img class="lazyload-img" src="images/loading.gif" data-original="images/pic001.jpg" />
<img class="lazyload-img" src="images/loading.gif" data-original="images/pic002.jpg" />
<img class="lazyload-img" src="images/loading.gif" data-original="images/pic003.jpg" />
</body>
</html>

使い方は簡単です。

  • img タグにダミーの画像を指定する(サイクルアニメ GIF とか)
  • img タグの data-original 属性に遅延読み込みしたい画像を指定する
  • CSS か img タグの属性で本当の画像のサイズを明示的に指定する

あとは jQuery オブジェクトから lazyload メソッドを呼び出すだけです。
簡単でしょ(゚∀゚)

効率のよい CSS

CSS セレクタは低コストなので、相当効率悪い書き方をしてもパフォーマンスの低下が問題になることは、まずありません。
最適化の効果が小さいので、これからサイトを作るときに頭の片隅に入れておく程度で十分です。
CSS セレクタは右から左へ解釈されますから、子孫セレクタにタグ名を指定すると、ページ全体の同名のタグを拾ってしまうので、その後の選択処理の計算量が増えます。

/* li ではなくクラス名を当てましょう */
#hoge > ul > li {
}

ユニバーサルセレクタやタグ名をセレクタに記述するのを極力避けるようにしましょう。
現在のモダンブラウザであれば、これだけ気を付けておけば十分です。

むしろ問題になるのは、重たい描画処理を伴うプロパティの使用です。
border-radius と box-shadow を同じ要素に指定したり、グラデーションを掛けたりするのは重いです。
スマホサイトで多用すると問題になりやすいので注意。
以下は重いプロパティの一例です

  • border-radius + box-shadow
  • gradient
  • background-size
  • background-attachment:fixed
  • position:fixed
  • @font-face
  • transition
  • animation
  • opacity
  • text-align

まとめ

いろいろ間違いを含んでいそうですが、気が付いた方はやんわり指摘してください。
いっぱい書きましたけど、私は全部実践しているわけではありません。
労力と効果を考えてバランスよく選択してくださいね。
保守性を犠牲にしてまで最適化すると、あとで苦労するので何事もほどほどにね。

参考サイト