読者です 読者をやめる 読者になる 読者になる

webhack / 猫とウェブ技術が好き

javascriptやcssやHTML5とかサーバーサイドの技術やプロジェクトマネジメントとかウェブに関するマーケティングとかWEBを取り巻く全般を好きに書くブログ

Google Analytics有でGooglePageSpeedを100点にする方法

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

PageSpeed Insightsで100点を取るためには「Google Analytics を外しましょう!」という情報をよく見かけるので、

Google Analyticsを外さずにちゃんと100点を取る方法を説明します。Bootstrap等のよくある普通のサイトを高速化してPage Speed Insightsで100点を取る方法も参考にしてください。

王道編

analytics.jsをサーバーにホスティングする

JavaScript ファイルをローカルに保存する際の注意点

サーバーにホスティングして、そこでキャッシュの設定を行えば解決ですが、定期的に更新しているから保存して使わないようにと警告されています。

そのためサーバー内でcron等を利用して定期的にanalytics.jsをダウンロード、そのファイルに変更があった場合にはパラメーターを変更する(ファイルのハッシュ値をパラメーターに利用しても良い)、と言ったanalytics.jsの更新に対応できれば問題ありません。

CDNを経由させる

analytics.jsをCDN経由で取得するようにすればCDNのキャッシュが有効になるため「キャッシュの有効期限が短い」と警告が出る問題が解決します。

WordPressでは、WordPress内で使われるcss/js/imgをCDN経由で取得するプラグインが幾つかあります。CDN EnablerやJackpotのプラグインPhoton等です。

番外編

ga-lite を使う

Analytics.jsのキャッシュ可能なサブセットga-liteを使うと機能制限がありながらも解決できます。

github.com

100行ほどの小さいjsにまとめられています。PVだけ取れれば良い人向けです。

外道編

PageSpeedInsightsのオプションでサードパーティーのリソースを除外する

PageSpeed ToolsのPagespeedapi: runpagespeedを見ると以下の通りで、Page Speed InsightのURLに?filter_third_party_resources=trueを付け加えるとGoogle Analyticsのjsファイルが評価から除外されます。

filter_third_party_resources     boolean     Indicates if third party resources should be filtered out before PageSpeed analysis. (Default: false) 

analytics.js等のサードパーティーリソースを除外したPageSpeedInsightsを使う場合は以下のリンクからアクセスしてしてください。

https://developers.google.com/speed/pagespeed/insights/?filter_third_party_resources

PageSpeedInsightsからのアクセスだったらGoogleAnalyticsを外す。

以下はPHPのコード例です。ユーザーエージェントを見てPageSpeedInsightsのアクセスであればGoogleAnalyticsのコードをHTMLに含めません。

<?php if (!isset($_SERVER['HTTP_USER_AGENT']) || stripos($_SERVER['HTTP_USER_AGENT'], 'Speed Insights') === false): ?>
// your analytics code here
<?php endif; ?>

おちついて編

PageSpeedInsightsの測定中はanalytics.jsを外す。

ストレートにanalytics.jsを読み込むscriptタグを外すと減点0になります。

サイトTOPからanalytics.jsを外す。

PageSpeedInsightsで測定するのはサイトTOPが多いと思います。サイトTOPだけanalytics.jsを読み込まなければ減点がありません。

まとめ

このanalytics.jsがあるために1点減点され、99点になっているサイト管理者もいると思います。

「気にするな」と言われれば、そこまでの問題ですが様々な解決方法があるので必要に応じて参考にして下さい。

GoogleのAPIでSEOする方法

f:id:tkosuga:20160813225351j:plain 写真はおもちゃに高速アタックするうちのみーこ。

ある程度の規模感があるサイトを複数運営していると以下のようなニーズが社内で出てくると思います。

  • 独自のSEOレポートを作りたい
  • 共通のSEOをする指標が欲しい
  • 各種プロモーションとSEOを連動させたい

