webhack / ウェブ技術が好き

javascriptやcssやHTML5とかサーバーサイド等のウェブ技術全般を好きに書くブログ

よくある構成のサイトを高速化してPageSpeed Insightsで100点を取る方法

f:id:tkosuga:20160716150725j:plain 写真はうちのみーこ。

サイト制作でよく使われる以下のライブラリ/CSSに依存したサイトを高速化してGoogleのPageSpeed Insightsで100点を取る方法を説明します。

実際にどのぐらい速度に違いがあるのかデモページを設置しました。使っているウェブサーバーはさくらの共有レンタルサーバーです。

高速化前

https://tkosuga.jp/experimental/pagespeed-unoptimized.html

f:id:tkosuga:20160731204541p:plain f:id:tkosuga:20160731204318p:plain

高速化後

https://tkosuga.jp/experimental/pagespeed-optimized.html

f:id:tkosuga:20160731204600p:plain f:id:tkosuga:20160731204305p:plain

リクエスト数が8件から5件に。ページの表示完了速度が1.41秒から0.54秒と38%に短縮されました。 諦めがちな PageSpeed Insights のスコアも71点から100点になりましたね。

では速くするために必要な項目を順番に説明して行きます。

CSSの高速化

高速化前のHTMLはCDN経由で3つのCSSファイルを読み込んでいます。

<link href='https://fonts.googleapis.com/css?family=Open+Sans' rel='stylesheet' type='text/css'>
<link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
<link href="https://maxcdn.bootstrapcdn.com/font-awesome/4.6.3/css/font-awesome.min.css" rel="stylesheet">

CDNの fonts.googleapis / bootstrapcdn は安定していて比較的に速い方だと思いますがHTMLのレンダリングをブロックするためブラウザがHTMLを描画するのに遅延が発生します。

詳しくはCSS の配信を最適化するを参照して下さい。

これを解決するために以下の手順を取ってCSSファイルを最適します。

  1. CSSファイルを1つにしてminify(最小化)する
  2. ページの最後で非同期に読み込む
  3. クリティカル・レンダリングパスをHTMLに埋め込む

1. CSSファイルを1つにしてminify(最小化)する

Node.jsのタスクランナーgulpを使ってcssを最適化して行きます。

2016年夏時点でフロントエンドのビルドと高速化のためにNode.js環境が必須になってきています。gulpでビルド書くのMakefileを書くよりも辛くない?と思わずにはいられませんが、そこは郷に入れば郷に従えで npmコマンド もしくは sh で代替えするか、gulpを身に着けるのが肝要だと思っています。

話が逸れたので戻します。sassをcssコンパイルしてminifyするスクリプトです。

var gulp = require('gulp');
var rename = require("gulp-rename");
var sass = require('gulp-sass');
var cleanCSS = require('gulp-clean-css');
var uglify = require('gulp-uglify');

const app_path = './app';
const build_path = './build';

gulp.task('sass:compile', function() {
  return gulp.src(`${app_path}/sass/app.scss`)
    .pipe(sass.sync().on('error', sass.logError))
    .pipe(gulp.dest(build_path));
});
gulp.task('sass:minify', ["sass:compile"], function() {
  return gulp.src([`${build_path}/app.css`])
    .pipe(cleanCSS({
      compatibility: 'ie8'
    }))
    .pipe(rename("pagespeed-optimized.min.css"))
    .pipe(gulp.dest(build_path));
});

app/sass/app.scss をコンパイルして build/app.css に出力。build/app.css をminifyして build/pagespeed-optimized.min.css に出力しています。

2. ページの最後で非同期に読み込む

できあがったcssファイルをHTMLの最後、body閉じタグ手前で非同期に読み込むようにします。

<script>
  (function(d) {
    var c = d.createElement('link');
    c.type = 'text/css';
    c.rel = 'stylesheet';
    c.href = 'pagespeed-optimized.min.css';
    var s = d.getElementsByTagName('script')[0];
    s.parentNode.insertBefore(c, s);
  })(document);
