「Webサイトやアプリに提供する画像って必要最低限のサイズでいいけどそういうこと出来ないかな?」
こんにちは、タカフです。
Webサイトやアプリや作っていると、そこで表示している画像サイズって気になりますよね。
画像サイズが重ければそれだけで画面表示まで時間がかかり、Webサイトならページ表示速度という面でSEOにも響いてきます。
アプリに必要な画像サイズというのはその端末サイズでキレイに見れる分だけのサイズであればいいのです。
どういうことかと言いますと、
例えばアプリに表示すべき画像ファイルが、幅6000px・高さ4000pxのサイズで4MBもの大きなファイルだった時に、
実際アプリに必要な画像はretina対応を考慮してもせいぜい幅600px・高さ400pxの画像さえあれば良いというわけです。
こういう時に便利なサービスがあります。
アプリ側から画像サイズをパラメータとしてURLで指定するとそのサイズで画像変換してくれるサービスです。
有名どころだとImgixとかですが、日本製だとさくらサーバーの会社が提供しているImageFluxです。gumletというのもあります。
これらのサービスでは
https://www.example.com/img/food.jpg?width=640
というようなGETパラメータで画像サイズを指定すると、その画像サイズに変換して且つそのサービス側でキャッシュしてくれて、その後はそのURLなら高速で画像を表示出来るというものです。
ただ今回は、そもそもシステム的に使えないとか、小規模だけどアプリやWebサイトを高速化させたい、という時に、このサービスを自前で構築する方法となっています。
PHPとCloudFrontを使って自前でこれを構築出来ます。
システム概要
システム概要図としては以下の通りになります。
アプリから必要なサイズで画像リクエストをして、CloudFrontを経由してオリジンサーバーで画像変換をしてリサイズした画像を返すというものです。
因みにこの図ではオリジンサーバーにEC2を書いてますが、
場合によってはロードバランサーがはさむこともあるでしょうし、また、
オリジンサーバーにはAWS以外のサーバーでも指定することが可能です。
PHP画像変換処理の実装
それではPHPによる画像変換処理を実装していきましょう。
今回のシステムは、特定パラメータがついたURLの場合に限り.htaccessでRewriteRuleしてPHP処理をする流れとなっています。
構築に必要なのは、
- 画像変換処理ライブラリのインストール
- .htaccessでRewriteRuleの処理
- PHPで画像リサイズと画像出力
です。順を追って説明します。
画像変換処理ライブラリのインストール
画像変換処理にはgumletのphp-image-resize
というライブラリを使用します。
https://packagist.org/packages/gumlet/php-image-resize
これを提供しているgumletは上記でも説明した画像変換キャッシュサービスを提供している会社ですが、
なんとその処理をライブラリとしてcomposerで提供しているのです。
同じ目的で作られたライブラリなのが安心できますよね。
packagistのドキュメントでも書いてますが、
If you don’t want to crop, resize and store images on your server, Gumlet.com is a free service which can process images in real-time and serve worldwide through CDN.
画像をトリミング、サイズ変更、サーバーに保存したくない場合、Gumlet.comは無料のサービスであり、リアルタイムで画像を処理し、CDNを通じて世界中で提供できます。
とのこと。
恐らく画像変換処理ライブラリは無料で提供することで認知度を広めてここの有料プランを使ってもらうというビジネスモデルなんでしょう。
それではcomposerを使ってインストールします。
$ composer require gumlet/php-image-resize
これだけで画像変換処理が使えるようになりました。
.htaccessでRewriteRuleの処理
画像リクエストがあった時に特定パラメータがついていた場合はPHPを動作させるようにします。
以下の.htaccess
をオリジンサーバーの画像変換したいディレクトリに設置します。
# CORS許可
Header set Access-Control-Allow-Origin "*"
# これはsystem内でのルーティング用RewriteRule
<IfModule mod_rewrite.c>
RewriteEngine On
# リクエストファイル名の最後の文字列が画像ファイルの場合はPHPで処理をする
# 画像ファイルのリクエストはこれに合致するのでRewriteRuleしない
RewriteCond %{REQUEST_FILENAME} ^.+\.(gif|jpg|jpeg|png)$
# ファイルが存在するとき
RewriteCond %{REQUEST_FILENAME} -f
# リサイズパラメータがある時
RewriteCond %{QUERY_STRING} (w=|h=)
# image.phpを呼び出す
RewriteRule ^(.*)$ ./image.php [QSA,L]
</IfModule>
これにより、少しでもサーバー負荷を上げない為に特定パラメータの時だけ画像変換処理のPHPを動かすようにします。
PHPで画像リサイズと画像出力
.htaccess
と同ディレクトリに以下のPHPを設置します。
composerのautoload.php
のパスは適宜変更してください。
<?php
require_once __DIR__ . "/vendor/autoload.php";
use \Gumlet\ImageResize;
// 画像ファイルの絶対パスを取得
if ( !empty($_SERVER["REDIRECT_URL"]) ){
$image_path = $_SERVER["DOCUMENT_ROOT"] . $_SERVER["REDIRECT_URL"];
}else{
$request_uri = $_SERVER["REQUEST_URI"];
$parse_result = parse_url($request_uri); // QuetyStringを取り除く為
$image_path = $_SERVER["DOCUMENT_ROOT"] . $parse_result["path"];
}
if ( !file_exists($image_path) ){
die("no image file.");
}
// width・heightを取得
$width = (isset($_GET["w"]) && is_numeric($_GET["w"])) ? intval($_GET["w"]) : null;
$height = (isset($_GET["h"]) && is_numeric($_GET["h"])) ? intval($_GET["h"]) : null;
// キャッシュ時間はお好みで!ここでは100日
$expire_seconds = (60 * 60 * 24 * 100);
// 画像のキャッシュをコントロール
header('Cache-Control: max-age=' . $expire_seconds);
try{
/* パラメータに合わせて画像をリサイズする */
$image = new ImageResize($image_path);
if( $width && $height ){
$image->resizeToBestFit($width, $height);
}else if( $width ){
$image->resizeToWidth($width);
}else if( $height ){
$image->resizeToHeight($height);
}
$image->output();
}catch(Exception $ex){
/* 何かしらの例外発生した場合はフェイルセーフとして元画像をそのまま出力する */
// MIMEタイプの取得
$finfo = new finfo(FILEINFO_MIME_TYPE);
$mime_type = $finfo->file($image_path);
header('Content-Type: ' . $mime_type);
readfile($image_path);
}
GETパラメータの値を取得して、その値に従った画像に変換してoutput
メソッドで画像としてレスポンス出すようにしてます。
キャッシュ時間もここで設定しています。サンプルコードは100日としていますが、システムに応じて任意の値を設定すると良いです。
また何かしらの例外が発生した時は、元の画像をフェイルセーフとして元画像をそのまま出力する処理にしています。
CloudFrontの構築
今回のシステムを構築するのに大切なのはそのキャッシュ設定です(Behavior設定)。
以下のようなキャッシュ設定にしています。
ミソとしては、
Minimum TTLとDefault TTLを0にして、Maximum TTLを1年にすることで、あとはキャッシュ時間をオリジンサーバー側で決めるようにしています。
先ほどのPHP側で
header('Cache-Control: max-age=' . $expire_seconds);
が実際のキャッシュ時間となるわけですね。
この方が柔軟性のあるキャッシュ時間に出来ます。
また、Quety String Forwarding and CachingいわゆるGETパラメータをどんなパラメータをも受け付けるように Forward all cache based on all
にしています。
実装結果
それでは実際にどのような挙動になるか見ていきましょう。
4MBのファイルをサーバーに置いてみて、
まずは普通にCloudFront経由でアクセスしてみます。
https://xxxxxxxxxxx.cloudfront.net/img/food.jpg
はい、3.9MBのファイル容量で、333msもかかってますね。
こんな画像がWebサイトに配置されていたら最悪ですね。
では次にパラメータ付きで幅640でリクエストしてみます。
https://xxxxxxxxxxx.cloudfront.net/img/food.jpg?w=640
6.67secこそかかってますが、ファイル容量は38KBにも減らすことが出来ました。
時間がかかっているのは、このテスト用に立てたサーバーが貧弱だったのと元画像が大きすぎるせいですね。
実運用ではアップロード時にも元画像をいくらかリサイズするといいですね。
そして、再度同じURLでリクエストをしてみます。
https://xxxxxxxxxxx.cloudfront.net/img/food.jpg?w=640
今度はキャッシュが効いているので、38KBのファイル容量でかつ18msecにまで短縮出来ました。
爆速ですね。
18msで画像を表示出来たら最高なえくすぺりえんすをユーザーに提供出来ていると言えますね。
気をつけるべき事
これを実際の稼働で利用するには気をつけるべき事があります。
それなりのメモリを積んだサーバーと、いたずら対策が必要です。
画像変換にはCPUとメモリーを結構食うので、強力なサーバーでしかやらないとか、もしくはこれ専用として別サーバーを用意するとかした方がいいです。
また、不本意のパラメータでたくさんリクエストを受け付けてその全てを画像変換していると、とてつもないサーバーリソースが必要で最悪サーバーが落ちたりするので、そのようないたずらの対策が必要です。
なので、許可された画像サイズだけを画像変換する等の処理を入れておく必要があります。
まとめ
このスマホ時代かつ格安SIM時代に、通信料のかかるWebサイトやアプリなんてユーザーに受け入れられません。
このような工夫を入れることで端末側の通信料は軽くしたり、アプリ表示を高速化することが出来ます。
と、ここまで書いといてなんですが、
PHPで画像変換処理は結構サーバーに負荷かかるので、出来ることならAWSのCloudFrontとlambda@edgeを使ってサーバーレスでこの処理をやった方がいいです。
その記事はまた別で書こうと思いますが、ただその手法は若干とっつきにくいのでとりあえずお手軽に構築するなら今回のPHP+CloudFrontでの方法でも十分使えると思います。
以上、カフーブログの提供でお送りしました。