プラグインのパフォーマンスを200%向上させた方法

執筆者:

カテゴリ:

,

去る2017年2月2日、Ann Taylor氏がCodeInWPで記事「12 Things We Need to See From WordPress Page Builders in 2017 + Who Already Gets It Right(2017年にWordPressページビルダーに求められる12のこと + すでにそれを実現しているプラグイン)」を執筆しました。私たちは1年以上にわたってPage Builder Sandwichの改善を続けてきました。リサーチを重ね、顧客の声に耳を傾けてきたため、Page Builder SandwichもCodeInWPの「すでにそれを実現しているプラグイン」の記事に選ばれるだろうと考えていました。しかし、パフォーマンスの問題が原因で選ばれませんでした。?

そこでAnn氏に問い合わせたところ、次のような返信がありました。

…無料版を少し触ってみました。多くのモジュール、非常に直感的なUI、素晴らしいWYSIWYG体験、すべてが簡単にドラッグできるなど、大いに期待できるものでした。しかし、ページ上に10個ほどのモジュールを配置した時点で、編集速度が遅くなり、作業を続けるのが難しくなりました…

彼女の指摘によると、Page Builder Sandwichにはパフォーマンス上の問題があるとのことでした。私たちは常にフィードバックに耳を傾けているため、早速調査を開始しました。

パフォーマンスのテスト

PBSのパフォーマンスを調べる方法が必要でした。PBSは主にJavascriptを介してブラウザ上で動作するため、ChromeのTimelineプロファイラが最適なツールでした。Timelineプロファイラは、実行時にウェブページ内でどのプロセスが発生しているかを特定・測定できるため、速度のボトルネックを見つけるのに最適です。

私たちは、まず最もシンプルな操作(複数の要素や行の上でマウスを動かすこと)をテストすべきだと考えました。簡単そうですよね?このような単純な操作でパフォーマンスの問題が発生するはずがないと確信していました。しかし残念ながら、その考えは甘かったのです。

録画ボタンを押し、画面上で約2秒間マウスを動かした後、停止ボタンを押しました。プロファイラから得られたタイムラインの結果がこちらです。

なんと驚いたことに、パフォーマンスのボトルネックで埋め尽くされていました!

グラフ上では多くのことが起こっていますが、最も明白な問題は紫色の領域が大量にあることです(本来こうあるべきではありません)。さらに、これらの紫色の領域は目に見えて非常に幅が広く、すべてがグラフのピークに達しているように見えます。また、グラフが常に埋まっており、紫色の領域がほぼゼロに落ちていないことにも気づくでしょう。ブラウザが常に何らかの処理で忙殺されており、休む暇がほとんどないような状態です。

しかし、これらの紫色の領域は一体何を意味しているのでしょうか?

プロファイラは親切にも、それがリフロー(reflow)と呼ばれるものであることを教えてくれました。Google Developerサイト:

リフローとは、ドキュメント内の要素の位置と形状を再計算するウェブブラウザのプロセスの名前です。リフローはブラウザにおいてユーザー操作をブロックする処理です。

つまり、PBS内でマウスを動かすだけで膨大な量のリフローが発生していることを意味します。さらに悪いことに、個々のリフローは計算を完了させるためにブラウザを一時的に停止させます。リフローが増えればブラウザの停止頻度も増え、操作体験はさらにカクカク(ジャンク)したものになります。簡単に言えば、紫色の領域が多い = 悪い状態、ということです。

結果としてAnn氏の指摘は正しかったのです。私たちには確かに大きなパフォーマンスのボトルネックが存在していたため、チームは直ちにその修正作業に取り掛かりました。

リフロー / カクつき問題の修正

何よりもまず、そもそも何が原因でこのリフローが発生するのかを調べる必要がありました。調査の結果、どうやらJavascriptにおける多くの一般的な処理が、ブラウザにリフローを引き起こす原因となり得ることが分かりました。

DOM要素の幅、高さ、位置を取得するために使用できるほぼすべての関数がリフローを誘発する可能性があります。Paul Irish氏が、リフローを引き起こす可能性のあるJavascriptのすべてのプロパティと関数の詳細なリストを作成しています。都合の悪いことに、Page Builder Sandwichはこれらのプロパティや関数を大量に使用しており、そのすべてがリフローを引き起こしていました。

  • elem.getBoundingClientRect()
  • window.getComputedStyle()
  • window.scrollY
  • その他多数…

