5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

Webフォントを軽くする方法(Googleフォントを無料でダイナミックサブセット化)

Last updated at Posted at 2021-03-08

Webフォントを軽くする方法(Googleフォントを無料でダイナミックサブセット化)

Googleフォントで読み込む文字を指定可能に!

Google Chromeのエンジニア マネージャーのツイートにて発表がありました。

方法(静的サブセット)

↓軽量化前

<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Potta+One&display=swap" rel="stylesheet">

↓軽量化後

<link rel="preconnect" href="https://fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css2?family=Potta+One&display=swap&text=ABCD" rel="stylesheet">

googleフォントを読み込むためのlinkタグのうち、href属性がhttps://fonts.gstatic.comでないほうのlinkタグにある、href属性値の末尾に「&text=」と追記して、その直後にURLエンコード化した文字(上記例ではABCD)を記述するだけです。

上記方法のデメリットと対策

あらかじめ読み込む文字を把握する必要があるため、変化の少ないサイト名などの単語を軽量化対象とするしかないのではないかと思う。
目標はサイト全体で更新のたびに流動的に変わる文字なども自動で軽量化(動的サブセット)して読み込みたい。
動的サブセットできるフォントサービスはAdobeなど有料のものが多いができれば無料で実現したい。
javascriptのinnerTextという機能で画面表示されている文字のみ抽出→URLエンコード化して、「&text=」の後に追記すれば良いが、それだとページ読み込み後に追加した要素の文字などにはGoogleフォントが適用されない。
そこでjavascriptのMutationObserverという機能を使ってページ読み込み後に追加した文字も察知してフォントに適用させようと思いました。

方法(動的サブセット)

<!DOCTYPE html>
<html lang="ja">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>テスト</title>
  <link rel="stylesheet" href="style.css">
</head>

<body>
  body直下の文字 (変化前)
  <button id="btn1">要素の属性を変更する(style属性)</button>
  <button id="btn2">要素の構造を変更する(文字変更含む)</button>
  <button id="btn3">要素の文字列のみを変更する</button>
  <div id="div">最初の文字</div>
  <p id="p">
    親要素
    <p id="p2">子要素</p>
  </p>
  <script src="script.js"></script>
</body>

</html>
style.css
/*↓googleフォントのサイトからcss指定をコピペ*/
body {
  font-family: 'Potta One', serif;
}

/*↓上記指定でフォントが適用されない要素は追記で指定*/
button {
  font-family: 'Potta One', serif;
}

body>*+* {
  margin-top: 20px;
}

button {
  display: block;
  width: 300px;
}

