ぼやき

0

WordPress 高速化を目指すため、 PageSpeed Insights でテストしてみると「スクロールせずに見えるコンテンツのレンダリングをブロックしている JavaScript/CSS を排除する」という項目を修正するように指摘されたので対応しました。プラグインを使わずに WordPress で対応すると結構大変だったので記録しておきます。

20170517_1.png

「スクロールせずに見えるコンテンツのレンダリングをブロックしている JavaScript/CSS を排除する」とは?

「スクロールせずに見えるコンテンツのレンダリングをブロックしている JavaScript/CSS を排除する」とはなんぞや、という話ですが、これは Above the fold (アバブ・ザ・フォールド)といわれているスクロールせずに見える領域を描写するのに必要ない JavaScript や CSS を読みこませるのを後回しにすることです。

20170517_2.png

Above the fold は直訳すると「折り目の上」という意味で、元々は新聞が半分に折って売られているときに見える上半分を意味します。この部分に書かれていることで新聞の売り上げが左右されるそうです。 Web 界隈ではファーストビューともいわれていますね。

ブラウザは JavaScript や CSS を読み込んでいる間レンダリングを停止します。なぜなら Javascript 内で document.write() で HTML が書き換えられる可能性があるので、 DOM の構築を停止するし、 CSS では DOM とともにレンダリングに必要な CSSOM の構築が終わるまでレンダリングを停止します。つまり必要ないのにすべての JavaScript や CSS を <head> で読み込むと、レンダリングがストップし最初にページが表示されるのが遅くなります。ページが遅いといわれる原因はこのあたりにもあります。ちなみに Google は Above the fold (ファーストビュー)を 1 秒以内に表示することを推奨しています。

JavaScript の読み込みを </body> 直前にし、不要なものは読み込まない

WordPress のプラグインや function.php でヘッダーで読み込んでいる JavaScript や CSS をファーストビュー (Above the fold) で必要なもの以外はフッター( </body> 直前)で読み込むようにします。

JavaScript を読み込むときに async 属性 や defer 属性を使うとレンダリングをブロックせずに非同期で読み込みことできます。しかし async だと JavaScript の実行順が確約されないし、実行順が確約される defer でも DOM の生成が終わる前に読み込んでおり、ブラウザの同時接続数を使ってしまい必要な画像が読み込まれるのが後回しになり表示が遅くなるので、ファーストビューに必要のない JavaScript はフッターで読み込みことにしました。

WordPress で JavaScript を </body> 直前に非同期で読み込む

対策ですが、 JavaScript は WordPress の enqueue に登録されたハンドル名がソースに表示されないので、以下のコードを function.php に書いてハンドル名を特定します。以下のサイトを参考にし一部使用するアクションフックを変更しています。

→ Tips Note by TAM : WordPress でプラグインから出力される jQuery ライブラリや CSS を整理する

function my_get_dependency( $dependency ) {
    $dep = "";
    if ( is_a( $dependency, "_WP_Dependency" ) ) {
        $dep .= "$dependency->handle";
        $dep .= " [" . implode( " ", $dependency->deps ) . "]";
        $dep .= " '$dependency->src'";
        $dep .= " '$dependency->ver'";
        $dep .= " '$dependency->args'";
        $dep .= " (" . implode( " ", $dependency->extra ) . ")";
    }
    return "$dep\n";
} 

function my_script_queues() {
	global $wp_scripts;
	echo "<!-- WP_Dependencies for scripts\n";
	foreach ( $wp_scripts->queue as $val ) {
		var_dump($wp_scripts->registered[$val]);
	}
	echo "-->\n";
}
add_action( 'wp_enqueue_scripts', 'my_script_queues');

ソースに以下のように出力されます。例は PhotoSwipe という画像を全面表示するプラグインです。このプラグインが読み込む JavaScript や CSS はユーザーが画像をクリックするまで必要ないので、読み込みは後回しにします。

<!-- WP_Dependencies for scripts
photoswipe-lib [] 'http://theme.1010uzu.com/wp/wp-content/plugins/photo-swipe/lib/photoswipe.min.js' '4.1.1.1' '' ()
photoswipe-ui-default [photoswipe-lib] 'http://theme.1010uzu.com/wp/wp-content/plugins/photo-swipe/lib/photoswipe-ui-default.min.js' '4.1.1.1' '' ()
photoswipe [photoswipe-lib photoswipe-ui-default jquery] 'http://theme.1010uzu.com/wp/wp-content/plugins/photo-swipe/js/photoswipe.js' '4.1.1.1' '' ()
-->