修正1: コストのかかるリフロー処理のキャッシュ

最も明白な解決策は、それらのプロパティや関数を使用しないことです。しかし、PBSはそれらに大きく依存しており、なしで済ませることはできません。

残された解決策は、それらの関数の呼び出し回数をできる限り削ぎ落とすことでした。難しかったのは、PBSのさまざまな箇所でこれらのプロパティや関数を何度も使用しており、ほとんどの場合、同じDOM要素に対して複数回使用していたという点です。これを修正するために、問題のあるプロパティ/関数ごとに独自のキャッシュメカニズムを作成しました。

例えば、getBoundingClientRect関数は、DOM要素の現在の上、左、幅、高さ、右、下のピクセル測定値を取得します。この関数は、PBSで要素のアウトラインを描画する位置を計算するために常に使用されています。これに対して作成したキャッシュ関数がこちらです。

// Cache: Our client rects are kept here, we can use this to clear them later.
var elemsWithBoundingRects = [];

window.pbsGetBoundingClientRect = function( element ) {

	// Check if we already got the client rect before.
	if ( ! element._boundingClientRect ) {

		// If not, get it then store it for future use.
		element._boundingClientRect = element.getBoundingClientRect();
		elemsWithBoundingRects.push( element );
	}
	return element._boundingClientRect;
};
​

getBoundingClientRectを直接呼び出す代わりに、pbsGetBoundingClientRectを使用するようにしました。これにより、クライアント矩形が照会されるたびにその値を将来の参照用に保存し、リフローが発生する回数を制限しました。

また、保存されたクライアント矩形が無効になった際にキャッシュをクリアする方法も必要でした。保存されたクライアント矩形の値は、ユーザーがページをスクロールしたり、画面上の要素を追加・変更・削除したりしたときに変化します。そのため、最新のクライアント矩形値が必要になったときにいつでも呼び出せるキャッシュクリア関数を作成しました。

var clearClientRects = function() {
	var i;
	for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
		if ( elemsWithBoundingRects[ i ] ) {
			elemsWithBoundingRects[ i ]._boundingClientRect = null;
		}
	}
	elemsWithBoundingRects = [];
};​

普段よく使用する他のコストの高い関数呼び出しに対しても、同様のキャッシュメカニズムを適用しました。

修正2: FastDomを使用した読み取りと書き込みのグループ化

修正1を実装したことでPBSのパフォーマンスは向上しましたが、それだけでは不十分でした。

さらにGoogleで検索したところ、Wilson Page氏による「レイアウトスラッシングの防止(Preventing ‘layout thrashing’)」という記事に出会いました。Wilson氏はこう述べています。

レイアウトスラッシング(リフロー)は、JavaScriptがDOMに対して激しく書き込みを行ってから読み取る処理を何度も繰り返し、ドキュメントのリフローを引き起こすことで発生します。

どうやら、特定のリフローを誘発するプロパティや関数の呼び出しを制限するだけでは不十分なようでした。DOMに対して「読み取り」と「書き込み」を連続して何度も実行すると、それだけでもブラウザのリフローが引き起こされます。Wilson氏のサイトから引用した典型的なシナリオは以下のとおりです。

// Read
var h1 = element1.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';

// Read (triggers reflow)
var h2 = element2.clientHeight;

// Write (invalidates layout)
element2.style.height = (h2 * 2) + 'px';

// Read (triggers reflow)
var h3 = element3.clientHeight;

// Write (invalidates layout)
element3.style.height = (h3 * 2) + 'px';

// Document reflows at end of frame​

上記の例では、書き込みの後にページ全体の寸法が無効化されるため、3回のリフローが発生します。これに対する推奨の修正方法は、コードの順序を並べ替えてすべての読み取りと書き込みをまとめて束ねることで、全体で1回のブラウザリフローしか引き起こさないようにすることです。

// Read
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;

// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';

// Document reflows at end of frame​

上記のスニペットを見ると、問題の修正は簡単に思えます。しかし実際には、PBS内においてリフローを誘発する読み取りと書き込みは、ライフサイクル中のさまざまな場所で、さまざまなタイミングで行われています。何千行ものJavascriptコードを再編成することは現実的ではありませんでした。