# p,
# p2 {
  display: none;
}
script.js
    //↓HTMLの要素構築が完了してから
    document.addEventListener("DOMContentLoaded", function (event) {

      //↓監視ターゲット設定
      const target = document.body;

      //↓画面上に表示されている文字のみ取得
      let before_text = target.innerText;

      //↓正規表現で改行もしくは空白文字を指定し、それらをbefore_textから除外
      const ptn = /\n|\r\n|\r|\s/g;
      before_text = before_text.replace(ptn, '');

      //before_textを一文字ごとの配列にしておく(後ほど利用します。「let before_text_ary = before_text.split('')」とするとサロゲートペア対策ができないので却下)      
      let before_text_ary = Array.from(before_text);

      //↓googleフォントを読み込むためのlinkタグのうち、href属性がhttps://fonts.gstatic.comでないほうのlinkタグのhref属性値を代入
      const css_url = "https://fonts.googleapis.com/css2?family=Potta+One&display=swap";

      //↓googleフォントを読み込むためのlinkタグの前半部分を作成
      const css_before = '<link rel="preconnect" href="https://fonts.gstatic.com"><link href="' + css_url +
        '&text=';

      //↓googleフォントを読み込むためのlinkタグの後半部分を作成
      const css_after = '" rel="stylesheet">';

      //↓画面上に表示されている文字のみを読み込む(ダイナミックサブセッティング)
      document.querySelector("head").insertAdjacentHTML('beforeend', css_before + encodeURI(before_text) + css_after);


      //↓監視時変更が起きたら実行する
      const observer = new MutationObserver(records => {

        //↓追加された文字を保存しておくために用意
        let add_text = "";

        //↓監視に引っかかった変更点ごとに実行
        for (const record of records) {

          //↓起こった変更の種類をブラウザの検証ツールに表示
          console.log("■type=" + record.type);


          //↓起こった変更の種類がchildList(監視対象の子要素が増減)の場合
          if (record.type === "childList") {

            //↓新規追加された要素ごとに、その文字をadd_textに保存しておく
            for (const node of Array.from(record.addedNodes)) {
              add_text += node.textContent;
            }

          }

          //↓起こった変更の種類がattributes(要素の属性変化)かcharacterData(テキストノード変化)の場合
          else if (record.type === "attributes" || record.type === "characterData") {
            //↓変化した要素の文字をadd_textに保存しておく
            add_text += record.target.textContent;
          }

        }

        //↓add_textを一文字ごとの配列にしておく(後ほど利用します。「let add_text_ary = add_text.split('')」とするとサロゲートペア対策ができないので却下)      
        var add_text_ary = Array.from(add_text);

        //↓新規追加された文字の配列をブラウザの検証ツールに表示
        console.log("■add_text_ary↓" + add_text_ary);

        //↓追加された文字の中に、もともと画面に表示されていた文字が含まれる場合除外して配列を作成
        var diff_ary = add_text_ary.filter(i => before_text_ary.indexOf(i) === -1)

        //↓上記で作成した配列を文字列型に変換
        var diff_text = diff_ary.join("")

        //↓上記で変換した文字列型から、改行と空白文字を削除
        diff_text = diff_text.replace(ptn, '');

        //↓新たに画面に表示された文字のみブラウザの検証ツールに表示
        console.log("■diff_text↓" + diff_text);


        //↓新たに画面に表示された文字で、もともと画面に表示されていた文字と差がある場合のみ実行
        if (diff_text !== '') {

          //↓追加された文字のみを読み込む(ダイナミックサブセッティング)
          document.querySelector("head").insertAdjacentHTML('beforeend', css_before +
            encodeURI(diff_text) +
            css_after);

          //↓次の変更時の比較用として、変更が加わった後の画面上の文字を保存しておく
          before_text_ary = before_text_ary.concat(diff_ary);

        }

      })

      // 監視開始
      observer.observe(target, {
        //↓childList(監視対象の子要素が増減した場合に察知)を有効にする
        childList: true,
        //↓characterData(監視対象のテキストノードが変化した場合に察知)を有効にする
        characterData: true,
        //↓attributes(監視対象の属性が変化した場合に察知)を有効にする
        attributes: true,
        //↓subtree(監視対象の子要素だけでなく孫要素が変化した場合も察知)を有効にする
        subtree: true
      })


      //↓#btn1をクリックした場合、要素の属性を変化させる
      document.querySelector('#btn1').addEventListener('click', event => {
        document.querySelector('#p').style.display = "block";
        document.querySelector('#p2').style.display = "block";
      });

      //↓#btn2をクリックした場合、要素の子レベルと孫要素の構造を変化させる
      document.querySelector('#btn2').addEventListener('click', event => {

        //bタグを作成
        let newNode = document.createElement("b");

        //bタグの中の文字列を設定
        newNode.insertAdjacentHTML('beforeend', 'どれみふぁ');

        //#divの後ろにタグを追加(observer.observeの第二引数の配列の中にchildList:trueがある場合に察知可能)
        document.getElementById("div").parentNode.insertBefore(newNode, document.getElementById(
            "div")
          .nextElementSibling);


        //#divの中にタグを追加(observer.observeの第二引数の配列の中にchildList:trueとsubtree: trueがある場合に察知可能)
        document.querySelector('#div').insertAdjacentHTML('beforeend', `<b>子要素も挿入可能</b>`);


      });


      //↓#btn3をクリックした場合、監視対象直下のテキストノードを変更させる
      document.querySelector('#btn3').addEventListener('click', event => {
        //↓bserver.observeの第二引数の配列の中にcharacterData:trueを明記するとこの変更を監視してくれる
        target.childNodes[0].textContent = "body直下の文字 (変化後) ";

        /*
        //下記の要領で文字を変化させた場合、observer.observeの第二引数の配列の中にchildList:trueを明記するとこの変更を監視してくれる
        target.textContent = "body直下の文字 (変化後) "
        target.innerText = "body直下の文字 (変化後) "
        target.innerHTML = "body直下の文字 (変化後) "
        //上記3行の命令はいずれも文字列の変更なのに、なぜcharacterData:trueで監視されないかは不明
        */

      });




    });

上記 動的サブセットについての懸念

javascriptのMutationObserverという機能でbodyタグ以下の要素すべてを常に監視すると逆に遅くなるのでは?という不安があります。
PageSpeed Insightsで調べてもいいのですが、このツールはシステムをいじっていなくても毎度値が変化してしまう時があるので正確な評価点がわかりません。参考にはなるかもしれません。
タイトルに「Webフォントを軽くする方法」とありますが「読み込むWebフォントファイルのデータを軽くする方法」であって「ページ自体の読み込みが早くなる」のかを知るにはもっと勉強しなきゃですね。
ちなみに「読み込むWebフォントファイルが軽くなる」かどうかは、実際にフォントPotta Oneのcssのコメント119部分のデータと「&text=a」を追記して読み込んだときの容量の違いで検証済みです。Googleフォントは文字コードのグループ(unicode-range)ごとに読み込むファイルを決めているようなので、検証パターンを変えたら早くならない場合もあるかもしれません。そしたらすみません。

追記1 テキストボックスとテキストエリア内の文字には対応していません。

一文字入力されるごとにフォントを読み込むのはガタツキの懸念があります。ガタつく場合は見た目も良くないしユーザーも戸惑うのではないのでしょうか。テキストボックスとテキストエリア内の文字はwebフォントを使わず、端末にインストールされているフォントを読み込むようにcssで指定したほうがいい気がします。

5
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?