これらを自社に合った形で実現するのに使えるGoogleが公開しているAPIを簡単な説明付きで紹介します。

Search Console API (Webmaster Tools API)

プログラムからSearch Console APIを利用するとページに訪問した検索キーワードが取得できたり便利です。

Google検索エンジンで(not provided)にまとめられてしまいGoogle Analyticsから参照できなくなったキーワード情報ですが、Search Console API経由で取得できます。

Google Analytics API

SEO目的でGoogle Analyticsを利用するのに良く使われるのがPVやセッション数などを取得するCore Reporting API。それと流入経路別でCV数や接点を取得できるMulti-Channel Funnels Reporting APIです。

またGoogle Analyticsで仮想ページや独自データを管理できるようにする analytics.js を良く使います。

AdWords API

Googleで広告出稿を行っていて、広告運用と連動したSEOを行う場合に使うのがAdWords APIです。キャンペーンの管理や自動入札などを行うためのAPIですが、広告出稿状況や成果に合わせてキーワード単位のSEO計画を立てる事が出来ます。

Cloud Natural Language API

これからSEOで使われるだろうAPIが先日公開されたGoogle自然言語APIです。

自前で機械学習環境を作らずに、Googleによって機械学習済み(これから学習強化されて行くと思われる)な日本語の自然言語処理を利用できる便利なAPIです。辞書学習が大変だった形態素解析しての関連語抽出(co-citation/co-occuranceと呼ばれるもの)もAPI経由で可能です。

APIは便利

欲しい情報がAPI経由で取得できれば加工して表示する幅も広がるし、何といっても複数APIを横断してURLやパス、キーワード単位でデータを統合できるのが良いですね。

自動生成したレポートをメールに張り付けて自動送信する事も出来るようになります。

複数APIを横断して統合した結果を返す自前のAPIサーバーを立ち上げてCSVを返すようにすれば、Excel上でチャート描画したり、ブラウザ上でd3.jsを使ってグラフを描画する事も出来ます。

認証やエラー処理が色々あって、データ取得も方法によっては遅いし、リソース制限が厳しいのでキャッシュするようにしたり、APIのバージョンが変わって動かなくなったりと気をつかう所は多々でてきますがAPIを使ったレポーティングには夢がありますよね。

おまけ

じぶんと何の関係もないツールですが、Google Analyticsのキレイなレポートをブラウザで作りたい人はMegalyticがとても便利ですよ。

Google Analyticsの詳しい調べ事をしていると良く出てくるサイトで、自由度の高い柔軟なカスタマイズが出来て使い方も分かり易いです。Google Analytics APIを使って何かしたい用途の多くは有料ですがMegalyticを使うと解決できると思います。

Googleの情報でSEOを体系的に学ぶ

f:id:tkosuga:20160804205854j:plain

絵はともだちが書いたみーこ。

SEOはブラックボックスなイメージが強いと思います。ですが最近はGoogleから日本語ドキュメントが多数公開され、さらに整備されているので勉強し易くなりました。

インハウスSEOを行っている方、協力会社でSEO作業をしている方、そのマネジメントをしている方向けにSEOを体系的に学べる情報をまとめました。※2016年8月22日時点

具体的には以下のような方向けです。

  • ウェブマスター(サイト管理者)
  • ウェブ広告全般の管理者
  • インハウスSEO従事者
  • アフィリエイター
  • ウェブプログラマ(フロントエンド)
  • サーバーサイドプログラマ(バックエンド)
  • ウェブディレクター
  • ウェブデザイナー
  • HTMLコーダー

検索の仕組みについて

検索エンジンのクローラーがサイトに訪問してページを収集、それをデータベースに入れて検索できるように処理をして(index/インデキシング)、検索結果ページに表示するのが一連の流れです。

この流れを見るとSEOの目的が少し理解し易くなると思いますよ。

SEOについて

ここでのSEOとは検索ランキングを上げる方法ではなくて、Googleが検索エンジン利用者にとって望ましい(と思われる)検索結果ページを返すために、ページをクロールする先のサイト運営者にして欲しい事柄だと思って下さい。

