Laravel

Laravelの表示速度をあげたいとき、実装コストに対して効果が高いものはコレ!

いつもご利用ありがとうございます。このブログは、広告費によって運営されています。

オススメ本
Web技術を勉強するなら、かなりオススメの雑誌です。毎月新しい発見があります。ついに最終号・・・、みなさん買いましょう!!
読んで損することはない名著。命名で悩むことが多い人はこの本がオススメです。

⇨ Laravel 記事の目次はこちら

初めまして。Laravel をはじめとして何でも屋的なコードを書いているフリーランスのまっつんです。

Laravel のプロダクトを開発運用していますが、ローンチ当初はかなり表示速度に課題感がありました。

そんな中で、Laravel の表示速度をあげるときにした作業のうち、簡単かつ効果が高いものについて書いていきます。

この記事が参考になるかもしれない人

○ Laravel でプロジェクトをリリースした人

○ 人数が少なくてチューニングする人手がない人

○ 初めてサービスをリリースした人

○ ポートフォリオを作成している人

パフォーマンスの低い原因を知る方法

表示速度が低い原因を調査する方法で最も早いのは、Google が提供している LightHouse か、GooglePageInsights を使うのが手っ取り早いです。

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

この数字が高ければ高いほど良く感じますが、数字自体はあくまで参考程度として、それ以上にパフォーマンスアップのために提案してくれる項目に注目すると良いと思います。

基本的にこの記事の内容は PageSpeedInsights が提供する情報の中から試した方法の中で、効果が高いものについてピックアップする形です。

ちなみに、僕のブログは React のフレームワーク Gatsby で作っているので爆速です(笑)

ページスピードインサイトの画像

gzip で圧縮する

gzip に関しては、ここでは説明しませんが、

サーバー側で早く読み取れるように圧縮したあとに、ユーザーに渡すっていう感じです。

⇨【別サイト】gzip 圧縮転送についてトコトン調べてみた

早速実装方法です。

まず、現在の状況を確認します。

サービスページで、右クリック ⇨ 検証でデベロッパーツールを開きます。

そこで Network タブを表示します。そしたら、一度 F5 を押して、ページ情報を再取得します。

そうすると、項目が多数現れると思いますが、その中から現在のパスを探します(多分一番上にあります)

そのパスをクリックすると、ResponseHeader というタブがあるので、開くと、「content-encoding」という項目があります。

ここが、「圧縮しているか、していないか」を判断できる場所となります。

Gzip 以外にも圧縮方法があるので、他の方法で圧縮している場合は、この項目は飛ばしてください。

何の圧縮もしていない場合

Laravel で Gzip に圧縮する方法はとても簡単です。

public/.htaccess を開きます。そこに追記してください。

# ------------------------------------------------------------------------------
# | Compression                                                                |
# ------------------------------------------------------------------------------

<IfModule mod_deflate.c>

    # Force compression for mangled headers.
    # http://developer.yahoo.com/blogs/ydn/posts/2010/12/pushing-beyond-gzipping
    <IfModule mod_setenvif.c>
        <IfModule mod_headers.c>
            SetEnvIfNoCase ^(Accept-EncodXng|X-cept-Encoding|X{15}|~{15}|-{15})$ ^((gzip|deflate)\s*,?\s*)+|[X~-]{4,13}$ HAVE_Accept-Encoding
            RequestHeader append Accept-Encoding "gzip,deflate" env=HAVE_Accept-Encoding
        </IfModule>
    </IfModule>

    # Compress all output labeled with one of the following MIME-types
    # (for Apache versions below 2.3.7, you don't need to enable `mod_filter`
    #  and can remove the `<IfModule mod_filter.c>` and `</IfModule>` lines
    #  as `AddOutputFilterByType` is still in the core directives).
    <IfModule mod_filter.c>
        AddOutputFilterByType DEFLATE application/atom+xml \
                                      application/javascript \
                                      application/json \
                                      application/ld+json \
                                      application/rss+xml \
                                      application/vnd.ms-fontobject \
                                      application/x-font-ttf \
                                      application/x-web-app-manifest+json \
                                      application/xhtml+xml \
                                      application/xml \
                                      font/opentype \
                                      image/svg+xml \
                                      image/x-icon \
                                      text/css \
                                      text/html \
                                      text/plain \
                                      text/x-component \
                                      text/xml
    </IfModule>
</IfModule>

これをコピペすれば、Gzip で圧縮されます。

先ほどと同じようにデベロッパーツールで確認してみてください。

content-encoding: gzip

と表示されたはずです。

