2026年7月10日

2026年7月10日

WordPressでBlurhashを使って画像のローディングプレースホルダーを実装する方法

BlurhashをWordPressで使うには、画像のアップロード時にBlurhash文字列を計算して添付ファイルのメタ情報に保存し、表示時にその文字列から作ったぼかし画像を <img> の背景として敷きます。この記事では、計算・保存・表示・既存画像への一括適用までを、独自プラグイン(またはmu-plugin)として組む手順を説明します。読み終えると、画像が読み込まれるまでの空白を、元画像の色味を残したぼかしで埋められるようになります。

Blurhashとは何か、何が良いのか

Blurhashは、画像を20〜30文字程度の短い文字列に要約し、そこから低解像度のぼかし画像を復元するアルゴリズムです。Wolt社がオープンソースで公開しており、PHP・JavaScript・Swift・Kotlinなど各言語の実装があります。

似た手法に「LQIP(極小のサムネイル画像をBase64で埋め込む)」や「平均色だけを背景色にする」方法があります。Blurhashの利点は、文字列がとても短いのでデータベースやREST APIのレスポンスに載せても負担が小さいことです。一方で、表示するには文字列を画像に「復元(デコード)」する処理が要ります。

この復元をどこで行うかで、設計が二つに分かれます。

  • ブラウザやアプリで復元する:ヘッドレス構成やスマホアプリでREST APIからBlurhash文字列を受け取り、JavaScript(npmの blurhash パッケージなど)でcanvasに描く。
  • サーバーで復元して小さなPNGにする:通常のテーマで表示する場合は、アップロード時に32px程度のPNGへ復元してdata URIとして保存し、CSSの背景に使う。JavaScriptが要らず、表示時の負荷もほぼありません。

この記事では両方に対応できるよう、Blurhash文字列と復元済みのdata URIを両方保存します。

準備:作業環境とライブラリ

作業は本番ではなくステージング環境で行い、事前にデータベースと wp-content/uploads のバックアップを取ってください。テーマの functions.php に書くとテーマ変更で消えるため、独自プラグインとして作ります。

PHPの実装には、Composerで入れられる kornrunner/blurhash を使います。プラグイン用のフォルダでComposerを実行します。

cd wp-content/plugins/my-blurhash
composer require kornrunner/blurhash

画像の読み込みにはPHPのGD拡張を使います。GDが有効かは、管理画面の「ツール」→「サイトヘルス」→「情報」タブのサーバー欄やメディア処理の欄で確認できます(表記はバージョンにより異なります)。WebPやAVIFを扱えるかはGDのビルドに依存するため、対応していない形式は処理を飛ばすようにしておきます。

アップロード時にBlurhashを計算して保存する

計算は wp_generate_attachment_metadata フィルターで行います。このフィルターは、アップロード後にサムネイルなどの中間サイズが作られたタイミングで呼ばれ、メタ情報と添付ファイルIDを受け取ります。

ポイントは、元画像ではなく小さな中間サイズ(thumbnail)から計算することです。Blurhashの計算量は画素数に比例するため、数千ピクセルの元画像をそのまま処理すると時間もメモリも大きく消費します。さらに32px幅程度まで縮小してから計算すれば、見た目の差はほとんどありません。

<?php
/**
 * Plugin Name: My Blurhash Placeholder
 */
defined( 'ABSPATH' ) || exit;

require_once __DIR__ . '/vendor/autoload.php';

use kornrunner\Blurhash\Blurhash;