そのため検索エンジンの利用者とクローラー(BOT)に違うコンテンツを見せる行為(クローキング)には避けるようにと明示をしたり、クローラーで読み取れないフラッシュコンテンツやJavaScriptで構築されたウェブアプリケーションへの注意を明示しています。

品質について

ここでの品質はウェブサイトの品質とコンテンツの品質が混ざった話です。基本的にはGoogleクローラーがサイトを辿り易くするのと、Google検索エンジンがコンテンツを評価し易くするための方法です。

モバイル対応

WordPressのモバイル対応方法がモバイル対応のテンプレートを使ってね。が基本になっているのが海外の発想らしくて良いですね。

モバイルからの検索量が増えている昨今、モバイル検索経由で訪問したユーザーに満足してもらうにはモバイルフレンドリーなサイトである事が望まれています。

ページスピードの向上

高速なインターネットの実現に情熱を注いでいるGoogleはウェブサイト高速化について本当に細かく情報を公開しています。

主にモバイル検索ですが索結果ページのランキング要素にページスピードが含まれているため、SEO対策の中にページ速度の改善も含まれてきます。ちなみに速いレンタルサーバーにサイトを乗せ換えれば速くなるから完了と言うものではないですよ。

Google検索結果ページの補助コンテンツと仕様

Google検索結果には、Googleが主導して仕様策定を進めたAMPや、マイクロフォーマットのschema.orgでの記述内容が表示されます。

これらは検索ランキングを上げるものではないとGoogleが回答していますが、サイト上で適切に実装されていれば検索結果からのトラフィック量を増やすにの有効な方法です。

エンジニア・プログラマ向け情報

ツール

Googleが開発して公開しているPageSpeedモジュールやChrome開発者ツールの説明を読んで動作を試して見るとSEOへの理解が深まると思います。

トレーニング

無料で利用できるUdacityでGoogleが公開しているウェブサイトについてのトレーニングコースはSEOの理解を深くする上で役に立ちます。

まとめ

Googleから公開されている情報を見直してみると、今まで気づいてなかった改善点など見つかるかも知れませんよ。

ただどんなにサイト分析して計画を立てて改善点を上げても、サイトの仕組み上できない事が多ければ時間を浪費するばかりなのでまずはサイトを運用・保守・開発している現場スタッフと相談してSEOを組み立てるのがおススメです。

いまのウェブプログラマー、ウェブデザイナー、ウェブマスターはSEOに慣れているせいか知識のある方が多いですし、少し追加で勉強すればマネージャーが思った以上にSEOを理解してくれますよ。

「Facebookで共有する」ボタンを高速・快適に表示するスクリプトの説明

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

昨日のツイートボタン表示と同じようにFacebookでも「共有ボタンの表示が遅い&重い」問題があります。

Facebookで共有ボタンを外すと当然速くなりますがサイトへのトラフィックが減る事になるので本末転倒です。

遅くなる&重く感じる理由はページの初期化が一斉に行われている中でFacebookスクリプトの読み込みと初期化が同時に行われているからです。

そのため任意のタイミングで初期化を行う、例えば共有をする直前で初期化する、仕組みにするともっさり感じが減って快適になります。

以下、任意のタイミングでFacebook JavaScript SDKを初期化してボタンを押すと共有ボタン表示を行うサンプルコードです。

    function initFacebookAPI(loadedHandler) {
      window.fbAsyncInit = function() {
        FB.init({xfbml: false, version: 'v2.7'});
        loadedHandler();
      };
      (function(d, s, id) {
        var js, fjs = d.getElementsByTagName(s)[0];
        if (d.getElementById(id)) {
          return;
        }
        js = d.createElement(s);
        js.id = id;
        js.src = "https://connect.facebook.net/en_US/sdk.js";
        fjs.parentNode.insertBefore(js, fjs);
      }(document, 'script', 'facebook-jssdk'));
    }
    initFacebookAPI(
      function() {
        document.getElementById('show-fb-share-button').onclick = function() {
          FB.XFBML.parse();
        };
      }
    );