画像の遅延読み込みをする

画像の読み込みは基本的に遅延させます。

80行程度のコードなので、ファイルを作成して、下のコードをコピーしてください。

var _typeof =
  typeof Symbol === "function" && typeof Symbol.iterator === "symbol"
    ? function (obj) {
        return typeof obj;
      }
    : function (obj) {
        return obj &&
          typeof Symbol === "function" &&
          obj.constructor === Symbol &&
          obj !== Symbol.prototype
          ? "symbol"
          : typeof obj;
      };

/*! Lazy Load 2.0.0-rc.2 - MIT license - Copyright 2007-2019 Mika Tuupola */
!(function (t, e) {
  "object" == (typeof exports === "undefined" ? "undefined" : _typeof(exports))
    ? (module.exports = e(t))
    : "function" == typeof define && define.amd
    ? define([], e)
    : (t.LazyLoad = e(t));
})(
  "undefined" != typeof global ? global : this.window || this.global,
  function (t) {
    "use strict";

    function e(t, e) {
      (this.settings = s(r, e || {})),
        (this.images = t || document.querySelectorAll(this.settings.selector)),
        (this.observer = null),
        this.init();
    }
    "function" == typeof define && define.amd && (t = window);
    var r = {
        src: "data-src",
        srcset: "data-srcset",
        selector: ".lazyload",
        root: null,
        rootMargin: "0px",
        threshold: 0,
      },
      s = function s() {
        var t = {},
          e = !1,
          r = 0,
          o = arguments.length;
        "[object Boolean]" === Object.prototype.toString.call(arguments[0]) &&
          ((e = arguments[0]), r++);
        for (; r < o; r++) {
          !(function (r) {
            for (var _o in r) {
              Object.prototype.hasOwnProperty.call(r, _o) &&
                (e &&
                "[object Object]" === Object.prototype.toString.call(r[_o])
                  ? (t[_o] = s(!0, t[_o], r[_o]))
                  : (t[_o] = r[_o]));
            }
          })(arguments[r]);
        }
        return t;
      };
    if (
      ((e.prototype = {
        init: function init() {
          if (!t.IntersectionObserver) return void this.loadImages();
          var e = this,
            r = {
              root: this.settings.root,
              rootMargin: this.settings.rootMargin,
              threshold: [this.settings.threshold],
            };
          (this.observer = new IntersectionObserver(function (t) {
            Array.prototype.forEach.call(t, function (t) {
              if (t.isIntersecting) {
                e.observer.unobserve(t.target);
                var _r = t.target.getAttribute(e.settings.src),
                  _s = t.target.getAttribute(e.settings.srcset);
                "img" === t.target.tagName.toLowerCase()
                  ? (_r && (t.target.src = _r), _s && (t.target.srcset = _s))
                  : (t.target.style.backgroundImage = "url(" + _r + ")");
              }
            });
          }, r)),
            Array.prototype.forEach.call(this.images, function (t) {
              e.observer.observe(t);
            });
        },
        loadAndDestroy: function loadAndDestroy() {
          this.settings && (this.loadImages(), this.destroy());
        },
        loadImages: function loadImages() {
          if (!this.settings) return;
          var t = this;
          Array.prototype.forEach.call(this.images, function (e) {
            var r = e.getAttribute(t.settings.src),
              s = e.getAttribute(t.settings.srcset);
            "img" === e.tagName.toLowerCase()
              ? (r && (e.src = r), s && (e.srcset = s))
              : (e.style.backgroundImage = "url('" + r + "')");
          });
        },
        destroy: function destroy() {
          this.settings && (this.observer.disconnect(), (this.settings = null));
        },
      }),
      (t.lazyload = function (t, r) {
        return new e(t, r);
      }),
      t.jQuery)
    ) {
      var _r2 = t.jQuery;
      _r2.fn.lazyload = function (t) {
        return (
          (t = t || {}),
          (t.attribute = t.attribute || "data-src"),
          new e(_r2.makeArray(this), t),
          this
        );
      };
    }
    return e;
  }
);

lazyload();

最後の、lazyload()の部分でこの関数を呼び出します。

layouts などで、

    <script src="{{ asset('js/lazyload.min.js') }}" async></script>

を記述し、js ファイルを読み込みます。

そしたら、あとは img タグを修正するだけです。

<img
  class="lazyload"
  loading="lazy"
  src="{{ asset('/images/lazyload.jpg') }}"
  data-src="{{ asset('/ここに表示させたい画像') }}"
/>

この4項目は全て必要になります。

