maesblog

turbolinksで静的サイトを擬似SPA化 – JSerによる導入Tipsを紹介

今回のこのブログにturbolinksを導入しました。Googleのモバイルファーストインデックスなども始まり、以前よりもサイトのパフォーマンスの重要性が高まってきていると言うこともあっての導入です。turbolinksはRuby on Railsのイメージが強いかと思いますが、JavaScript版もリリースされていて、どんなサイトにも導入が可能です。フロントエンドエンジニアを名乗っている以上、ツールがなんであれ使いこなしてなんぼだと思うので、このブログを使って試行錯誤しながらいろいろ試しました。その中で実装のポイントとなると思ったTipsをいくつか紹介したいと思います。

はじめに – turbolinksとは

turbolinksは、Ruby on Rails 4からデフォルトの機能となったGemで、Railsアプリのパフォーマンスを向上させるためのツールとして登場しました。ただJavaScriptの実装に慣れていないと、思ったようにJavaScriptで実装した機能が動かなくなったりするため、よくRailsのプロジェクトを始める際には、「まず何をするか?turbolinksを解除でしょ!」と言ってturbolinksの使用を辞めてしてしまうという話をよく聞いたりします。

turbolinksの仕組み

そんなturbolinksとは何者なのか、まずざっくり言うと、サーバー側でレンダリングされたHTMLを使用したWebサイト(静的サイト)に、SPA(シングルページアプリケーション)のような高速なページ遷移の機能を導入するためのツールです。つまり、静的サイトをまるでSPAのようなサイトにしてくれるツールです。仕組みとしては、ページ内の同一ドメインのリンクをクリックした際に、そのままページ遷移するのではなく、遷移先のページを非同期通信でサーバーから取得し、<body>の部分(コンテンツの部分)のみを置き換えるようになっています

一方、<head>の部分は変更がなければ更新しません(変更があった部分のみマージします)。一般的にサイトのパフォーマンスを低下させる要因のひとつとして、外部ファイルの取得が挙げられます。取り込むファイルの容量やサーバーへのリクエストの回数が増えると、それだけサイトのパフォーマンスは悪くなります。こうした外部ファイルの取得する記述を<head>内にしておけば、ページ遷移があった場合でも再取得するリクエストが投げられなくなり、それによりサイトのパフォーマンスを向上させてくれると言うわけです。いわゆるpjaxの仕組みと同じようなもので、History APIをゴニョゴニョいじくりまわして、まるでSPAのような動きを実現させています。

turbolinksを導入する

turbolinksはnpmのパッケージ版も用意されており、Railアプリでなくても導入が可能です。導入はすごく簡単で、turblolinksのモジュールを読み込んでTurbolinks.start();を実行するだけでOKです。

/** turbolinksをインポート */
import Turbolinks from 'turbolinks';

/** turbolinksを起動 */
Turbolinks.start();

順番が逆になりますが、以下のコマンドでプロジェクトディレクトリ内にturbolinksをインストールします。

$ npm install --save turbolinks

開発環境としては、History APIを使っていることから、ローカルサーバが必要となります。お手軽に開発するなら、Parcelhttp-serverなどを使うとよいでしょう。

turbolinksを導入する難易度

なお、上記したように、いきなりサイトに導入してしまうと、場合によってはJavaScriptで書いた機能が動かなくなります。Google AdSenseやAnalyticsなども同様です。従って、導入は簡単ですが、JavaScriptをふんだんに使ったサイトと共存させるにはそれなりにテクニックが必要で敷居は高いかと思います。他のJavaScriptライブラリとの相性も悪かったりするので、このブログにturbolinksを導入する際にも、エラーや警告が頻発し、途中で泣きそうになりました。

また、turbolinksはSPAのような動きを実現してくれると言いましたが、つまりライフサイクルを意識する必要があり、それなりのSPA構築のスキルが求められます。AngularやReactのようなJavaScriptフレームワークを普段使っていれば特に問題はないかと思います。

turbolinksの開発Tipsを紹介する前に

前置きが長くなりましたが、これより私がこのブログにturbolinksを導入するに当たって、ポイントとなりそうだと思った導入テクニックを紹介していきたいと思います。

なお、Tipsの説明していくにあたり、turbolinksの基本的な部分は細かく説明しないかもしれないので、意味がわからない部分があれば、turbolinksの公式のドキュメントなどをご確認ください。特にライフサイクルの部分はturbolinksの肝になる部分ですが、それが故にここでは詳しく説明しません。