HTMLは以下のようにしておきます。

  <div>
    <button id="show-fb-share-button">show-fb-share-button</button>
    <span class="fb-share-button" data-href="https://developers.facebook.com/docs/plugins/" ref="unique-parameter-value">
    </span>
  </div>

個別に説明して行きます。

以下スクリプトはクイックスタート: Facebook SDK for Javascriptで説明されている内容に手を加えたものです。

      window.fbAsyncInit = function() {
        FB.init({xfbml: false, version: 'v2.7'});
        loadedHandler();
      };
      (function(d, s, id) {
        var js, fjs = d.getElementsByTagName(s)[0];
        if (d.getElementById(id)) {
          return;
        }
        js = d.createElement(s);
        js.id = id;
        js.src = "https://connect.facebook.net/en_US/sdk.js";
        fjs.parentNode.insertBefore(js, fjs);
      }(document, 'script', 'facebook-jssdk'));

まずsdk.jsが非同期に読み込まれます。読み込みが完了すると fbAsyncInit が呼び出されます。

fbAsyncInit では FB.init を呼び出しFacebook SDKの初期化を行います。

オプションの xfbml を false にすると、XFBMLマークアップを自動的にパースして共有ボタンを設置・表示する処理が行われません。共有ボタンは任意のタイミングで表示させたいので falseにして高速化します。

次に「シェアボタンを表示」を押したタイミングでFacebook共有ボタンを表示するようにします。

    initFacebookAPI(
      function() {
        document.getElementById('show-fb-share-button').onclick = function() {
          FB.XFBML.parse();
        };
      }
    );

事前にFacebookSDKの初期化が完了しているので FB.XFBML.parse を呼び出すとFacebook共有ボタンが表示されます。

以上でFacebook共有ボタンの表示タイミングをずらす事で快適に表示できるようになりました。

すっきり。

Facebook Javascript SDK の読み込みと初期化速度を測定するテストページを設置しました。

https://tkosuga.jp/experimental/comfortable-facebook-share-button.html

テストしてみるとSDKの初期化は速いですがFB.XFBML.parseに時間が掛かっているのが分かります。共有ボタンの表示はページ読み込み時からずらした方が快適になると思います。

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

tkosuga.jp

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

「Twitterで共有する」ボタンを高速・快適に表示するスクリプトの説明

f:id:tkosuga:20160731002044j:plain 写真はうちのみーこが子猫だった頃。

「ツイート」ボタンを表示しているサイトが抱える共通の課題に「ツイートボタンの表示が遅い&重い」があります。

ツイートボタンを外すと当然速くなりますがサイトへのトラフィックが減る事になるので本末転倒です。

この遅くなる&重く感じる理由は、ページ表示タイミングで各スクリプトの読み込みと初期化が一斉に行われている中でTwitterスクリプトも同じく読み込みと初期化が行われているためです。

ページを読み進めてツイートボタンが表示された時に初めて初期化するなど、ページ読み込み時ではなく任意のタイミングでTwitterスクリプトの読み込み・初期化・表示を行うとこの課題を解決できます。

以下、任意のタイミングでTwitterスクリプトを初期化してツイートボタン表示まで行うサンプルコードです。

    function initTwitterAPI(loadedHandler, clickHandler) {
      window.twttr = (function(d, s, id) {
        var js, fjs = d.getElementsByTagName(s)[0],
          t = window.twttr || {};
        if (d.getElementById(id)) return t;
        js = d.createElement(s);
        js.id = id;
        js.src = "https://platform.twitter.com/widgets.js";
        fjs.parentNode.insertBefore(js, fjs);

        t._e = [];
        t.ready = function(f) {
          t._e.push(f);
        };
        return t;
      }(document, "script", "twitter-wjs"));

      window.twttr.ready(function(twttr) {
        window.twttr.events.bind('loaded', function(event) {
          if (!event) return;
          loadedHandler(event);
        });
        window.twttr.events.bind('click', function(event) {
          if (!event) return;
          clickHandler(event);
        });
      });
    }
    setTimeout(function() { // 任意のタイミングを1.5秒後として初期化を行う。
      initTwitterAPI(
        function(event) {
          window.twttr.widgets.createShareButton(null, document.getElementById('twitter-share-button'));
        },
        function(event) {
          document.getElementById('twitter-share-thanks').innerHTML = "thank you your tweet!!";
        }
      );
    }, 1500);

