Webフロントエンド表示速度、最速化手法まとめ


やりすぎても無駄になることがあるので、必要十分なバランスを見極めて実施する。主に社内のプログラマ向けのまとめ資料。他にもあれば追記していく予定。

# ページの状態を調べる

まず対象のページが、どういった状態なのかを調べる。

## Google PageSpeed Insights

これを100点満点にする必要はないが、赤くなっている項目に関しては、なるべく対応したほうが良い。モバイルサイトにも対応しているので便利。

- https://developers.google.com/speed/pagespeed/insights/?hl=ja

## pingdom

実際にページにアクセスしてスピードを計測できる。海外サーバなので、けっこう遅い…。最適化前と最適化後で比較するのに使うといい。

- http://tools.pingdom.com/fpt/

## Chromeデベロッパーツール

コンテンツの読み込み順序／速度／ページ全体のファイルサイズなどが分かる。これを開いて効果があるかどうか常に確認する。初回訪問ユーザのパフォーマンス改善のために「Disable cache」にチェックを入れておく。

![chrome1.png](https://qiita-image-store.s3.amazonaws.com/0/2152/9da53470-cd8f-47a6-fa0e-4ac6144f61ce.png "chrome1.png")

各ファイルにキャッシュとgzipの設定が効いているかどうかもResponse Headersから確認が可能。

![chrome2.png](https://qiita-image-store.s3.amazonaws.com/0/2152/81989208-ba80-0810-bc98-201397f523a0.png "chrome2.png")

## speedlimit（Mac）3G / LTEシミュレーション

モバイルの回線環境を、PCから再現するのに便利なツール。対象ホスト／ポート単位でスピードを制限できる。

- http://mschrag.github.io/


## 目安

ページのスタイルにもよるので、なんとも言えないけど。

### モバイルサイト

- ロード速度：1-2sec
- ページサイズ：1MB以下（できれば500KB以下）

### PCサイト

- ロード速度：1-2sec
- ページサイズ：2MB以下

# 画像ファイルのサイズを削る

とにかく画像ファイルのサイズを削ることを頑張る。

## ImageOptim（Mac）

とにかく画像ファイルを軽くしたいときに放り込む。CPUめっちゃ使う。過去何もしていない画像であれば、約10〜30%くらいは削れる。

https://imageoptim.com/

## convert（ImageMagick）

JPGであれば、圧縮率を高めにすることでファイルサイズを削れる。ただし、画質も当然落ちる（JPGからJPGに再圧縮をかけるから、より荒れやすい）。

なにも考えずにPSD→JPG画質100で出力された画像を、とりあえず削りたい時とかに使うと良い。

```console:command
$ convert 入力ファイル名 -quality 値 出力ファイル名
```

## mogrify（ImageMagick）

convertと基本同じだけど、一括処理するときに便利。上書きされるので、バージョン管理もしくは事前にコピーをとっておくことをオススメする。

```console:command
$ mogrify -quality 50 *.jpg
```

そもそもHTMLコーディングのスライス時に、ちゃんと適切な画像品質にすることが重要（Photoshopでの画質60プログレッシブとか、PNG/GIFの色数落としとか）。

# Retina対応

Retinaなデバイスで綺麗に画像を表示させたい、それは人の性。でも考えなしに対応すると死ぬ（回線が）。

### 圧縮率高め、2倍の大きさに

100x100の大きさで表示するとして、iPhone5とかなら200x200の大きさで画像を用意すれば綺麗にRetina対応できる。

```html
<img src="hoge.jpg" width="100" height="100">
<!--  hoge.jpgは実際は 200x200 -->
```

もちろんファイルサイズも大きくなる。

そこで、大きさを2倍にする代わりにJPGの圧縮率を高め（30〜50くらい）にすることで、大きいけど軽い画像ファイルを作ることが出来る。

圧縮率を高めにすることで、品質低下が心配されるがモバイル機器であれば、Retina対応していることで、ある程度カバーされるように感じる。ここは、実際に試してみて適正な圧縮率を検討したほうが良い。


### HTML5 srcset属性

Retina用の画像、通常用の画像それぞれ専用に用意して、最適な画像をブラウザが自動的に選択してダウンロードしてくれる便利な属性。ただ現状では対応ブラウザは少なく、ポリフィルあてるJS使ったとしても、パフォーマンスが良くなるわけじゃないので、ある程度妥協するほうがいいかも。

```html
<img src="hoge.jpg" srcset="hoge@2x.jpg 2x">
```

### HTML5 pictureタグ

こちらも、srcset属性と同じく…。

```html
<picture>
  <source media="(min-width: 320px)" srcset="s.jpg 1x, s@2x.jpg 2x">
  <source media="(min-width: 640px)" srcset="b.jpg 1x, b@2x.jpg 2x">
  <img src="s.jpg">
</picture>
```

# リクエスト数を減らす

## CSS Sprite

複数の画像を1つの画像としてまとめて、CSSで表示場所を指定することで、1回のリクエストで画像を使いまわせる。

かなり使い古された手法だが、色々なアイコン画像を1ページ中にたくさん使っているようなページでは、それなりに効果あり。手動でCSS Sprite作るのは死ぬので、Compass / Gruntなどで自動化すると良い。

なお、なんでもかんでもCSS Spriteにすると、画像ファイルが肥大化して、逆に表示パフォーマンス落ちることもあるので適切に（15KB超えるようだと微妙なラインかも）。

## DataURI

画像データをimgタグに直接埋め込むことで、画像ファイルのリクエストをなくす。

```html
<img src="data:image/gif;base64,R0lGODdhMAAwAPAAAAAAAP///ywAAAAAMAAw
   AAAC8IyPqcvt3wCcDkiLc7C0qwyGHhSWpjQu5yqmCYsapyuvUUlvONmOZtfzgFz
   ByTB10QgxOR0TqBQejhRNzOfkVJ+5YiUqrXF5Y5lKh/DeuNcP5yLWGsEbtLiOSp
   a/TPg7JpJHxyendzWTBfX0cxOnKPjgBzi4diinWGdkF8kjdfnycQZXZeYGejmJl
   ZeGl9i2icVqaNVailT6F5iJ90m6mvuTS4OK05M0vDk0Q4XUtwvKOzrcd3iq9uis
   F81M1OIcR7lEewwcLp7tuNNkM3uNna3F2JQFo97Vriy/Xl4/f1cf5VWzXyym7PH
   hhx4dbgYKAAA7">
```

ただし、ファイルサイズが大きい物を無理やり入れると逆にダメな感じする。透過GIFとかのダミー画像など小さい画像レベルじゃないと使わないかも。

## JS / CSSファイルの結合・minify

Railsなどではアセットパイプラインとして標準でサポートされている。こちらも複数のJavaScript / CSSファイルを1つのファイルに結合してリクエスト数を減らす。

CSS Spriteと同様にGruntなどで自動化すべき。

```javascript:Gruntfile.js
// 例
module.exports = function (grunt) {

  grunt.initConfig({
    pkg: grunt.file.readJSON('package.json'),
    concat: {
      options: {
        separator: ';',
      },
      dist: {
        src: ['app/scripts/*.js'],
        dest: 'dist/built.js',
      },
    },
    uglify: {
      my_target: {
        files: {
          'dist/built.js': ['dist/built.js']
        }
      }
    },
    cssmin: {
      compress: {
        files: {
          'dist/built.css': ['app/css/*.css']
        }
      }
    }
  });

  grunt.registerTask('default', ['concat', 'cssmin', 'uglify']);

  grunt.loadNpmTasks('grunt-contrib-concat');
  grunt.loadNpmTasks('grunt-contrib-cssmin');
  grunt.loadNpmTasks('grunt-contrib-uglify');

};
```

# Expiresでファイルキャッシュを有効に

以下はApacheでの例。キャッシュ期間と対象ファイルは、サービスごとに適切に。基本、画像ファイルとCSS / JavaScriptファイルを対象にすれば良い。キャッシュのクリアには、タイムスタンプをファイル名に埋め込む形で行う（Gruntなどのツール使用前提）。

```apache:.htaccess
<IfModule mod_expires.c>
ExpiresActive On
ExpiresByType image/gif "access plus 30 days"
ExpiresByType image/jpeg "access plus 30 days"
ExpiresByType image/png "access plus 30 days"
ExpiresByType application/x-javascript "access plus 30 days"
ExpiresByType text/css "access plus 30 days"
<IfModule mod_deflate.c>
```

# gzipを使って圧縮させる

mod_deflateを使った例。画像ファイル以外をすべてgzip圧縮する。

```apache:.htaccess
<IfModule mod_deflate.c>
SetOutputFilter DEFLATE
 
BrowserMatch ^Mozilla/4 gzip-only-text/html
BrowserMatch ^Mozilla/4\.0[678] no-gzip
BrowserMatch \bMSI[E] !no-gzip !gzip-only-text/html
 
SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png|ico)$ no-gzip dont-vary
 
Header append Vary User-Agent env=!dont-vary
</IfModule>
```

リソースファイルをAWSのS3などにおいてある場合は、AWS SDKなどを通じてgzip圧縮したファイルを転送する必要がある。詳しくはこちらを参照。[AWS SDK for Ruby を使ってコンテンツを S3 に gzip 圧縮して保存する](http://dev.classmethod.jp/cloud/aws/aws-sdk-for-ruby-s3-gzip/)

# JavaScriptの最適化

**JavaScriptは重い！**というのを常に意識する。

## 無駄なJavaScriptは読まない

SNSのプラグイン設置などでありがち。ページ内に複数同じSNSプラグインを設置して、同じJavaScriptファイルを何回も読みに行こうとする。最初の1回だけにする。

## 外部ライブラリはminify化されたものを利用する

jQueryなどはかなり大きいので、minify化されたものを利用するようにする。また、プラグインなど使っていないページでも全部読ませてたりする場合は、外せないか検討する。そのjQueryプラグイン、本当にプラグインじゃなきゃダメか？自前でちょいちょいっと書けばいけないか？確認する。

JavaScriptコード自体の最適化は、とりあえず無視…だけど、最低限。

- DOMをゴリゴリ動かすのは避ける
- JSファイル読み込みは、HTMLの一番最後にする（レンダリングブロックするから）

# CSSの最適化

CSSのセレクタでレンダリング速度向上…は、やってみたけど、あまり体感できなかったので、そこまで気にしてやってない。ただ、以下のことだけはコーディング時に意識をしている。

- ワイルドカード（*）なユニバーサルセレクタを使わない
- id / classはなるべく1発指定にする（変に階層にしない）
- セレクタは右から左に解釈される（インド人を右に）

# 画像ファイルの後読み込み

通常、ディスプレイに表示されていない画像ファイルとかも読み込みをするので長いページの場合、初回アクセスに無駄なリクエストが飛んでいることになる。というわけで、ディスプレイ外の画像は読み込まずに、必要になったタイミングでリクエストを飛ばすことで、初回アクセスのレスポンスを良くすることが出来る。

が、スクロールイベントを利用しているため、ユーザ体験的にスクロールするたびに画像が読み込まれている印象があり、あまり気持ちのいいものではないかもしれない。かといって、フェードインするようなアニメーションをしても、やり過ぎ感あったりと難しいところ。

ただ、 **長いページであれば、かなり効果はある。**

## jQuery Lazy Load

jQueryを使っているのであればこちらが良い。

- http://www.appelsiini.net/projects/lazyload

```html
<img class="lazy" data-original="img/example.jpg" width="640" height="480">

<script src="jquery.js" type="text/javascript"></script>
<script src="jquery.lazyload.js" type="text/javascript"></script>
<script>
$(function() {
    $("img.lazy").lazyload();
});
</script>
```

これだけ。ポイントは`<img>`タグに横縦のサイズを明示してあげること。これがないと、スクロールするたびに、ガタガタしてしまう。また、`src`属性でファイル名を指定せず、`data-original`で指定してあげる。じゃないと意味がなくなる。

## vvo/lazyload

スタンドアローンなJavaScriptライブラリであればこちら。

- https://github.com/vvo/lazyload

# CDNを使う

AkamaiやAWS Cloudfrontを気軽に使えるようなサービスであれば、積極的に使ったほうが良い。が…Webサーバがウンコではなく、トラフィックもそんなにないサービスの場合は、そこまで恩恵はないかもしれない。

# 最後に

フロントエンドでやれる表示速度最適化は、けっこう大変。大変な割にそこまで体感速度が上がらなかったりと、苦労する。けど今どきならGrunt/Gulpなど自動化ツールがあるので、そこら辺の恩恵を受けるのが良い。環境をまるっと用意するなら、Yeomanとかもオススメ。

また、JavaScriptフレームワーク系とかにはふれていないので、ガッツリやるならもっとやれることがあると思う。

あと、そもそもHTMLのレスポンス自体が1秒以上かかっている場合、サーバサイドの見直しもやったほうが良い。たいていDBへの接続／無駄なSQL発行／インデックス使われていない／PHPが重い／エラー吐きまくりあたりを見なおせばOK。