それから、これから説明するコードは、最終的にbundleして、head要素内で読み込むいわゆるbundle.jsファイルに含まれることになるコードとなります。body要素内に書くとturbolinksの効果が発揮されないので、お気をつけください。

turbolinks対応のブラウザを判定する

turbolinksは、HTML5 History APIWindow.requestAnimationFrameに依存しているため、これらをサポートしていないブラウザでは機能しません。

ブラウザのサポート状況は以下の通りです。

そこで、対応ブラウザとそうでないブラウザで処理を分ける場合はTurbolinks.supportedを使います。ブラウザのサポート状況を検知してくれるプロパティです。以下のように使います。trueであれば、turbolinksに対応、falseであれば未対応と言うことになります。

if (Turbolinks.supported) {
  /** turbolinks対応のブラウザ向けの処理 */
} else {
  /** turbolinks未対応のブラウザ向けの処理 */
}

また、ブラウザのサポートに加えて、明示的にturbolinksの使用をコントロールしたい時は、以下のように変数を用意するとよいかと思います。

let shuoludTurbolinks = true; // 使用したくない場合はfalse

if (Turbolinks.supported && shouldTurbolinks) {
  /** turbolinksを使用する場合の処理 */
} else {
  /** turbolinksを使用しない場合の処理 */
}

Google Analyticsに対応させる

Google Analyticsの対応は比較的簡単にできます。Google AnalyticsのヘルプページでもSPAへの対応方法について説明されています。この方法を応用することで、turbolinksに対応させることができます。

通常のGoogle Analyticsのコード

まずは通常のGoogle Analyticsのコードを以下に提示しておきます。ちなみにGoogle Analyticsのコードは昨年の後半よりgtag.jsによる新方式に変更されています。

<!-- Global site tag (gtag.js) - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=UA-XXXXXXXX-1"></script>
<script>
  window.dataLayer = window.dataLayer || [];
  function gtag(){dataLayer.push(arguments);}
  gtag('js', new Date());

  gtag('config', 'UA-XXXXXXXX-1');
</script>

対策後のコード

対策後のコードは以下のようになります。詳細はコメントをお読みください。

/**
 * Google Analyticsを表示させるための関数
 */
function callAnalytics() {
  /** Urlのパスを取得 */
  const path = window.location.pathname;
  /** Urlのクエリを取得 */
  const params = window.location.search;

  /** Google Analyticの処理 */
  window.dataLayer = window.dataLayer || [];
  function gtag() {
    window.dataLayer.push(arguments);
  }
  gtag('js', new Date());
  /** オプションに通知するページのURLを指定 */
  gtag('config', 'UA-XXXXXXXX-1', { page_path: path + params });
}

/**
 * ページがロードされた時に呼び出される処理
 */
document.addEventListener('turbolinks:load', () => {
  /** cllAnalytics()を呼び出す */
  callAnalytics();
}, false);

Google Analyticsのscriptファイルの読み込みはhead要素内で行うようにします。

<head>
  <link rel="stylesheet" href="/assets/bundle.css">
  <script src="/assets/bundle.js" defer></script>
  <script src="https://www.googletagmanager.com/gtag/js?id=UA-XXXXXXXX-1" defer></script>
</head>

実装のポイント

ページがロードされると、gtag()が呼び出され、Google Analyticsに通知が飛ぶようになります。その際に、通知するページのURLを、オプションのpage_pathパラメータの値に指定します。クエリレベルで判別したい場合など、状況に応じてwindow.locationを使ってURLから必要な情報を取得してください。

なお、上記のようにturbolinks:load'のタイミングでcallAnalytics()を呼び出すと、ブラウザの「戻る」や「進む」を押した際にもGoogle Analyticsに通知が飛びます。もし、このタイミングで通知したくない場合は、'turbolinks:before-visit'のタイミングでcallAnalytics()を呼び出すようにしてください。ブラウザの「戻る」や「進む」では呼び出されなくなります。

document.addEventListener('turbolinks:before-visit', () => {
  callAnalytics();
}, false);

analytics.jsを使用する場合

gtag.js以前のanalytics.jsをturbolinksに対応させるには、callAnalytics()の中を以下のようにします。

function callAnalytics() {
  /** Urlのパスを取得 */
  const path = window.location.pathname;
  /** Urlのクエリを取得 */
  const params = window.location.search;

  /** オプションに通知するページのURLを指定 */
  window.ga('send', 'pageview', path + params);
}