個別に説明して行きます。まずTwitterAPI(platform.twitter.com/widgets.js)の初期化はSet-up Twitter for Websitesの内容ままです。 このコードは非同期に実行され初期化が終わるとTwitterAPIの twttr が利用できるようになります。

      window.twttr = (function(d, s, id) {
        var js, fjs = d.getElementsByTagName(s)[0],
          t = window.twttr || {};
        if (d.getElementById(id)) return t;
        js = d.createElement(s);
        js.id = id;
        js.src = "https://platform.twitter.com/widgets.js";
        fjs.parentNode.insertBefore(js, fjs);

        t._e = [];
        t.ready = function(f) {
          t._e.push(f);
        };
        return t;
      }(document, "script", "twitter-wjs"));

次にloadの完了とツイートボタンをclickした時のイベントを追加します。loadedHandler と clickHandler は初期化時に引数で指定しています。 このイベントはScripting: Eventsで説明されている内容です。 イベントの主な用途にはGoogleAnalyticsでツイートボタンをクリックされた回数をトラッキングする等があります。

      window.twttr.ready(function(twttr) {
        window.twttr.events.bind('loaded', function(event) {
          if (!event) return;
          loadedHandler(event);
        });
        window.twttr.events.bind('click', function(event) {
          if (!event) return;
          clickHandler(event);
        });
      });

任意のタイミングでTwitterAPIを初期化して同時に実行したいハンドラーを引数に指定します。

1つ目のハンドラー内でシェアボタンを動的に作ります。twttr.widgets.createShareButton はScripting: Factory FunctionsTweet Button JavaScript Factory Functionで説明されています。 2つ目のハンドラー内でツイートボタンを押してくれた人に向けて感謝のメッセージを動的に表示します。

      initTwitterAPI(
        function(event) {
          window.twttr.widgets.createShareButton(null, document.getElementById('twitter-share-button'));
        },
        function(event) {
          document.getElementById('twitter-share-thanks').innerHTML = "ツイートありがとう!!";
        }
      );

以上でツイートボタンを快適に表示できるようになりました。

すっきり。

CodePenにデモを設置しました。Run Penを押して挙動を確認して下さい。

codepen.io

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

tkosuga.jp

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

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

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

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

  • Google Fonts
  • jQuery
  • Bootstrap
  • FontAwesome

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

高速化前

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/CSSをAmazon 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で作られているサイトであれば以下のようなスライドを読んで理解できる必要があります。

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

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

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

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

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

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

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

夏だしダイエットしたいから痩せれるウェブアプリを作った

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

今年も夏がやってきました。毎年この季節になると思うことはなんでしょうか?

そう、ダイエットですよね。

海にプールにフェスにビアガーデンと夏特有のイベントがあるたびに痩せなきゃ!と気を引き締めるのですが3日後にはすっかり忘れているものです。

毎日のカロリーバランスを消費 > 摂取にすれば自然と痩せられると頭では分かっていても実現するのが難しい。

そんな自分のために、毎日のカロリーバランスを記録して摂取カロリーが消費カロリーを上回らないようにするウェブアプリを開発しました。

tkosuga.jp

使い方はかんたん、摂取カロリーを毎日入力するだけです。

はじめて使うと使い方説明のツアーが始まるので、説明を見て進めるだけで利用開始できます。