</script>

ブラウザのレンダリングをブロックする事なくcssの読み込みと適用が行われます。

こうすると読み込みと描画が高速化されますが、最初にページを表示した時にはCSSが反映されていない崩れたHTMLが表示されます。

これを回避するにはクリティカル・レンダリングパスの最適化を行います。具体的にはファーストビューで見える範囲のCSSをHTMLに埋め込みます。

3.クリティカル・レンダリングパスをHTMLに埋め込む

クリティカル・レンダリングパスの詳細は以下ページを読んでください。

developers.google.com

難しい話ですよね。誤解を招きそうですが、1行で説明すると

ページを開いてスクロールしない範囲のCSSがHTMLにあると表示が速いよ!

という事です。

gulp内でビルド中にファストビュー範囲のインラインCSSを作るライブラリに critical があります。

www.npmjs.com

手作業でファストビュー範囲のインラインCSSを作るには Critical Path CSS Generator がおススメです。今回はこのツールを利用しました。

jonassebastianohlsson.com

SPA(シングルページアプリケーション)でページ内の全てが動的に初期化されるサイトは上記2つのツールでは正しく動作しません。

そのためヘッドレスブラウザのPhantomJSを使ってjavascriptを実行できる penthouse があります。

github.com

クリティカル・レンダリングパスの生成はけっこう複雑です。ツールに困った方は以下の説明が良くまとまっていたので参考にして下さい。

github.com

Apache/Nginxで使えるpagespeedモジュールのPrioritize Critical CSSにクリティカル・レンダリングパスの生成を自動化する機能があるようです。リスクの章で説明されているようにデバイスに合わせてコンテンツの振り分けが厳密に行われている場合であれば、自動化できて便利かも知れません。

JSの高速化

高速化前のHTMLはCDN経由で2つのjsファイルを読み込んでいます。

  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
  <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/js/bootstrap.min.js"></script>

CDNの fonts.googleapis / bootstrapcdn は速い方だと思いますが、スクリプトの読み込みはリソースの読み込みが終わるまでHTMLのレンダリングをブロックします

詳しくはレンダリングを妨げる JavaScript を削除するを参照して下さい。

これを解決するために以下の手順を取ってJSファイルを最適します。

  1. JSファイルを1つにしてminify(最小化)する
  2. ページの最後で非同期に読み込む

1. JSファイルを1つにしてminify(最小化)する

jsをコンパイルして1つにまとめるスクリプトです。jquery.jsとbootstrap.jsはローカルに保存しておきます。

var gulp = require('gulp');
var rename = require("gulp-rename");
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var browserify = require("browserify");
var watchify = require('watchify')
var babelify = require("babelify");
var source = require('vinyl-source-stream');

const app_path = './app';
const build_path = './build';

gulp.task('js:compile', function() {
  return browserify(`${app_path}/js/app.js`)
    .transform(babelify, {
      // sourceMaps: true,
      presets: ['es2015'],
    })
    .bundle()
    .on('error', function(e) {
      console.log("Error : " + e.message);
      this.emit("end");
    })
    .pipe(source('app.compiled.js'))
    .pipe(gulp.dest(build_path));
});
gulp.task('js:concat', ['js:compile'], function() {
  return gulp.src([
      `${app_path}/js/jquery.js`,
      `${app_path}/js/bootstrap.js`,
      `${app_path}/js/app.compiled.js`,
    ])
    .pipe(concat('app.js'))
    .pipe(gulp.dest(build_path));
});
gulp.task('js:minify', ['js:concat'], function() {
  function createErrorHandler(name) {
    return function(e) {
      console.error('Error from ' + name + ' in compress task', e.toString());
    };
  }
  return gulp.src(`${build_path}/app.js`)
    .on('error', createErrorHandler('gulp.src'))
    .pipe(uglify())
    .on('error', createErrorHandler('uglify'))
    .pipe(rename("pagespeed-optimized.min.js"))
    .pipe(gulp.dest(build_path))
    .on('error', createErrorHandler('gulp.dest'));
});