function myblur_pixels_from_file( $path, $w = 32 ) {
	$data = @file_get_contents( $path );
	if ( false === $data ) {
		return null;
	}
	$src = @imagecreatefromstring( $data );
	if ( ! $src ) {
		return null; // GDが対応していない形式
	}
	$small = imagescale( $src, $w );
	imagedestroy( $src );
	if ( ! $small ) {
		return null;
	}
	$pixels = array();
	$height = imagesy( $small );
	for ( $y = 0; $y < $height; $y++ ) {
		$row = array();
		for ( $x = 0; $x < $w; $x++ ) {
			$rgb   = imagecolorat( $small, $x, $y );
			$row[] = array( ( $rgb >> 16 ) & 0xFF, ( $rgb >> 8 ) & 0xFF, $rgb & 0xFF );
		}
		$pixels[] = $row;
	}
	imagedestroy( $small );
	return $pixels;
}

function myblur_data_uri( $hash, $w = 32, $h = 32 ) {
	$pixels = Blurhash::decode( $hash, $w, $h );
	$img    = imagecreatetruecolor( $w, $h );
	for ( $y = 0; $y < $h; $y++ ) {
		for ( $x = 0; $x < $w; $x++ ) {
			list( $r, $g, $b ) = $pixels[ $y ][ $x ];
			imagesetpixel( $img, $x, $y, imagecolorallocate( $img, (int) $r, (int) $g, (int) $b ) );
		}
	}
	ob_start();
	imagepng( $img );
	imagedestroy( $img );
	return 'data:image/png;base64,' . base64_encode( ob_get_clean() );
}

function myblur_process( $attachment_id, $metadata ) {
	if ( ! wp_attachment_is_image( $attachment_id ) || empty( $metadata['file'] ) ) {
		return;
	}
	$dir  = dirname( get_attached_file( $attachment_id ) );
	$file = ! empty( $metadata['sizes']['thumbnail']['file'] )
		? $dir . '/' . $metadata['sizes']['thumbnail']['file']
		: get_attached_file( $attachment_id );

	$pixels = myblur_pixels_from_file( $file );
	if ( ! $pixels ) {
		return;
	}
	$hash = Blurhash::encode( $pixels, 4, 3 );
	update_post_meta( $attachment_id, '_myblur_hash', $hash );

	// 元画像の縦横比に合わせて復元サイズを決める
	$ratio = ( ! empty( $metadata['width'] ) && ! empty( $metadata['height'] ) )
		? $metadata['height'] / $metadata['width'] : 1;
	$h = max( 1, (int) round( 32 * $ratio ) );
	update_post_meta( $attachment_id, '_myblur_datauri', myblur_data_uri( $hash, 32, $h ) );
}

add_filter( 'wp_generate_attachment_metadata', function ( $metadata, $attachment_id ) {
	myblur_process( $attachment_id, $metadata );
	return $metadata;
}, 20, 2 );

Blurhash::encode() の第2・第3引数は横方向・縦方向の成分数で、1〜9の範囲で指定します。数を増やすと細部が残る代わりに文字列が長くなります。横長の写真なら「4, 3」程度で十分です。

フィルターなので、最後に受け取った $metadata を変更せずに返すことを忘れないでください。返し忘れると、サムネイル情報が失われて画像が表示されなくなります。

表示時に背景として敷く

保存したdata URIは、wp_get_attachment_image_attributes フィルターで <img> の style 属性に追加します。このフィルターは wp_get_attachment_image() を通る画像(アイキャッチや多くのテーマの一覧画像)に効き、戻り値の属性は関数側で esc_attr() されます。

add_filter( 'wp_get_attachment_image_attributes', function ( $attr, $attachment ) {
	$uri = get_post_meta( $attachment->ID, '_myblur_datauri', true );
	if ( $uri && 0 === strpos( $uri, 'data:image/png;base64,' ) ) {
		$style          = isset( $attr['style'] ) ? $attr['style'] . ';' : '';
		$attr['style']  = $style . 'background-image:url(' . $uri . ');background-size:cover;';
		$attr['class'] .= ' has-blurhash';
	}
	return $attr;
}, 10, 2 );

本文中の画像(ブロックエディターで挿入した画像)は保存済みのHTMLとして出力されるため、このフィルターは効きません。WordPress 6.0以降で使える wp_content_img_tag フィルターなら、本文の <img> タグ1つずつと添付ファイルIDを受け取れるので、同様に style を足せます。その場合は esc_attr() で値をエスケープしてから埋め込みます。