このカロリーを記録するダイエット方法をレコーディングダイエットと呼びます。数年前に流行りましたよね。

基礎代謝量と生活強度から日常生活で自然消費されるカロリーを計算するので、摂取カロリーが消費カロリーを上回らないよう生活に気を付けて暮らせば自然と痩せて行く皮算用です。

さらにこのウェブアプリではダイエットの成果をはちみつ・鮭・りんご・どんぐり・おにぎり換算で共有する機能もあるので、痩せてきたら友達に鮭x匹分も痩せたよ!と自慢しましょう。

f:id:tkosuga:20160726214740p:plain ※キャプチャ1「おにぎり40個分のダイエットに成功したよ!やったね!」

f:id:tkosuga:20160726214746p:plain ※キャプチャ2「鮭4匹相当のダイエットに成功だね!すごいね!」

データ保持について

データは全てブラウザ内に保存しています。またダイエットレポートを共有する場合を除き、データを外部に転送・保存など通信していません。複数ユーザーでPC/スマホを共有していてブラウザも共有している場合、共有している他ユーザーに見られてしまう可能性がありますのでご注意下さい。

※プライバシーに関する情報(基礎代謝計算のための体重・年齢・性別)はダイエットレポートの共有および移行データに含まれません。

個人的なダイエット成果と開発の動機

ぼくはレコーディングダイエットのくまスリムの開発を始めた=個人で使い始めた6月~7カ月末の2カ月で80kgから74kgの6kgのダイエットに成功しました。

ダイエット開始初日はスマホのメモアプリで摂取カロリーと消費カロリーを記録して、電卓アプリで合算してカロリーコントロールしていたのですが、2日目には早くも辛くなってきたので以下の3つ要件を満たしたアプリを自作する事にしました。

  1. スマホでお手軽に使える。
  2. ボタンを押すだけでカロリーコントロールできる。
  3. モチベーションを維持できる。

要件を満たしていく中で幾つかの課題を抽出できました。以下解決した課題とその解決方法です。

  1. アプリにするとインストール面倒、保守も面倒
    • 解決方法
      • ブラウザだけで使えるウェブアプリにする。
  2. 読み込みや反応が遅いとストレス
    • 解決方法
      • 外部リソースをとにかく減らす。
      • 動作をとにかく高速化する。
      • AdBlock/コンテンツブロッカー環境で動作するようにする。
  3. 痩せたら褒められたい
    • 解決方法
      • はちみつ・鮭などの謎単位で何だか凄い感を演出。
      • SNS共有機能を付ける(LINE/Facebook/Twitter)。
  4. ポジティブな数字しか見たくない
    • 解決方法
      • 目標値やノルマを表示しない。
      • 現在の体重とか見える所に表示しない。
  5. メンテナンスに苦労したくない
    • 解決方法
      • サーバーサイドを使わずHTMLで完結する形にする。
      • Node.js + gulpでビルドを自動化。
  6. お金かけたくない
    • 解決方法
      • HTTPSには無料のStartSSLを利用。
      • いま使っているさくらのレンタルサーバー(月額数百円)で動かす。
  7. セキュリティ/プライバシーで苦労したくない
    • 解決方法
      • HTTPSの導入
      • ウェブストレージの利用。サーバーサイドで保存しない。
      • 生年月日にニックネーム入力等をしない。

技術的な情報

ここからツール紹介からがらっと変わって技術的な情報です。

StartSSL

本アプリのドメイン(tkosuga.jp)には無料で使えるStartSSLを利用しています。

StartSSL™ Certificates; Public Key Infrastructure

1アカウントで上限5つのサーバー証明書を無料で発行できます。企業情報や個人事業主の情報を入力する必要がありません。クラス1の証明書です。

f:id:tkosuga:20160726223445p:plain

※上記キャプチャはhttps://cryptoreport.websecurity.symantec.com/checker/の結果

せっかくなのでSSL脆弱性診断(SSL Server Test)を試して見ます。Fか。

