Webフォントを軽くする方法(Googleフォントを無料でダイナミックサブセット化)
Googleフォントで読み込む文字を指定可能に!
Google Chromeのエンジニア マネージャーのツイートにて発表がありました。
方法(静的サブセット)
↓軽量化前
<linkrel="preconnect"href="https://fonts.gstatic.com"><linkhref="https://fonts.googleapis.com/css2?family=Potta+One&display=swap"rel="stylesheet">
↓軽量化後
<linkrel="preconnect"href="https://fonts.gstatic.com"><linkhref="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><htmllang="ja"><head><metacharset="UTF-8"><metahttp-equiv="X-UA-Compatible"content="IE=edge"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>テスト</title><linkrel="stylesheet"href="style.css"></head><body>
body直下の文字 (変化前)
<buttonid="btn1">要素の属性を変更する(style属性)</button><buttonid="btn2">要素の構造を変更する(文字変更含む)</button><buttonid="btn3">要素の文字列のみを変更する</button><divid="div">最初の文字</div><pid="p">親要素
<pid="p2">子要素</p></p><script src="script.js"></script></body></html>
/*↓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;}
//↓HTMLの要素構築が完了してからdocument.addEventListener("DOMContentLoaded",function(event){//↓監視ターゲット設定consttarget=document.body;//↓画面上に表示されている文字のみ取得letbefore_text=target.innerText;//↓正規表現で改行もしくは空白文字を指定し、それらをbefore_textから除外constptn=/\n|\r\n|\r|\s/g;before_text=before_text.replace(ptn,'');//before_textを一文字ごとの配列にしておく(後ほど利用します。「let before_text_ary = before_text.split('')」とするとサロゲートペア対策ができないので却下) letbefore_text_ary=Array.from(before_text);//↓googleフォントを読み込むためのlinkタグのうち、href属性がhttps://fonts.gstatic.comでないほうのlinkタグのhref属性値を代入constcss_url="https://fonts.googleapis.com/css2?family=Potta+One&display=swap";//↓googleフォントを読み込むためのlinkタグの前半部分を作成constcss_before='<link rel="preconnect" href="https://fonts.gstatic.com"><link href="'+css_url+'&text=';//↓googleフォントを読み込むためのlinkタグの後半部分を作成constcss_after='" rel="stylesheet">';//↓画面上に表示されている文字のみを読み込む(ダイナミックサブセッティング)document.querySelector("head").insertAdjacentHTML('beforeend',css_before+encodeURI(before_text)+css_after);//↓監視時変更が起きたら実行するconstobserver=newMutationObserver(records=>{//↓追加された文字を保存しておくために用意letadd_text="";//↓監視に引っかかった変更点ごとに実行for(constrecordofrecords){//↓起こった変更の種類をブラウザの検証ツールに表示console.log("■type="+record.type);//↓起こった変更の種類がchildList(監視対象の子要素が増減)の場合if(record.type==="childList"){//↓新規追加された要素ごとに、その文字をadd_textに保存しておくfor(constnodeofArray.from(record.addedNodes)){add_text+=node.textContent;}}//↓起こった変更の種類がattributes(要素の属性変化)かcharacterData(テキストノード変化)の場合elseif(record.type==="attributes"||record.type==="characterData"){//↓変化した要素の文字をadd_textに保存しておくadd_text+=record.target.textContent;}}//↓add_textを一文字ごとの配列にしておく(後ほど利用します。「let add_text_ary = add_text.split('')」とするとサロゲートペア対策ができないので却下) varadd_text_ary=Array.from(add_text);//↓新規追加された文字の配列をブラウザの検証ツールに表示console.log("■add_text_ary↓"+add_text_ary);//↓追加された文字の中に、もともと画面に表示されていた文字が含まれる場合除外して配列を作成vardiff_ary=add_text_ary.filter(i=>before_text_ary.indexOf(i)===-1)//↓上記で作成した配列を文字列型に変換vardiff_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タグを作成letnewNode=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で指定したほうがいい気がします。