各行の冒頭に表示されているのがハンドル名です。これらを使って function.php に以下のように記述しました。

function my_deregister_script() {
	$first_img = get_post_meta($post_id, '_external_featured_image', true);
	$first_img_old = get_post_meta($post_id, 'thumbnail_url', true);
	if(!empty($first_img['url']) || !empty($first_img_old)){
		wp_enqueue_script('photoswipe-lib','','','',true);
		wp_enqueue_script('photoswipe-ui-default','','','',true);
		wp_enqueue_script('photoswipe','','','',true);
	}else{
		wp_deregister_script('photoswipe-lib');
		wp_deregister_script('photoswipe-ui-default');
		wp_deregister_script('photoswipe');
		wp_deregister_style('photoswipe-lib');
		wp_deregister_style('photoswipe-default-skin');
	}
	wp_register_script('jquery','','','',true);
	wp_register_script('jquery-core','','','',true);
	wp_register_script('jquery-migrate','','','',true);		
}
add_action('wp_enqueue_scripts', 'my_deregister_script');

1 枚目の画像をアイキャッチに自動的に指定するためその URL をカスタムフィールドに登録するようにしているので、カスタムフィールドが空ではない場合はハンドル名以外の引数を空の文字列にし、 5 番目の引数を true にすることで、スクリプトの URL などを引き継いだままフッターに出力されるようになります。

→ Cloud Four : Getting all JavaScript into the Footer in WordPress? Not so fast, Buster!

それ以外の場合は enqueue から登録を外しています。アイキャッチは URL にて指定する関数を自分で作っているのを使っています。コンテンツ内に画像があるか調べるためのもっとよい指定の仕方があるような気もしますが、アイキャッチを設定していてコンテンツ内に画像がない記事は少ないので、今回はこれでお茶を濁します。

→ WordPress で外部サーバーの画像をアイキャッチ画像として表示する

また jQuery も wp_register_script() でフッターに出力するように登録し直しています。依存関係にあるスクリプトもフッターに移動させないとヘッダで読み込んでしまいます。このサイトでは以下のページに書いたとおり Google のものに置き換えています。その場合 15 〜 17 行目は不要です。

→ jQuery の読み込みのアクションフックを変更

また wp_enqueue_scripts のアクションフックにて wp_deregister_script() を使ってうまく操作できない JavaScript は、推奨されていないアクションフックで登録されている場合がほとんどです。対象のプラグインを書き換えるか、登録されたときに使われているアクションフックで削除します。

これまでにプラグインで使われてきた JavaScript/CSS を登録するアクションフック

WordPress で JavaScript を defer 属性で非同期読み込みにする

メモとして書き残しておきますが、 JavaScript を defer 属性で非同期読み込みにする場合は以下のように function.php に書きます。例は jQuery です。 jQuery をファーストビューの猫写に使っている場合などに使用するとよいでしょう。

function my_add_defer_enqueue_script( $tag, $handle ) {
    if ( 'jquery' == $handle ) { return $tag; }
    return str_replace( ' src', ' defer src', $tag );
}
add_filter( 'script_loader_tag', 'my_add_defer_enqueue_script', 10, 2 );

ファーストビューのレンダリングに不要な CSS を <head> から排除する

PageSpeed Insights のテストで指摘された 6 つの CSS のうちファーストビューのレンダリングに使用しているのは style.css の一部と fontello.css です。それ以外の CSS ファイルの読み込みはやめるか後回しにします。

軽い CSS ファイルは style.css に組み込む

目次を表示させるのに Easy Table of Contents というプラグインを使っているのですが、大したことない CSS なのに 2 つもファイルを読み込んでいるので、 style.css で直接指定して、 CSS ファイルの読み込みを停止しました。

設定画面の「高度な設定」の「 CSS 」の項目の「プラグインの CSS の読み込みを防ぎます。」にチェックを入れると、プラグインの CSS が <head> に出力されなくなります。

20170517_3.png

もしプラグインの設定にそのような項目がない場合は、 wp_deregister_style() を使って CSS を登録から外します。

不要な CSS ファイルの削除

先程の PhotoSwipe の JavaScript を取り除くコードは、プラグインで出力されている不要な CSS も同時に wp_deregister_style() を使って登録から外しています。( 12 行目と 13 行目。)

