
一休.comでWebフロントエンドを開発している宇都宮です。
先日、一休.comホテルページのスマホ版から、jQueryを取り除きました。jQueryを取り除いた経緯、やったこと、結果について書きます。
ちなみに、ホテルページには以下のURLでアクセスできます(スマホで開くか、PCの場合はUAをスマホに偽装する必要があります)
https://www.ikyu.com/sd/00001290/
JavaScriptサイズの削減のためです。一休.comホテルページは、以前は合計で約300KBのjsファイルを読み込んでいました(300KBはgzip後の転送量なので、実ファイルはもっと大きいです)。
よくいわれる「jsは170KB以内ルール」は、回線速度のベースラインが400Kbpsという前提1です。一休.comの平均的ユーザはもっと良質の回線2を使っているので、170KBまで切り詰めようと思っているわけではありません。
しかし、jQueryで実装されている処理は、最近のDOM APIを使えば代替可能です。ブラウザAPIの統一が進みつつある現在、jQueryを使う理由はないのでは? と考え、jQuery依存を取り除くプロジェクトを進めました。
jQueryを使用している箇所は多かったため、細かくプルリクエストを切って、都度masterにマージしていく方針で進めました。
結果的に、修正プルリクは12個、総変更行数は±2500行程度になりました。
また、メインプロジェクトと並行して進めていたため、去年の8月頃から着手して、完了は先週でした。約4ヶ月かかった計算になります。
ここからは、やったことを細かく書いていきます。
jQuery.ajax() を、ブラウザの標準APIであるfetch に置き換えました。fetchが利用可能なのはiOS 10.3以上なので、polyfillも導入しました。
※ライブラリをバンドルすると、全てのユーザにpolyfillを配信することになります。パフォーマンス観点からは、polyfill.ioでfetchが使えない場合のみpolyfillを使うのも良いと思います。
基本的には、Promiseを使っているところはそのまま置き換え、コールバックを使っているところはPromiseベースに書き換えました。1カ所だけ同期のajaxを使っているところがあったので、そこは非同期に書き直しました。
$.ajax('https://www.ikyu.com/api/...',{}).then(data => data);const data = await fetch('https://www.ikyu.com/api/...').then(res => res.json());
比較したのはXMLHttpRequest(XHR)とaxiosですが、
XHRと比べると、
axiosと比べると、
という感じかなと思います。
fetchのConsについては、
という理由から実質的に問題ないと考えて、fetchのpolyfillを採用しました。
jQueryで行っていたDOM操作を、全てブラウザの標準APIに置き換えます。jQuery => DOM APIの置き換えに関する包括的なドキュメントは以下がおすすめです。
ここでは、今回のプロジェクトで実際に使った置き換えのみ紹介します。
jQueryの$() は単体の取得とリストの取得を透過的に扱えるようになっていますが、DOM APIでは区別が必要です。
$(selector);// 1個だけ取るdocument.querySelector(selector);// 要素のリストを取るdocument.querySelectorAll(selector);// 要素のリストを取って、一括操作する(NodeList.forEachはiOS 9では使えないので配列化している)[...document.querySelectorAll(selector)].forEach(/**/);
注意が必要なのは存在しない要素へのクエリです。jQueryは、存在しない要素に対するクエリを発行して、返却されたオブジェクトにメソッドを発行しても、エラーにはなりません。存在したりしなかったりする要素に対する処理をjQueryで行っている場合、DOM APIへの置き換えは一手間必要です。
// エラーにならない$('こんな要素はない').show();// document.querySelector()の結果はnullなので、styleへのアクセスでエラー発生document.querySelector('こんな要素はない').style.display ='block';
$el.show();$el.hide();$el.toggle();el.style.display ='';el.style.display ='none';if (el.ownerDocument.defaultView.getComputedStyle(el,null).display ==='none'){ el.style.display ='';}else{ el.style.display ='none';}
実際に使う際は、関数化したほうがよいでしょう。
class操作はclassListで置き換え可能です。IE 10以上対応なので安心。
$el.addClass('class');$el.removeClass('class');$el.hasClass('class');el.classList.add('class');el.classList.remove('class');el.classList.contains('class');
$el.html(html);$el.text(text);el.innerHTML = html;el.textContent = text;
jQueryのアニメーションAPIは手軽に使えて高機能なので、完全な置き換えは難しいです。ユースケースに合わせて、CSSアニメーションに置き換えていくのがよいでしょう。
これについてもhttps://github.com/nefe/You-Dont-Need-jQuery が参考になります。
You Don't Need jQueryには載っていない、アニメーションを伴うスクロールは以下のように実装しました。
/** * 指定した要素までスクロールする * @param {string} selector スクロール対象のHTML要素のCSSセレクタ * @param {number} step スクロール幅(px) * @param {number} timeout スクロールを行う間隔(ms) */exportfunction scrollToElement( selector,{ step = 100, timeout = 16} ={},){const target =document.querySelector(selector);if (!target)return;// 目的地のY座標const destY = target.offsetTop;// 目的地が現在位置より上にある場合は上(負のstep)、下にある場合は下(正のstep)にスクロールconst stepWithDirection = destY <window.scrollY ? -step : step;const scrollByStep = () =>{if (Math.abs(window.scrollY - destY) > step){// step よりも距離が開いているときはscrollByで近づくwindow.scrollBy(0, stepWithDirection); setTimeout(scrollByStep, timeout);}else{// step 以下の距離まで近づいたらscrollToでピッタリ移動するwindow.scrollTo(0, destY);}}; setTimeout(scrollByStep, timeout);}
$.readyはブラウザの対応状況にあわせて load と DOMContentLoaded を使い分けてくれます。が、すでにDOMContentLoaded未対応ブラウザ(IE 8以前)は滅びているので、DOMContentLoaded のみでOKでしょう。
$.ready(function(){// 処理});$(function(){// 処理});document.addEventListener('DOMContentLoaded', () =>{// 処理})
jQueryだと、「doument配下のclickイベントを全てキャッチし、そのクリック対象、およびクリック対象の親要素が特定の属性をもつ場合にだけハンドラを実行する」という処理が、以下のように簡単に書けます。
$(document).on('click','[data-xxx]', eventHandler);
これをDOMの標準APIで実装すると、少々面倒です。
function findParentByAttribute(target, attributeName){let el = target;while (el.parentNode){if (el.getAttribute(attributeName)){return el;} el = el.parentNode;}returnnull;}document.addEventListener('click',event =>{if (!findParentByAttribute(event.target,'data-xxx'))return;// handle event});
jQueryを取り除く作業をしたファイルには、先頭に以下の記述を追加して、jQueryを使ってはダメなことがわかるようにしました。
// このファイルではjQuery使用禁止!const $ =undefined;
このコードは、ローカル変数の$ を定義して、undefinedで初期化します。これによって、グローバルな$ はローカルの$ でシャドウされます(グローバルな$ は上書きされませんが、シンボルの探索ではローカルの$ が優先されます)。さらに、$ の値はundefined なので、$() などの呼び出しを行うとエラーが発生します。constなので再代入もできません。
これでも、window.$ 、window.jQuery 、jqueryのimportなど、jQueryにアクセスする手段は残されています。が、一休.com開発チームの規模やスキルを考えると、この方法で十分と判断しました。
なお、上記コードはES Modules(またはwebpack)環境での動作を前提にしています。ES Modulesはファイル毎のスコープを切ってくれますが、ES Modulesを使っていない場合も即時関数でスコープを切ることで同じことができます。
(function(){// ファイルの先頭// このファイルではjQuery使用禁止!const $ =undefined;...})();// ファイルの末尾


↑は、jQuery削除前後のPageSpeed Insightsのスコアです。どちらも71点。Time To Interactive/First CPU Idleは改善していますが、SpeedIndexは悪化しています。この程度の変動は何も変更しなくても起きるので、スコアが変わるほどのインパクトはなかったということですね。
パフォーマンス改善の観点からは、jQuery削除は、コスパが悪かったという結論になるかと思います。たぶん、同じ時間を別のタスクに使えば、もっと改善できたはず…。
なお、今回この結論に達したのは、既存コードのjQueryへの依存度が高かったからという理由もあります。サクッと取り除けるような状態なら、もっとコスパは良かったと思います。また、一休.comのホテルページスマホ版においては効果がなかったということであり、条件が異なれば、別の結果が得られると思います。
パフォーマンスの観点からは、ロードするJSの量を減らすことは重要です。一方で、JSライブラリ30KB程度の削除だと、誤差の範囲程度の改善効果しか得られない、ということもわかりました。塵も積もれば山となるので、無駄ではないと思いますが、もっとコスパの良い改善施策を実施していきたいところです。
Bonfire Frontend #3が、1/24に開催されます。テーマは「パフォーマンス改善」です。今回、Yahooグループのよしみで(?)お声がけいただき、登壇する機会をいただきました。今回の記事のような、一休.comで進めているパフォーマンス改善のお話しをしようと思っているので、是非ご参加ください!(すでに満席ですが、1/18に抽選なので、まだ間に合います)
http://infrequently.org/2017/10/can-you-afford-it-real-world-web-performance-budgets/↩
一休.comでは回線速度のベースラインを1.4Mbpsで考えています。LighthouseのSlow 4G相当です。↩
引用をストックしました
引用するにはまずログインしてください
引用をストックできませんでした。再度お試しください
限定公開記事のため引用できません。