analytics.jsファイルは、同様にhead要素内で読み込むようにします。analytics.jpのSAP対応については以下も合わせてご確認ください。

以上、Google Analyticsをturbolinksに対応させる方法となります。

Google AdSenseに対応させる

Google AdSense対応はけっこう難易度高いかと思います。ネットで調べると、やはり悩んでいる方がたくさんいました。対策方法としてよく紹介されていたのが、以下のサイトでした。

ただ、こちらのサイトで紹介されている方法は、コード量が多く、しかもCoffeeScriptで書かれているので、解読する気にもならず、独自実装を試みました。ちょうど以前にReactで作ったSPAのサイトにGoogle AdSenseを表示させたことがあったので、その方法を応用することにしました。

ちなみに以下は、上記のCoffeeScriptのコードをJavaScriptに変換したものになります。

通常のGoogle AdSenseのコード

まずは通常のGoogle AdSenseのコードを以下に提示しておきます。お馴染みのコードですね。

<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>
<!-- xxxxxx -->
<ins class="adsbygoogle"
     style="display:block"
     data-ad-client="ca-pub-xxxxxxxxxxxxxxxx"
     data-ad-slot="xxxxxxxxxx"
     data-ad-format="auto"></ins>
<script>
(adsbygoogle = window.adsbygoogle || []).push({});
</script>

対策後のコード

対策後のコードは以下のようになります。詳細はコメントをお読みください。

/**
 * Google AdSenseを表示させるための関数
 */
function callAdSense() {
  /** ins要素を取得 */
  const ads = document.querySelectorAll('.adsbygoogle');
  /** ins要素に子要素があれば削除 */
  ads.forEach((ad) => {
    if (ad.firstChild) ad.removeChild(ad.firstChild);
  });

  /** ページ内にins要素が存在すれば、広告を注入 */
  if (ads.length > 0) {
    ads.forEach(() => {
      window.adsbygoogle = window.adsbygoogle || [];
      window.adsbygoogle.push({});
    });
  }
}

/** turbolinksを起動 */
Turbolinks.start();

/** callAdSense関数を呼び出すかどうかのフラグ */
let shouldCallAdSense = true;

/**
 * ページがロードされた時に呼び出される処理
 */
document.addEventListener('turbolinks:load', () => {
  if (shouldCallAdSense) {
    /** cllAdSense()を呼び出す */
    callAdSense();
    shouldCallAdSense = false;
  }
}, false);

/** 
 * ページに訪れる前に時に呼び出される処理
 * ※ブラウザの「戻る」「進む」では呼び出されない
 */
document.addEventListener('turbolinks:before-visit', () => {
  shouldCallAdSense = true;
}, false);

実装の際の注意点

ここで注意しなければいけないのが、Google AdSenseの表示先となるins要素にすでに広告が存在している時に、window.adsbygoogle.push({});を呼び出さないようにするということです。以下のようなエラーが出ます。

adsbygoogle.push() error: All ins elements in the DOM with class=adsbygoogle already have ads in them.

これを避けるために、window.adsbygoogle.push({});を呼び出す前にins要素の子要素を削除したり、ブラウザの「戻る」「進む」を押した場合は、callAdSense()を呼び出さなようにしています。

HTML側の対策

adsbygoogle.jsファイルの読み込みはhead要素内で行うようにします。

<head>
  <link rel="stylesheet" href="/assets/bundle.css">
  <script src="/assets/bundle.js" defer></script>
  <script src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js" defer></script>
</head>

実際に広告を表示させる箇所はins要素のみ残した状態にします。

<!-- xxxxxx -->
  <ins class="adsbygoogle"
   style="display:block;"
  data-ad-client="ca-pub-xxxxxxxxxxxxxxxx"
  data-ad-slot="xxxxxxxxxx"
  data-ad-format="auto"></ins>

以上、Google AdSenseをturbolinksに対応させる方法となります。ページ遷移と同時に広告も表示され、エラーもおそらく出ないようになっているかと思います。ここは私もけっこう悩んだところなので、もっと良い方法があればお知らせいただければと思います。

JavaScript側からturbolinksのページ遷移を行う

turbolinksは基本的には同じドメインのリンク先が指定された<a href>にのみ機能します。しかし、任意の要素をクリックなどしたタイミングで、JavaScript側からページ遷移させたい場合などもあるかと思います。