使う関数が違うだけで JavaScript の場合と同じなのですが、 CSS の場合はハンドル名がブラウザのソースで見ることができます。

<link rel='stylesheet' id='photoswipe-lib-css'  href='http://theme.1010uzu.com/wp/wp-content/plugins/photo-swipe/lib/photoswipe.css?ver=4.1.1.1' type='text/css' media='all'>

id 属性から -css を除いた文字列 photoswipe-lib が emqueue に登録されているハンドル名なので、これを使って不要なページでは wp_deregister_style() で登録から外します。

WordPress で preload を併用して CSS を非同期で読み込み

この対策だけだと PhotoSwipe の表示に使う CSS など、ファーストビューをレンダリングするのに必要ない CSS をヘッダーで読み込んでいる時に、 DOM の生成をブロックするので、 CSS を非同期で読み込みにようと思います。

<link rel='preload' as='style' class='async-css' id='photoswipe-lib-css'  href='http://theme.1010uzu.com/wp/wp-content/plugins/photo-swipe/lib/photoswipe.css?ver=4.1.1.1' type='text/css' media='all' onload='this.rel="stylesheet"'>

上記のように link タグに rel='preload' as='style' と指定することで、 DOM の生成を邪魔することなく CSS を読み込むことができます。読み込み終わったらイベントが発生するので、 onload で rel 属性を stylesheet に変更しブラウザにスタイルシートだと認識させています。しかしこれは今のところ Chrome 50 以上と Android 56 以上、 Opera 44 以上しか対応していません。

Safari 11 、 Firefox 56 から preload に対応するようです。

20170517_4.png

→ Can I use : Resource Hints: preload

いろいろ実験した結果、その他のブラウザは JavaScript によって </body> 直前で属性を rel='stylesheet' に書き換えて適応させることにしました。管理画面は上手く動かないので除外してあります。 firefox などで少しでも速く読み込みさせようと、 preload と prefetch を併記しようかとも思ったのですが、読み込む順番は変わらないし、 Chrome にて 3 回くらいリクエストが発生するのでやめました。

→ HAIL2U : rel=subresource を併用した CSS の遅延読み込み

遅延読み込みに関しては上記サイトを参考にしたのですが、 subresource は Chrome のみでしか使えないようで、かつ preload に置き換えられたようなので、そちらを使っています。また JavaScript でクラスを指定するのに querySelectorAll() を使っていますが、これは動作が遅いようなので、 getElementsByClassName() を使うことにしました。 Firefox において CSS が読み込まれるタイミングが速くなりました。

テーマの style.css を読み込むのに header.php に直接書いている場合、 wp_enqueue_scripts のアクションフックが使えないので、 header.php の記述を削除し function.php に以下のコードを追加します。 3 番目の引数に 5 が設定されているのは、 WordPress は enqueue に登録された順番で出力されるので、早めに登録するためです。

//メインのCSS追加
function add_css_style() {
	wp_enqueue_style( 'css-style', get_stylesheet_uri(), array(), null );
}
add_action( 'wp_enqueue_scripts', 'add_css_style', 5 );

//管理画面以外でCSSの遅延読み込み
if ( !is_admin() ) {
	add_filter( 'style_loader_tag', 'my_css_asynchronous', 10, 2 );
	add_action('wp_footer', 'add_link_rel_stylesheet', 11);
}

//link タグの属性を変更
function my_css_asynchronous($html, $handle){
	if(!($handle == 'firstview' || $handle == 'fontello')){
		$html = preg_replace( array( "| rel='.+?'\s*|", '| />|' ), array( " rel='preload' as='style' class='async-css' ", " onload='this.rel=\"stylesheet\"'>" ), $html );
	}
	return $html;
}

//フッターに rel = 'stylesheet' に変更する JavaScript の追加
function add_link_rel_stylesheet(){
	$my_css_data = '<script type=\'text/javascript\'>
	var css = document.getElementsByClassName("async-css");
	for (var i = 0, l = css.length; i < l; i++) {
		css[i].rel = \'stylesheet\';
	}
	</script>';
	echo $my_css_data;
}

( 15 行目はファーストビューのレンダリングに必要な firstview.css と fontello.css を非同期読み込みの対象から外しています。必要ないなら削除して構わないです。)

ファーストビューに必要な CSS をインライン化(随時圧縮)