f:id:tkosuga:20160726224058p:plain

※上記キャプチャはhttps://www.ssllabs.com/ssltest/の結果

SSLハンドシェイクの欄を見るとStartSSLは昔のブラウザに対応していないんですね。

f:id:tkosuga:20160726224307p:plain

※上記キャプチャはhttps://www.ssllabs.com/ssltest/の結果

さくらレンタルサーバーでのSSL転送

本アプリはさくらレンタルサーバーで動作しているのですが、SSLを有効にするとサーバーにはHTTPSが解除された状態で転送されます。リバースプロキシを経由してレンタルサーバーに辿りついているようです。

そのため.htaccess等で「httpならhttpsに転送する設定」をするとリダイレクトループが起こります。これを回避するにはさくらレンタルサーバー特有のカスタムHTTPヘッダに付与されているX-Sakura-Forwarded-Forを利用します。

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteCond %{HTTP:X-Sakura-Forwarded-For} ^$
  RewriteRule ^(.*)$ https://tkosuga.jp%{REQUEST_URI} [R=301,L]
</IfModule>

SVG

本アプリはPNG/JPEG画像等(ラスター画像)を使わずにインラインSVG(ベクター画像)を利用しています。以下のアイコンは全てインラインSVGです。

  • タイトルロゴ(くまのアイコン)
  • カロリー換算(はちみつ・鮭・おにぎり等)
  • 機能アイコン(Font Awesomeアイコン)

SVGアニメーション

ロゴアイコンのアニメーションにhttp://maxwellito.github.io/vivus/を利用しています。

CodePenにロゴアイコンとVivusを使ったデモを設置しました。

codepen.io

Vivusを使うと下のコードだけでSVGアウトラインのアニメーションができます。100msecでポーリングしながら開始と終了を延々とループさせています。

var animationVector = 1;
var vivus = new Vivus('app-icon-svg', {}, function() {
  $('#app-icon-svg').removeClass('app-icon-svg');
});
setInterval(function() {
  if (vivus.getStatus() == 'start') {
    $('#app-icon-svg').addClass('app-icon-svg');
    vivus.reset().play(animationVector = 1);
  } else if (vivus.getStatus() == 'end') {
    $('#app-icon-svg').addClass('app-icon-svg');
    vivus.stop().play(animationVector = -1);
  }
}, 100);

Font AwesomeをSVGに置き換え

Font Awesomeのフォントアイコンを利用するには以下HTMLのように「fa fa-icon-name」とします。お馴染みの形式ですね。

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

本アプリではFont Awesomeのフォントファイルを使わず全てSVGを利用するようにしたので以下のようにしてFont Awesomeのアイコンを表示しています。

<i class="fa-svg"><%- include("font-awesome-svg/cogs.svg") %></i>

CSSでは以下のように文字幅に合わせてSVGの横幅を指定し、横幅に合わせてSVGの高さ上限を指定しています。納得でしょ。

.fa-svg svg {
  color: $text-color;
  display: inline-block;
  max-height: 1.1rem;
  width: 1.1rem;
}

rem単位(root em)についてはMozillaの説明が分かり易いです。

developer.mozilla.org

remは2016年現在の主要ブラウザでサポートされているので利用して問題ないと思います。

http://caniuse.com/#feat=rem

Font-AwesomeアイコンのSVG化には以下を利用しています。

github.com

ウェブストレージ利用で注意した点

本アプリではダイエットデータの保存にWeb Storage APIを利用しています。

developer.mozilla.org

ウェブストレージは便利な反面、もしブラウザに脆弱性が見つかりその範囲にウェブストレージが含まれていればウェブストレージに保管されている重要な情報が漏えいする可能性があります。

そのためダイエットアプリでお約束になっている生年月日の入力・保存といった個人の特定に繋がる情報(他には姓名やニックネーム)を保存しないようにしています。

ウェブストレージ利用のサンプルコード