class に「lazyload」というクラスを指定します。js でこのクラスに対して処理をする記述がしてあるためです。

loading=“lazy”属性を付けます。

src=""には、めちゃくちゃ小さな画像ファイルを用意して指定します。img タグには src にファイルを指定しないとエラーになるので、何かしら小さな(1バイトとかの)画像ファイルを用意して読み込ませます。

data-src=""には、本来表示させたい画像のパスを書きます。

以上で、遅延読み込みができます。

画像を圧縮してできるだけ小さくする

さきほどの項目で、画像の遅延読み込みをしましたが、画像そのもののファイルサイズを減らすこともとても効果的です。

結局いずれ読み込むファイルなので、遅延で読み込むにしてもファイルサイズを減らした方が良いです。

webp というファイル形式があります。

以前こういうツイートをしました。サービスで投稿される画像を全て Webp に圧縮するシステムに変更し、縦横比率も表示のピクセルの比率に完璧に合わせて無駄を減らした形です。

WebP とは

「うぇっぴー」と読むようです笑

WebP は、Google が開発している画像ファイルのフォーマットです。拡張子は.webp です。

肌感ではありますが、ツイートのとおり jpg よりも50%前後ファイルが小さくなるにもかかわらず、画質はほとんどかわりません(僕の目には同じに見えます)

これを、

① スマホ用サイズの webp

② パソコン用サイズの webp

③webp に対応していないブラウザ用の jpg

の3種類用意してそれぞれを表示するようにします。

<picture>
  <source
    type="image/webp"
    media="(max-width: 480px)"
    srcset="スマホ用WebP画像"
  />
  <source type="image/webp" srcset="パソコン用WebP画像" />
  <img
    loading="lazy"
    class="lazyload"
    data-src="webp非対応ブラウザ用jpg画像"
    src="lazyload.jpg"
    alt=""
  />
</picture>

picture タグを使って、分岐させれば OK です。

検索結果のキャッシュ化

Laravel では、データベースの検索結果をキャッシュ化させることがとても簡単にできます。

例えば、30 分に1回計算して、その計算結果をユーザーに提供し続けると言うことです。

ホーム画面などでは、色んなところからデータを引っ張ってこないといけないので、めっちゃ計算が必要になったりします。

それをキャッシュ化させます。

デメリットは、30 分間は同じ結果が表示され続けるので、リアルタイムに新着を更新させたい場合などには向かないかもしれませんが、それでも数分に1回計算するという記述にしておけばそこまでユーザビリティを下げることにはならないと思います。

それ以上にホームの多すぎる計算を全てのユーザーでやる方が良くないと僕は思います。

検索結果のキャッシュ化の方法

Laravel 公式ドキュメント

$value = Cache::remember('users', $seconds, function () {
    return DB::table('users')->get();
});

保存したい計算結果を return します。

第一引数の users はキャッシュの名前、第二引数の$seconds は、キャッシュを再計算するまでの時間を指します。

これが便利なところは、「キャッシュがなければ再計算」してくれるところです。

ありがてえ〜〜〜!!

Javascript や CSS を圧縮する、遅延読み込みする

Laravel-mix を使って、Javascript や CSS を圧縮すると良いです。

Javascript や CSS はできるだけ遅延読み込みします。例えば、Google タグマネージャーの script には async を付けて遅延読み込みします。

Google タグマネージャーの管理画面でも、「ページが読み込み終わったら」という発動条件にすることによって最初のページの読み込み速度に影響が出なくなります。

まとめ

いかがだったでしょうか?

ここに挙げた項目は、パフォーマンス改善では一部だとは思いますが、マンパワーをかけられないプロダクトでは、この項目だけでも見直せば表示速度が爆上がりするはずです。

僕のプロダクトでも3とかから20〜30に上がりました。画像枚数が多かったり DOM 自体が膨大な Home だと中々ポイントをあげるのは難しいですが、表示速度は明らかに向上して、読み込みが長すぎることによる離脱は少なくなるはずです。

ページ読み込みは「2 秒以内」に - 3 秒待てないモバイルユーザー、画像圧縮で表示速度改善を

このノウハウを生かして、ホームページの表示速度の改善の案件も受けています(連絡は、いつでもできますが、作業は平日の夜または土日となります)

現在1社の改善をして、CLS というウェブ表示に関する指標を真っ赤の状態から良好にすることができました。

ワードプレスに関しても知見がありますので、ページのホーム以外でも個別ページやコラムの詳細ページのパフォーマンス向上が可能です。

興味がある方は、Twitter の DM か、コンタクトにあるメールアドレスにご連絡くださいませ。