このままでは style.css が読み込まれるのが最後になるので、スタイルシートが適応されるまでタイムラグが発生してしまいます。なので style.css からファーストビューのレンダリングに必要な CSS だけを抜き出した firstview.css を作成し、それを自動でインライン化し圧縮することにしました。同時にアイコンを Web フォントで表示するのに必要な fontello.css もファーストビューに表示されているのでまとめることにしました。

→ WordPress にソーシャルボタンを Fontello の Web フォントを使って設置する方法

PHP での処理が増えるのでサーバーの応答時間が伸びるかもしれませんが、 WP Super Cache を使ってキャッシュしているのでまあ大丈夫でしょう。

function add_inline_css(){
	//相対パスのベースを取得
	$home_url = trailingslashit(get_home_url('/'));
	$top_url = preg_replace( '/^(https?:\/\/.+?)\/(.*)$/', '$1', $home_url );
	$relative_path_base = str_replace( $top_url, '', get_stylesheet_directory_uri() );
	//CSSを読み込み相対パスに変換
	$inline_css = file_get_contents( get_stylesheet_directory_uri() . '/firstview.css', true);
	$inline_css = str_replace( 'url("images/' , 'url("'. $relative_path_base . '/images/', $inline_css );
	$fontello = file_get_contents(get_stylesheet_directory_uri() . '/fontello-icon/css/fontello.css', true);	
	$fontello = str_replace( 'url(\'../font/fontello.' , 'url(\''. $relative_path_base . '/fontello-icon/font/fontello.', $fontello );
	$inline_css .= $fontello;
	//CSS圧縮
	$inline_css = preg_replace( array('/\r\n|\r|\n|\t/', '/\/\*.*?\*\//', '/\s+/','/(\(|\[)\s+/','/\s+(\)|\]|::)/', '/\s*(\{|\}|:|;|,|\+|>)\s*/', '/;\}/', '/[^{}]+\{\}/'), array('', '', ' ', '$1', '$1', '$1', '}', ''), $inline_css );
	echo '<style type=\'text/css\'>' . $inline_css . '</style>'. PHP_EOL;
}
add_action('wp_head', 'add_inline_css', 5);

相対パスを生成するためのテーマフォルダまでのベースになるパスを求め、それによって CSS 内の背景画像のパスを置換しています。またそのまま書き出すと長いので出力されるため無駄なスペースや改行を削除して圧縮しています。 CSS の圧縮に関しては下記のサイトを参考にコードが短くなるように書き直しました。

→れいぶろぐ:【 WordPress 】外部 CSS を圧縮して HTML 内に出力する

ファーストビューに必要な CSS をインライン化(圧縮ファイル用意)※追記 2017.9.17

上記方法でインライン化していたのですが、やはりサーバーの負荷が気になったので、インライン化する CSS を予めまとめて圧縮したものを用意して読み込ませるようにしました。

function add_css_style() {
	//インラインCSSの追加
	wp_register_style( 'firstview', false );
	wp_enqueue_style( 'firstview' );
	$inline_css = file_get_contents( get_stylesheet_directory_uri() . '/firstview.min.css', true);
	wp_add_inline_style( 'firstview', $inline_css );
	//メインのCSS追加
	wp_enqueue_style( 'css-style', get_stylesheet_directory_uri() . '/style.min.css', array(), '0.1.9.6.1' );
}
add_action( 'wp_enqueue_scripts', 'add_css_style', 5 );

また wp-head のフックを使ってインライン CSS を出力していたのですが、 wp_enqueue_scripts のフックにて wp_add_inline_style() の関数で登録する方法がわかったので、メインの CSS を読み込むのと同時に処理しています。 wp_register_style() の第 2 引数を false にして URL なしの空のハンドルを登録し、それに wp_add_inline_style() を使ってインライン CSS をつけ加えている形です。 3 〜 4 行目を 1 行で wp_enqueue_style( 'firstview', false ); のように記述するとなぜかうまく動きません。

→ From Our Blog : How to late enqueue inline CSS in WordPress

施策前後の PageSpeed Insights のスコア

20170517_5.png

これで PageSpeed Insights のスコアはモバイル 57 、パソコン 67 だったのが、モバイル 70 、パソコン 74 とかなり改善しました。「スクロールせずに見えるコンテンツのレンダリングをブロックしている JavaScript/CSS を排除する」の「修正が必要」は警告されなくなりました。

ファーストビューを 1 秒以内にするためにはもう少し施策が必要です。

関連記事