app/sass/app.js をコンパイルして build/app.compiled.js に出力。build/app.compiled.jsと他jsを1つにまとめてminifyして build/pagespeed-optimized.min.js に出力しています。

2. ページの最後で非同期に読み込む

できあがったjsファイルをbody閉じタグの手前に入れます。scriptには一応ですがasyncを入れて非同期で実行されるようにします。

  <script async src="pagespeed-optimized.min.js"></script>

初期化順が関係する複数jsファイル読み込みにasyncを使うと初期化の流れが入れ違いになる等でエラーが起こりますが、今回のケースでは1ファイルにまとめてあるので async を付けて非同期読み込みで問題ありません。

scriptタグのasyncについてMozillaが詳しく説明しています。asyncを付けた時の挙動が気になる方はこちらを参照して下さい。

developer.mozilla.org

HTMLの軽量化

html-minifierを使ってHTMLを最小化します。

var gulp = require('gulp');
var rename = require("gulp-rename");
var ejs = require('gulp-ejs');
var minify = require('gulp-html-minifier');

const app_path = './app';
const build_path = './build';

gulp.task('html:minify', function() {
  function createErrorHandler(name) {
    return function(e) {
      console.error('Error from ' + name + ' in compress task', e.toString());
    };
  }
  return gulp.src(`${build_path}/pagespeed-optimized.origin.html`)
    .on('error', createErrorHandler('gulp.src'))
    .pipe(minify({
      collapseWhitespace: true,
      includeAutoGeneratedTags: false,
      minifyCSS: true,
      minifyJS: true,
      removeComments: true,
      removeEmptyAttributes: true,
    }))
    .on('error', createErrorHandler('minify'))
    .pipe(rename("pagespeed-optimized.html"))
    .pipe(gulp.dest(build_path))
    .on('error', createErrorHandler('gulp.dest'));
});

HTML等のリソースを圧縮すると転送速度が速くなります。リソース(HTML、CSS、JavaScript)を圧縮するを参考にして下さい。

サーバーサイドでリソースの圧縮とキャッシュ

お約束のmod_deflatemod_expiresです。

<IfModule mod_deflate.c>
  AddOutputFilterByType DEFLATE image/svg+xml
  AddOutputFilterByType DEFLATE text/plain
  AddOutputFilterByType DEFLATE text/html
  AddOutputFilterByType DEFLATE text/xml
  AddOutputFilterByType DEFLATE text/css
  AddOutputFilterByType DEFLATE text/javascript
  AddOutputFilterByType DEFLATE application/xml
  AddOutputFilterByType DEFLATE application/xhtml+xml
  AddOutputFilterByType DEFLATE application/rss+xml
  AddOutputFilterByType DEFLATE application/javascript
  AddOutputFilterByType DEFLATE application/x-javascript
  AddOutputFilterByType DEFLATE application/x-font-ttf
  AddOutputFilterByType DEFLATE application/vnd.ms-fontobject
  AddOutputFilterByType DEFLATE font/opentype font/ttf font/eot font/otf
</IfModule>

<ifModule mod_expires.c>
  ExpiresActive On
  ExpiresDefault "access plus 7 days"
  ExpiresByType text/css "access plus 7 days"
  ExpiresByType text/javascript "access plus 7 days"
  ExpiresByType application/x-javascript "access plus 7 days"
  ExpiresByType text/html "access plus 7 days"
</ifModule>

読み込むリソースを減らす

FontAwesomeのフォントファイルをインラインSVGに置き換える

FontAwesomeはCSSの中からフォントファイルを読み込みます。使うアイコンが限られている場合はインラインSVGに置き換えてしまうとFontAwesomeのCSSファイルとフォントファイル、2つのファイルの読み込みを削減できます。

FontAwesomeを使うにはclassにfaとfa-xxxxを指定します。

<i class="fa fa-cogs"></i>