その際には、JavaScript側で以下のようにTurbolinks.visit(location)メソッドを呼ぶことで、JavaScript側からturbolinksのページ遷移を行うことができるようになります。引数には遷移先のページのURLを指定します。

Turbolinks.visit('./index.html');

以下は、あるボタンをクリックした際にJavaScript側からページ遷移を行う実装例です(処理は関数として外出ししてもOKです)。

document.addEventListener('turbolinks:load', () => {

  button.addEventListener('click', (event) => {
    event.preventDefault();
    Turbolinks.visit('./index.html');
  }, false);

}, false);

ポイントは、clickのイベントリスナーを'turbolinks:load'のタイミングで呼び出すようにすることです。状況に応じて、event.preventDefault();を呼ぶようにして、従来の処理を停止するようにします。

ページ内アンカーリンクはturbolinksの対象から外す

turbolinksでは、ページ内のアンカーリンクにも機能してしまいます。つまり、他のページへの遷移時と同じようにコンテンツを取得するためのサーバーへの非同期通信が発生してしまいます。必要のないサーバーへのリクエストは避けたいところです。

turbolinksは、<a href>data-turbolinks="false"を指定することでリンク単位で無効にすることができます。つまりアンカーリンクにこのdata-turbolinks="false"を指定すれば問題は解決です。しかし今回当ブログにturbolinksを導入するにあたり、記事内に含まれているアンカーリンクひとつひとつを探して、対応するのは効率的ではありませんでした。

そこで、以下のようにJavaScriptで制御するようにしました。リンクをクリックした時に、そのリンクがアンカーリンクであれば、data-turbolinks="false"を付与するようにしました。

/**
 * アンカーリンクにdata-turbolinks="false"を付与する
 */
