OHEYAGOでファーストビューでのJavaScript容量を28%削減した話

はじめに

OHEYAGOの開発をしている田渕です。特にフロントエンド専門というわけではないのですが、最近はフロントエンドのチューニングをたくさんやっていたので、フロントエンド関連の記事が多くなりそうです。

サイトの速度はSEOなどの面からも重要で、その中でも特にJavaScriptの容量はボトルネックになりやすいため、特にtoCのサイトでは気にかけておく必要があります。

OHEYAGOでは、あまり気にしていない状態でリリースしてしまいましたが、リリース後にかなり注力してJavaScriptの容量の削減に努めたので、その記録を残しておきます。

JavaScriptの容量問題で悩んでいる方の参考になれば幸いです!

結果

※この記事では、全てgzipped-sizeで測定しています。また、途中でその他の開発も行っているため、必ずしも正確な数字ではないことに注意してください。

測定はwebpack-bundle-analyzerで行っています。

全ファイルを合計した容量が471.7 KBから449.52 KBまで減少しました。
OHEYAGOではmulti_entrypointsを採用しているので、特にUXの上で重要と言われているファーストビューでの容量に注目すると、176.53 KBから127.85 KBまで、なんと28%弱の容量削減に成功しました。

lodash-webpack-pluginの導入

lodash-webpack-pluginを導入するだけで、471.7 KB → 448.45 KBになりました。

CSS modulesのclassNameのminify

OHEYAGOではCSS modulesを採用しているのですが、production環境ではclassNameを短くするだけで、448.45KB → 439.43 KBになりました。
差分は以下のとおりですが、意外なほど効果がありました。

- modules: { localIdentName: '[local]--[hash:base64:8]' },
+ modules: { localIdentName: '[sha1:hash:hex:4]' },

ただ、もちろん本番でのデザイン崩れの修正などの難易度は向上します。
デザイナーと相談の上、ローカルでデザイン崩れを再現できないことが基本的にはないと判断し、導入しました。

lodash自体の削除

lodashの中で使っている関数がisEmptyだけだったので、TypeScriptの力を借りつつちゃんと書き換えて、lodash自体を削除してしまいました。
型がちゃんとついていたので、引数がArrayなのかStringなのかundefinedなのかすぐ分かりました!

「その変数が空のArrayか、もしくはundefinedの場合」みたいな分岐が多く、そもそもの書き方が良くなかったので、リファクタリングも兼ねて修正しました。

こちらに関しては、同時に起こった他の変更がかなり大きかったのであまり参考になる測定ができませんでした。
元からlodash-webpack-pluginでかなり削減されていたので、それほど大きな削減には繋がらなかったと記憶しています。

moment.jsからday.jsへの乗り換え

要件上、moment.jsではなくてday.jsで十分だったので、乗り換えました。438.05 KB → 422.97 KBになりました。

google-map-reactの自前実装

google-map-reactというpackageを使っていましたが、以下のような理由で削除しました。

  • OHEYAGOでは殆どの機能を使っていなかったため、容量が勿体なかった
  • 最近はあまりメンテナンスされていない
  • 代替packageも同じような状況で、特に使いたいものがなかった
  • 一つのコンポーネントからのみ依存していたので、安全に削除できる

その結果、422.97 KB → 412.18 KBの削減に成功しました。

こちらの実装を参考にしつつ、TypeScriptとReact Hooksを利用して書いたものが以下になります。

import * as React from 'react';
import styles from './Map.scss';

interface Props {
  id: string;
  options: {};
  onMapLoad: (map: google.maps.Map) => void;
}

export default function Map(props: Props): JSX.Element {
  const { id, options, onMapLoad } = props;

  React.useEffect(() => {
    const onScriptLoad = (): void => {
      const elm = document.getElementById(id);
      if (elm !== null) {
        const map = new window.google.maps.Map(elm, options);
        onMapLoad(map);
      }
    };
    const API_KEY = 'OHEYAGOGOGOGOGOGOGOGOGOGOGOGOGO';

    if (!window.google) {
      const s = document.createElement('script');
      s.type = 'text/javascript';
      s.src = `https://maps.google.com/maps/api/js?key=${API_KEY}`;
      const x = document.getElementsByTagName('script')[0];
      if (x.parentNode !== null) {
        x.parentNode.insertBefore(s, x);
        s.addEventListener('load', () => {
          onScriptLoad();
        });
      }
    } else {
      onScriptLoad();
    }
  }, [id, onMapLoad, options]);

  return <div className={styles.Map} id={id} />;
}

結果

主にvendor.js(外部ライブラリ)の容量削減に成功しました。
vendor.jsにのみ注目すると、146.69 KB → 96.86 KBです。

f:id:ktabuchi:20200102211354p:plain
容量削減前
f:id:ktabuchi:20200102211411p:plain
容量削減後

容量の差が目で見えて面白いですね。特に容量の大きかったMoment.jsとlodashが消えたのが、見て取れると思います。

終わりに

基本的には、まずは大きなpackageを減らしていくことになると思います。測定→改善のサイクルを回しましょう。
特にlodashとmoment.jsは容量が大きいので、lodashを日頃からあまり使わないようにしてあわよくば脱却する、moment.jsはday.jsに置き換えられないか考える、の2つはまず試して損はないと思います。

今回のgoogle mapのように、安易にライブラリに頼らずに、自前で実装するという選択肢も重要だと思います。(デメリットも結構あるので、あくまでも選択肢としてです)

これ以外にも色々改善した結果、実際のpage speedも大幅に向上させることに成功したので、後日記事にしようと思います!