以下のコードはウェブストレージAPIを使ってハッシュマップをsave/loadするサンプルです。JSON#parseとJSON#stringifyでシリアライズを実現しています。

  function loadFromStorage() {
    return JSON.parse(storage.getItem("options"));
  }
  function saveToStorage(options) {
    storage.setItem("options", JSON.stringify(options));
  }
  var storage = localStorage;
  var options = loadFromStorage();
  saveToStorage(options);

共有するデータのシリアライズ+圧縮

データ共有の仕組みにはウェブストレージに保存しているデータをシリアライズしURLに付与して、他ブラウザで開いた時のそのURLからデータを復元する方法を取っています。

このURLに付与するシリアライズされた文字列を生成するのをブラウザ内のJavascriptで行うのに lz-string を利用しています。ものすごく便利なライブラリです。

github.com

以下lz-string を利用をしたサンプルコードです。文字列をURIセーフな形に圧縮して復元しています。

const lzstring = require('lz-string');
const string = "stringstringstring";
const compressedString = lzstring.compressToEncodedURIComponent(string);
lzstring.decompressFromEncodedURIComponent(compressedString);

短縮URL

データ共有のための短縮URLにはGoogle URL Shortener APIを利用しています。

developers.google.com

ここだけサーバーサイドでrubyのCGIを利用しています。

require 'net/http'
require 'net/https'
require 'yaml'
require 'cgi'

def shorten(original_url)
  req = Net::HTTP::Post.new("/urlshortener/v1/url?key=#{API_KEY}", initheader = {'Content-Type' => 'application/json'})
  req.body = %|{"longUrl":"#{original_url}"}|
  https = Net::HTTP.new("www.googleapis.com", 443)
  https.use_ssl = true
  https.verify_mode = OpenSSL::SSL::VERIFY_NONE
  res = https.start{|http|
    http.request(req)
  }
  hash = YAML.load(res.body)
  hash['id']
end
shorten(URI.unescape(cgi.params['u'].first))

毎日のカロリー変動を見るラインチャート

MetricsGraphics.jsを利用しています。

metricsgraphicsjs.org

CodePenにデモを準備しました。見やすくするため線と文字を太く大きくしています。

codepen.io

四方の余白と高さを指定して2本の線で描画するデータを渡すとキレイなラインチャートが表示されます。

MG.data_graphic({
  data: [intakeKcals, usedKcals],
  width: $(".header").width(),
  height: 90,
  left: 30,
  right: 40,
  top: 10,
  bottom: 30,
  target: ".chart-daily-kcal",
  x_accessor: 'date',
  y_accessor: 'value',
  legend: ['摂取kcal', '消費kcal'],
  colors: ['#e80703', '#909090'],
  //show_secondary_x_label: false,
  //show_confidence_band: ['l', 'u'],
  //x_extended_ticks: true,
});

その日のカロリー摂取量を見るパイチャート

日付の横にある小さいチャートには Peity を利用しています。

http://benpickles.github.io/peity/

CodePenのデモを準備しました。

codepen.io

このチャートライブラリはとても分かり易くて便利で、使い方は以下の通りです。直感的でシンプルですよね。

<span class="pie">1/5</span>
$(function() {
  $("span.pie").peity("pie")
);

スマホのクリックを速くするfastclick

github.com

fastclickを利用してスマホでボタンを押したときの反応速度を向上させています。何をしているのかなと思ってこのライブラリの実装を見てみるとclickやtouchstartのEventListenerを入れ替えたりイベントの伝播に変更を加えたりしています。

あと残りの説明

長くなってきたのでアプリ内で利用していて説明していないライブラリを列挙します。

まとめ

お手軽に使えるダイエットアプリ作ったので使って見てね!

tkosuga.jp

ぼくはこのウェブアプリでダイエットに成功、さらにフロントエンドとNode.js/gulpと今風のサイト高速化にSVGの勉強もできたので万々歳です。

意見要望があれば気軽にブログのコメントやTwitter宛てにコメントをください。