function addTurbolinksFalseToAnchorLinks() {
  /** a要素を取得 */
  const links = document.querySelectorAll('a');

  links.forEach((link) => {
    /** リンクがクリックされた時 */
    link.addEventListener('click', (event) => {
      /** リンクに「#」が含まれていた場合 */
      if (link.href && link.href.match(/[#]/)) {
        /** リンクにdata-turbolinks="false"を付与 */
        event.target.setAttribute('data-turbolinks', 'false');
      }
    }, false);
  });
}

/** ページロード時にaddTurbolinksFalseToAnchorLinksを呼ぶ */
document.addEventListener('turbolinks:load', () => {
  addTurbolinksFalseToAnchorLinks();
}, false);

アンカーリンククリック時にのみscroll-behavior: smooth;を適用させる

当ブログでは、アンカーリンククリック時のスムーズスクロール対応としてcssのscroll-behavior: smooth;を使用しています。

html要素に対して、以下のスタイルを適用させています。

html {
  overflow-y: scroll;
  scroll-behavior: smooth;
}

ここで問題なのは、turbolinksのページ遷移が発生した時に、スクロール位置がtop(最上部)に変更されることです。ページ遷移だろうがなんだろうがスムーズスクロールが発生してしまいます。

そこで、上記の「ページ内アンカーリンクはturbolinksの対象から外す」のコードに、アンカーリンクであれば、html要素にscroll-behavior: smooth;を指定し、アンカーリンクでなければ、scroll-behavior: auto;を指定するような処理を追加しました。

/**
 * アンカーリンクにdata-turbolinks="false"を付与する
 */
function addTurbolinksFalseToAnchorLinks() {
  const links = document.querySelectorAll('a');
  /** html要素を取得 */
  const html = document.querySelector('html');
  links.forEach((link) => {
    link.addEventListener('click', (event) => {
      if (link.href && link.href.match(/[#]/)) {
        /** html要素にscroll-behavior: smooth;を指定 */
        html.setAttribute('style', 'scroll-behavior: smooth;');
        event.target.setAttribute('data-turbolinks', 'false');
      } else {
        /** html要素にscroll-behavior: auto;を指定 */
        html.setAttribute('style', 'scroll-behavior: auto;');
      }
    }, false);
  });
}

/** ページロード時にaddTurbolinksFalseToAnchorLinksを呼ぶ */
document.addEventListener('turbolinks:load', () => {
  addTurbolinksFalseToAnchorLinks();
}, false);

任意の要素を更新させないようにする(DOMの恒久化)

turbolinksを使うとSPAのようなサイトを実現してくれるということは、コンテンツ内の更新したい部分だけ更新させることも可能です。つまり、head要素と同じように、ページ遷移時に更新させたくない要素を指定することができます。例えば、サイトのヘッダーやフッター、サイドバーなどすべてのページで共通なコンテンツ以外の部分に適用するとよいでしょう。

以下のように任意の要素にiddata-turbolinks-permanentを付与すると恒久化させることができます。

<header id="header" data-turbolinks-permanent>
  常に更新しなくてもよい内容
</header>

なお、JavaScriptから動的にdata-turbolinks-permanentを付与したい場合は以下のように書くとよいでしょう(処理は関数として外出ししてもOKです)。

document.addEventListener('DOMContentLoaded', () => {
  /** 恒久化したい要素を取得 */
  const header = document.querySelector('#header');
  const category = document.querySelector('#navi');
  const sidebar = document.querySelector('#sidebar');
  const footer = document.querySelector('#footer');

  /** 取得した要素にdata-turbolinks-permanent="true"を付与 */
  [header, navi, sidebar, footer].forEach((elem) => {
    elem.setAttribute('data-turbolinks-permanent', true);
  });

  /** turbolinksを起動 */
  Turbolinks.start();
}, false);

恒久化した要素はページ遷移ごも保持されるため、これらの要素に対する変更はページ遷移後に再適用する必要はないということなので、DOMContentLoadedのタイミングで、適用させたい要素を取得し、それぞれにdata-turbolinks-permanentを付与するようにしています。その後にTurbolinks.start();を呼び出しています。

turbolinksによるページ遷移のreferrer(履歴)を記録する

JavaScript側で直前のページに戻るための処理を書くためにwindow.referrerを扱いたい時があるかと思います。しかしturbolinksで遷移したページはwindow.referrerでは取得できません。

そこで、turbolinksの履歴を扱いたい場合は、ページ遷移のタイミング(ページに到達する前のタイミング)で、その都度window.referreに現在のURL(window.location.href)を登録するようにしてあげます。

document.addEventListener('turbolinks:before-visit', () => {
  window.referrer = window.location.href;
}, false);

以下のようにwindow.referrerでturbolinksの履歴を取得することができるようになります。Turbolinks.visit()で直前のページに戻らせることも可能です。

const referrer = window.referrer;
Turbolinks.visit(referrer);

まとめ

 

長くなりましたが、以上が、私がこのブログにturbolinksを導入した際に使用したテクニックとなります。<head>内がいじれるものであれば、一般的なブログサービスなどでもturbolinksは導入できるかと思います。使えそうな部分があれば、ぜひ参考にしていただければと思います。

turbolinksを導入した結果

このブログ(WordPressのテーマ)にturbolinksを導入した感想としては、最初にcssファイルやjsファイルをhead要素で読み込むようにしたので、やはり初期表示は若干遅くなったかなという印象です。ただページ遷移に関しては、サーバーへのリクエストの数も極端に減り、リンクをクリックしたと同時に切り替わるようになったので、turbolinksの効果が出ているかなと思っています。

以下は、turbolinksでトップページに遷移した時のサーバーリクエストの状況になります。毎回若干結果は異なりますが、リクエスト数は19で、転送量は19.6kbとなっています。

tubolinks導入後のサーバーリクエスト回数

やはりGoogle AdSenseの広告を表示していると、リクエスト回数はかなり増えます。その辺りを気にする場合は、Google AdSenseの表示を調整する必要が出てきますかね。

それから、初回ロード時のパフォーマンスとして、GTMetrixであるページのページスピードを計測した結果が以下です。

turbolinks導入後のページロード時のpagespeed計測結果

やはり、初回ロード時のパフォーマンスには特にturbolinksの影響はなさそうですね。そういうことからも、turbolinksはリピータも多く、1人当たりのページ閲覧数の多いサイトなどに向いていると言えますかね。

今後の予定

このブログは、検索エンジン経由のアクセスが多く、ページの回遊率はそこまで高くないので、そこまでturbolinksの恩恵を受けることは少ないかと思います。場合によっては、元の状態に戻すかもしれませんが、エラーもそこまで出なくなりましたし(YouTubeを表示するとエラーが出ます)、しばらくは使い続ける予定です。

今回turblinksを導入したのは、このブログのパフォーマンスを上げたいという目的もありましたが、もうひとつの理由として今年の1月30日にv1.0がリリースされたStimulusというフレームワークを使ってみたいというのがありました。まだjQueryで実装している部分が残っているので、その辺りをStimulusで試しに置き換えてみようと思っています。Stimulusの話も書ければ、このブログで紹介していきたいと思います。

コメント

  • 必須

コメント