幸いなことに、Wilson氏はこの記事の中で解決策も提供してくれていました。彼はこれらの読み取りと書き込みをまとめるのに役立つFastDomという小さなユーティリティスクリプトを作成していたのです。

以下は、FastDomを使用し、上記の修正1も組み込んだPage Builder Sandwichのコードです。

// Bunch together the reads from other threads.
fastdom.measure( function() {

	// Use our cached client rect.
	var rect = window.pbsGetBoundingClientRect( elem );

	// Bunch together the writes from other threads.
	fastdom.mutate( function() {
		elem.style.top = ( rect.top + window.pbsScrollY() ) + 'px';
		elem.style.height = rect.height + 'px';
		elem.style.left = rect.left + 'px';
		elem.style.width = rect.width + 'px';
	}.bind( this ) );
}.bind( this ) );​

FastDomの役割は、すべてのfastdom.measure呼び出しとすべてのfastdom.mutate呼び出しをグループ化して非同期に実行し、すべての読み取りと書き込みを効率的にまとめてブラウザ内のリフローを軽減することです。

修正3: スタイルオブジェクトへのアクセスは負荷が高い

また、jsPerfを利用して調査したところ、スタイルを取得する関数の中には他よりも高速なものがあることがわかりました。

PBSでは通常、要素の特定のスタイルの現在値を取得する必要があります。例えば、通常は上マージンを取得するために次のように記述します。

var styles = window.getComputedStyle( elem );
var marginTop = styles.marginTop;​

しかし、次のように処理した方がはるかに高速であることがわかりました。

var marginTop = elem.style.marginTop;​

ただし、elem.style.marginTopとgetComputedStyleは大きく異なります。前者は要素のインラインスタイルのみを返し、スタイルシートを考慮しません。一方、後者はインラインスタイルとスタイルシートの両方を考慮します。

さらに、getComputedStyleの結果をキャッシュした場合でも、依然として非常に低速でした。判明したこととして、スタイルオブジェクト自体にアクセスすること自体が遅いプロセスだったのです。

幸いなことに、PBSは要素にインラインスタイルを追加しており、(ほとんどの場合)インラインスタイルはスタイルシートよりも優先されるため、インラインスタイルの値を取得するだけで十分なケースが存在します。したがって、上記の2つの方法を組み合わせれば、はるかに高速に動作するコードを作成できます。

var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
	var styles = window.getComputedStyle( elem );
	marginTop = styles.marginTop;
}​

コード改善の結果

上記の3つの修正には多くの研究、テスト、実際の実装が必要であり(フォント機能のリリースも延期されました)、最終的にはその価値が十分にありました。

画面上でマウスを2秒間動かすという初期テストに戻ってみましょう。以下は以前の見苦しくカクついていたタイムラインです。

そして、パフォーマンス改善後の新しいタイムラインがこちらです。

ご覧のとおり、グラフ内の紫色の部分が減少しています。また、ブラウザによって実行されている処理の量にも顕著な違いがあります。下部の長方形の部分も薄くなっており、以前よりも処理が高速に実行されていることを示しています。

新旧のグラフで高さや幅のある紫色の領域の量を比較すると、なんと200%のパフォーマンス向上を達成しています!全体的にパフォーマンスの向上が見られます。素晴らしい成果です!

得られた教訓と学び

正直なところ、Ann氏から指摘されるまで、PBSにパフォーマンスの問題があるとは思っていませんでした。主に開発用スペックのマシンを使用していたため、パフォーマンスのボトルネックがそれほど目立たなかったためです。振り返ってみると、パフォーマンスのチェックは最優先事項の1つであるべきでした。しかし少なくとも現在、バージョン4.3の時点でこの問題は解決されています。?

ご自身のWordPressプラグインのパフォーマンスを向上させたい方、特にJavascriptを多用している方に向けて、今回の経験から得られたポイントをご紹介します。

  1. リフローを引き起こすプロパティや関数の呼び出しを制限/キャッシュする。こちらのガイドをご活用ください:What forces layout/reflow. The comprehensive list.(何がレイアウト/リフローを強制するか。包括的なリスト)
  2. DOMスタイルに対する「読み取り」と「書き込み」をグループ化する。これを行うにはFastDomを使用します。
  3. ChromeのTimelineプロファイラを使用し、リフロー(紫色の部分)を可能な限り少なく抑える。