久々のブログだ〜。 最近はフリーになりました。忙しいですがお仕事はそれなりに探してたり探してなかったりします。

さて、メディアサービスとか画像をそれなりに使うようなサイトだと、クライアントサイドのパフォーマンスのために画像のサイズや品質を数パターン用意しなきゃいけないことがあると思います。

が、さすがに最初から必要なパターンは想定できないし、ならnginxで動的に変換しちゃおうみたいになることが多いんじゃないでしょうか。

僕も今回ちょっと構築したりしたんだけど、設定を忘れがちなので自分用にメモっときたい。

余談

ちなみにこれは存在自体は知ってたんだけど、がっつりプロダクションで使わせてもらったのは以下サービスに携わってるとき。

アルバイト探しならJOBLIST[ジョブリスト]

@mikedaさんが構築した環境でへーこうやるのかー!と思っていつかつーかおと思ってた。

以下記事も参考にしてね。僕なんかよりよっぽどわかりやすい書かれてるよ。

AWS、nginxでお手軽動的サムネイル - Rista Tech Blog

エンジニアも募集してるから興味あったら行ってみてね。僕(@dim0627)に声かけてもらっても大丈夫だよ。

掲載数7万件突破!求人業界を変えたいRailsエンジニアWANTED! - 株式会社リスタのWeb エンジニア中途・インターンシップの求人 - Wantedly

ngx_http_image_filter_module

画像のリサイズやクロップをやってくれるモジュールはこれ。

Module ngx_http_image_filter_module

例にあるように、こんな感じでリサイズとか回転ができる。

location /img/ {
    proxy_pass   http://backend;
    image_filter resize 150 100;
    image_filter rotate 90;
    error_page   415 = /empty;
}

location = /empty {
    empty_gif;
}

Install

こういうモジュールを使うとき、nginxはビルド時になんかパラメータを付けなきゃいけなかったんだけど、最近ダイナミックモジュールとやらが出てきたので比較的簡単に導入できる。

image filterについては以下記事が参考になる。

nginxが入ってるなら、動的モジュールをyumで入れちゃえばよい。 ちなみにこの作業はAmazon Linuxでやってます。(2じゃないよ)

yum install nginx-mod-http-image-filter

導入したらnginx.confから読み込ませる。1行目がそれ。

include /usr/share/nginx/modules/mod-http-image-filter.conf;

user nginx;
worker_processes 1;

events {
    worker_connections 1024;
}

http {
    include mime.types;
    default_type application/octet-stream;

    sendfile on;
    keepalive_timeout 65;

    server {
        listen 80;
        server_name your-assets.example.jp;

        location ~ ^/crop/w(\d+)h(\d+)q(\d+)/(.*)$ {
            set $width $1;
            set $height $2;
            set $quality $3;
            set $path $4;

            image_filter crop $width $height;
            image_filter_jpeg_quality $quality;
            image_filter_buffer 2M;
            rewrite ^ /$path break;

            proxy_pass http://your-assets.s3-website-ap-northeast-1.amazonaws.com;
        }

        location ~ ^/resize/w(\d+)h(\d+)q(\d+)/(.*)$ {
            set $width $1;
            set $height $2;
            set $quality $3;
            set $path $4;

            image_filter resize $width $height;
            image_filter_jpeg_quality $quality;
            image_filter_buffer 2M;
            rewrite ^ /$path break;

            proxy_pass http://your-assets.s3-website-ap-northeast-1.amazonaws.com;
        }

        location / {
            proxy_pass http://your-assets.s3-website-ap-northeast-1.amazonaws.com;
        }
    }
}

回転とかは全然いらないので、リサイズとクロップ、それぞれで縦横と品質を指定できるようにしてる。

URLはこんな感じにすればリサイズ、クロップができます。品質の数値については議論あると思うので言及しません!

リサイズ

/resize/w150h70q82/uploads/example.jpg

クロップ

/crop/w150h70q82/uploads/examplejpg

オリジナル

/uploads/example.jpg

RailsでURLをうまいことやる

縦横と品質はURLで指定するので、RailsみたいにURLを生成するためのヘルパがある場合はうまいこと工夫するとらく。

CarrierWaveを使ってる前提で、マウントしたカラムを渡してURLを生成させるメソッドの例としてはこんな感じ。

module ImagesHelper
  def resized_url(mounted_column, w: 100, h: 100, q: 100)
    return mounted_column.url if mounted_column.blank?

    uri = URI.parse(mounted_column.url)
    uri.path = "/resize/w#{w}h#{h}q#{q}#{uri.path}"
    uri.to_s
  end

  def cropped_url(mounted_column, w: 100, h: 100, q: 100)
    return mounted_column.url if mounted_column.blank?

    uri = URI.parse(mounted_column.url)
    uri.path = "/crop/w#{w}h#{h}q#{q}#{uri.path}"
    uri.to_s
  end
end

Low Quality Image Placeholderにも

最近よく使われてますよね、Mediumとかが使ってるのが有名なのかな。

品質を5とかに設定できるので、比較的大きい画像も5KBとかにおさえられたりします。

本来であればBASE64エンコードされた画像データをHTMLに埋め込みたいんだけど、それをやるのはちょっと手間なので、簡易LQIPみたいなやり方であればすぐできちゃう。

ぼかしをかけたいならこんな感じで。

.lqip-container {
  &[data-unloaded] {
    overflow: hidden;

    & img {
      filter: blur(1rem);
    }
  }
}

Turbolinks前提だけど、JSは雑にこんな感じでだろうだろう。 noscriptの対応してないけど・・・。

// Loq Quality Image Placeholderの機能を提供する
const loadedImages = [];

// キャッシュ済み画像として格納する
const storeLoadedImages = (path) => {
  loadedImages.push(path);
};

// キャッシュ済み画像かを判定する
const isLoadedImage = (path) => {
  return loadedImages.indexOf(path) >= 0;
};

document.addEventListener('turbolinks:load', () => {
  const lqipImages = document.querySelectorAll('[data-lqip-src]');
  if (!lqipImages) return;

  for (let i = 0; i < lqipImages.length; i += 1) {
    const lqipImage = lqipImages[i];

    // Turbolinksのキャッシュに乗ってる場合は処理しない
    if (isLoadedImage(lqipImage.getAttribute("src"))) {
      lqipImage.parentNode.removeAttribute("data-unloaded");
      continue;
    }

    // キャッシュ済み配列に格納
    storeLoadedImages(lqipImage.getAttribute("src"));

    // プレースホルダに一時差し替え
    const fullSrc = lqipImage.getAttribute("src");
    lqipImage.setAttribute("src", lqipImage.getAttribute("data-lqip-src"));

    // 正しいimgタグを生成して読み込みを指せる
    const fullImage = new Image();
    fullImage.setAttribute("src", fullSrc);

    // ロードが完了したらsrcを差し替える
    fullImage.addEventListener("load", () => {
      setTimeout(() => {
        lqipImage.src = fullSrc;
        lqipImage.parentNode.removeAttribute("data-unloaded");
      }, 100);
    });
  }
});

DOM用のヘルパはたとえばこんな感じとか?

def lqip_image_tag(lqip_src, original_src, alt:)
  content_tag :div, class: "lqip-container", data: { unloaded: true } do
    image_tag original_src, alt: alt, data: { "lqip-src": lqip_src }
  end
end

ここまでやれば、素の画像をそのまま使いまわすよりだいぶユーザ体験はよくなるんじゃないかな〜。

webp対応とかもまだまだやれるけど、それはまた別の機会に!

Happy Performance Tuning!