これを以下のようにSVGをインライン展開します。

<i class="fa-svg">
 <svg width="1792" height="1792" viewBox="0 0 1792 1792" xmlns="http://www.w3.org/2000/svg"><path d="M1764 11q33 24 27 64l-256 1536q-5 29-32 45-14 8-31 8-11 0-24-5l-527-215-298 327q-18 21-47 21-14 0-23-4-19-7-30-23.5t-11-36.5v-452l-472-193q-37-14-40-55-3-39 32-59l1664-960q35-21 68 2zm-342 1499l221-1323-1434 827 336 137 863-639-478 797z"/></svg>
</i>

展開しているSVGのスタイルをCSSで定義します。

.fa-svg svg {
  height: 9rem;
  max-width: 9.5rem;
}
.fa-svg path {
  fill: #fff;
}

FontAwesomeを個別にSVGにする説明は以下を参照して下さい。

github.com

まとめ

ページ表示が速くなると気持ちいいですね。

FontAwesomeのようにクロスオリジンの影響でCSS内でリソースを追加取得しているものはCDN経由を止めて自前でウェブサーバーに設置するか、この記事のようにSVGに置き換えてしまうと良いかも知れません。

minifyしたHTML/JS/CSSAmazon CloudFront等のCDNに設置するとさらに速くなります。

ディスプレイ広告やアクセス解析タグが複数入ってくるとPageSpeedで100点を取るのがさらに手間で難しくなりますが、速いサイトは永遠のテーマですので頑張って追及して下さい。

この記事はレコーディングダイエットができるウェブアプリを開発した中で「Twitterで共有する」機能を実装している中で色々調べた内容をまとめたものです。

tkosuga.jp

インストール必要なしブラウザで動作するのでよかったら試して見て下さいね。

おまけ

WordPressを高速化するには?

自前で色々な労力を割くよりもKUSAMAGIを使った方が確実に速くて堅牢です。

kusanagi.tokyo

高速化したWordPressの保守で苦労しないためには?

保守で色々と苦労するなら最初からWordPress.comを使った方が確実で堅牢です。全ドメインHTTPS対応しています。トラフィックの急増におびえる必要もありません。

wordpress.com

SEOのためにページ速度を上げるためには?

まず以下の記事に目を通して下さい。

moz.com

www.semrush.com

次にGoogle Developersのパフォーマンスの最適化を読んで下さい。

developers.google.com

これらページ速度最適化を行うにはCSS/JS/HTMLの知識の他に、サイトで利用しているフレームワークプログラミング言語の知識が必須です。例えばRubyOnRailsで作られているサイトであれば以下のようなスライドを読んで理解できる必要があります。

多くのサイトではネットワークよりもデータベースの性能がページ表示速度に大きく影響します。例えばMySQLPostgreSQLが遅いならば以下のようなスライドを参考にしてDBAやプログラマに相談する必要があります。

データベースのボトルネックが解消されれば次にネットワークのボトルネックを解消します。具体的にはロードバランサーやリバースプロキシ、CDNの活用、リクエストを捌くウェブサーバーとアプリケーションサーバーの構成等の問題を発見し的確なアプローチを出来なければ速くなりません。

クラウドコンピューティングやベアメタルサーバーの導入検討もここに分類されます。

長くなってきたので話を折ります。SEOを目的としたページ速度向上はサイトのトップページだけ速くしてPageSpeedで良い点を取っても効果が薄いです。

極端に遅いページが多数存在している、アクセスが集中したらすぐに遅くなる、アクセスエラーが頻発するようなサイトではなく、どのページに誰がアクセスしても速くて快適な状態を作るのがSEOでのページ速度向上です。

そのためにはサイトを構成する一番上から下までに存在するボトルネックを1つづつ見つけて解消する必要があり、それを通して行うには相応の知識が必要です。

SEOのためサイト速くする、という話は決して軽い話ではありません。サイトを開発・運用している現場と相談の上で進めるのをおススメします。