つまずきやすい点

  • 透過PNGで背景が透ける:画像の読み込み後もぼかしが透明部分に残ります。透過のある画像は対象外にするか、load イベントで style.backgroundImage を消す小さなスクリプトを wp_enqueue_script() で読み込みます。
  • レイアウトシフトは防げない:プレースホルダーは見た目の空白を埋めるだけです。CLS(Cumulative Layout Shift)を防ぐのは <img> の width と height 属性で、wp_get_attachment_image() は通常これを出力します。テーマのCSSで height: auto を指定し、親要素の幅を超えないように最大幅を設定しておきます。
  • 遅延読み込みとの関係:WordPressは画像に loading="lazy" を自動で付けることがあり、スクロールして近づくまで本体を読み込みません。その間ぼかしが見えるので、Blurhashと相性の良い組み合わせです。ファーストビューの画像は遅延読み込みしない方がLCP(Largest Contentful Paint)に有利です。

ヘッドレス構成でREST APIから使う

フロントエンドをNext.jsやSvelteKitなどで作っている場合は、Blurhash文字列をREST APIで返し、ブラウザ側で復元します。register_rest_field() で添付ファイルのレスポンスにフィールドを追加します。

add_action( 'rest_api_init', function () {
	register_rest_field( 'attachment', 'blurhash', array(
		'get_callback' => function ( $obj ) {
			return get_post_meta( $obj['id'], '_myblur_hash', true ) ?: null;
		},
		'schema'       => array( 'type' => array( 'string', 'null' ) ),
	) );
} );

フロント側では、npmの blurhash パッケージの decode() で画素データに戻し、canvasに描きます。ReactやVueなどにはこれを包んだコンポーネントも公開されていますが、保守状況は導入前に確認してください。

既存の画像にまとめて適用する

フィルターは新しくアップロードした画像にしか効きません。既存画像には、WP-CLIの wp eval-file で小分けに処理します。wp media regenerate でも同じフィルターが呼ばれますが、全サムネイルを作り直すため時間と負荷が大きくなります。

<?php
// backfill-blurhash.php : wp eval-file backfill-blurhash.php
$ids = get_posts( array(
	'post_type'      => 'attachment',
	'post_status'    => 'inherit',
	'post_mime_type' => 'image',
	'posts_per_page' => 200,
	'fields'         => 'ids',
	'meta_query'     => array( array( 'key' => '_myblur_hash', 'compare' => 'NOT EXISTS' ) ),
) );
foreach ( $ids as $id ) {
	myblur_process( $id, wp_get_attachment_metadata( $id ) );
	WP_CLI::log( "done: {$id}" );
}
WP_CLI::success( count( $ids ) . ' 件を処理しました' );

1回で200件ずつ処理し、「0件」になるまで繰り返します。処理後は、画像を含むページをブラウザで開き、開発者ツールのネットワーク設定で回線速度を落として、ぼかしが表示されてから本体に切り替わるかを目で確認します。

まとめ

  • Blurhashは画像を短い文字列に要約し、ぼかし画像として復元する仕組みです。通常のテーマではサーバー側で小さなPNGに復元してCSS背景に使うと、JavaScriptが要りません。
  • 計算は wp_generate_attachment_metadata で、元画像ではなく縮小した中間サイズから行います。$metadata は変更せずに返します。
  • 表示は wp_get_attachment_image_attributes、本文画像は wp_content_img_tag で行い、値はエスケープします。透過画像とCLS対策(width/height属性)は別に考えます。
  • ヘッドレス構成では register_rest_field() で文字列を返し、フロント側で復元します。
  • ライブラリの仕様やWordPressの画像出力の挙動(2026年10月現在)は変わることがあるため、更新時はステージングで表示を確かめてから本番に反映してください。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