Help us understand the problem. What is going on with this article?

更新されたJS、CSS、画像のみブラウザキャッシュを破棄して読み込ませる

More than 1 year has passed since last update.

 WEBサービスの保守や開発をやっていると、JavaScriptやスタイルシート、画像などの静的リソースを更新した際、しばしば「確認したけど、更新されてないよ」とか云われて、「ブラウザのキャッシュを消してからもう一度見てみてください」みたいなやり取りが発生することがある。これが、サービス内部のメンバー間のやり取りであれば(非効率ではあるものの)まだ許せるが、サービスを提供している顧客側に更新内容が反映されない事態が発生してしまうと、それは障害と同義だ。

 そんなわけで、WEBページにおける静的リソースの読み込みには開発時に注意を払う必要がある。確実な対処方法としては、静的リソースの読み込み時にリソースパスに動的パラメータを付与して、ブラウザにキャッシュされないようにすることだ。
 下記のように、フロントエンドだけで対応することもできる。

<link href="/css/style.css?20180101010100" rel="stylesheet" type="text/css">

<img src="/images/sample.jpg?20180101010100" alt="Sample">

 読み込む静的リソースのファイルパスの後にGETパラメータとして更新日時等を付けてやるだけだ。これで、それらの静的リソースはブラウザに新しいリソースとして認識され、それまでキャッシュされていたリソースは破棄される。
 だが、この方法だと、複数の画像を更新した際にHTML側で対応しなければならない箇所が多岐に渡ってしまうし、実際のページを見ながらスタイルシートを調整している時などはスタイルシートを更新する都度パラメータを変更しなければならず、作業効率が非常に悪くなる。
 そこで、リソースパスに対しては常に動的パラメータを付与するようにしてみる。

PHPでの例
<link href="/css/style.css?<?= strtotime('now') ?>" rel="stylesheet" type="text/css">
# => <link href="/css/style.css?1516854535" rel="stylesheet" type="text/css">
Rubyでの例
= image_tag "/images/sample.jpg?#{Time.now.to_i}"
# => <img alt="Sample" src="/images/sample.jpg?1516854061">

 こんな感じに、PHPやRubyでページのテンプレートに元から動的パラメータを仕込んでしまえば良いのだ。

 しかし、これだと毎回ページを読み込んだ際に静的リソースの読み込みが発生してしまい、WEBサービスとしてのパフォーマンスが著しく悪くなってしまうという弊害がある。理想としては、更新が発生した静的リソースのみブラウザキャッシュを破棄して読み込ませ、更新されていないリソースファイルにはブラウザキャッシュを効かせたい。

 そういう時に使えるメソッドが、PHPやRubyなどには準備されている。

PHPなら filemtime() を使おう

 PHPの filemtime() は、指定されたファイルの更新時刻を取得する関数だ。これを使って静的リソースファイルのパスに動的パラメータを付与しておけば、リソースファイルが更新されない限り動的パラメータ部分が変わらないため、一度ブラウザにキャッシュされたリソースを有効に活用できる。
 実際には、PHPでレンダリングするテンプレート等で下記のようにコーディングしておけば良い。

基本
<link href="/css/style.css?<?= filemtime( "{$_SERVER['DOCUMENT_ROOT']}/css/style.css" ) ?>" rel="stylesheet" type="text/css">

 注意点としては、filemtime() に指定するリソースファイルのパスは、サーバ上でのパスになるということだ。しかし、上記のような記述をテンプレート上のすべての静的リソースに行うのはテンプレートの可読性が著しく下がるので、指定されたファイルパスに動的パラメータが付与されたURIを返すような関数を作った方が良いだろう。

独自関数化
function optimize_uri( $resource_absolute_path=null ) {
  $docroot = $_SERVER['DOCUMENT_ROOT'];
  if ( ! empty( $resource_absolute_path ) && file_exists( $docroot . $resource_absolute_path ) ) {
    $resource_absolute_path .= '?' . filemtime( $docroot . $resource_absolute_path );
  }
  return $resource_absolute_path;
}

 上記の関数を使って静的リソースファイルを読み込むテンプレートの例は下記のようになる。

関数による最適化
<link href="<?= optimize_uri( '/css/style.css' ) ?>" rel="stylesheet" type="text/css">
# 指定されたリソースファイルがあれば、ファイルの更新日時のタイムスタンプをパラメータに付与する
# => <link href="/css/style.css?1516855734" rel="stylesheet" type="text/css">

<script src="<?= optimize_uri( '/js/none.js' ) ?>"></script>
# 指定されたリソースファイルがなければ、指定されたファイルパスをそのまま返す
# => <script src="/js/none.js"></script>

<img src="<?= optimize_uri( '/images/sample.jpg' ) ?>" alt="Sample">
# => <img src="/images/sample.jpg?1516855734" alt="Sample">

Rubyなら File.mtime() を使おう

 Rubyでは Fileオブジェクト の mtime() メソッドでファイルの更新日時を取得できる。これを使って静的リソースファイルのパスに動的パラメータを付与していけば、リソースファイルが更新されない限り動的パラメータ部分が変わらないため、一度ブラウザにキャッシュされたリソースを有効に活用できるようになる。

基本(Railsプロジェクト)
no_cache_param = Time.parse( File.mtime( "#{Rails.public_path}/images/sample.jpg" ).to_s ).to_i
= image_tag "/images/sample.jpg?#{no_cache_param}"
# => <img alt="Sample" src="/images/sample.jpg?1451907511">

 注意点としては、File.mtime() に指定するリソースファイルのパスは、サーバ上でのパス(RailsであればRails上の公開ディレクトリ経由のパス)になるということだ。しかし、上記のような記述をテンプレート上のすべての静的リソースに行うのはテンプレートの可読性が著しく下がるので、指定されたファイルパスに動的パラメータが付与されたURIを返すような関数を作った方が良いだろう。 

独自関数化
def optimize_uri(resource_absolute_path = nil)
  return resource_absolute_path if resource_absolute_path.blank?
  return resource_absolute_path unless File.exist?("#{Rails.public_path}#{resource_absolute_path}")
  no_cache_param = Time.parse(File.mtime("#{Rails.public_path}#{resource_absolute_path}").to_s).to_i
  resource_absolute_path = "#{resource_absolute_path}?#{no_cache_param}"
end

 Railsアプリなら、アプリケーション全体の共通処理用ヘルパーメソッド(app/helpers/application_helper.rb)に上記の関数を定義しておけば良い。そして、Viewのテンプレートではその関数にリソースパスを渡すようにする。

関数による最適化
= stylesheet_link_tag optimize_uri('/css/style.css')
# 指定されたリソースファイルがあれば、ファイルの更新日時のタイムスタンプをパラメータに付与する
# => <link href="/css/style.css?1516780069" media="screen" rel="stylesheet">

= javascript_include_tag optimize_uri('/js/none.js')
# 指定されたリソースファイルがなければ、指定されたファイルパスをそのまま返す
# => <script src="/js/none.js"></script>

= image_tag optimize_uri('/images/sample.png')
# => <img alt="Sample" src="/images/sample.jpg?1451907511">

おまけ: WordPressでの静的リソースの読み込みを最適化する

 WordPressではテーマへの静的リソースの読み込みは、スタイルシートなら wp_enqueue_style() 関数、スクリプトなら wp_enqueue_script() 関数を使って wp_enqueue_scripts のアクションフックに読み込み処理を追加するのがセオリーである。
 wp_enqueue_style()wp_enqueue_script() の関数では、引数で読み込むリソースのバージョン番号などを指定することができるので、そこにキャッシングを抑止するための動的パラメータを渡してあげるのが良いだろう。もちろん、バージョン管理されている外部リソースであればバージョン番号をそのまま指定するべきだが、厳密なバージョン管理を行っていない自サイトのみで使っている静的リソースであれば動的パラメータを付与しておくと効果的にキャッシングされる。

WordPressテーマのfunctions.php等
add_action( 'wp_enqueue_scripts', function() {
  // Stylesheets to load
  wp_enqueue_style(
    'style-handle-name', 
    get_stylesheet_directory_uri() . '/css/style.css', 
    array(), 
    filemtime( get_template_directory() . '/css/style.css' ) 
  );

  // JavaScripts to load
  wp_enqueue_script(
    'script-handle-name', 
    get_template_directory_uri() . '/js/script.js', 
    array(), 
    filemtime( get_template_directory() . '/js/script.js' ), 
    true
  );
}, PHP_INT_MAX );

 画像リソースについては、各WordPressサイトやテーマによって取り扱いが異なるので、キャッシングの最適化は個別対応となってしまう。もしWordPressにビルドインされているテンプレート関数を利用して出力を行っているのであれば、 アイキャッチ画像ならば post_thumbnail_html フィルター、本文内の埋め込み画像ならば the_content フィルター、管理画面からアップロードされたメディアならば get_attached_file フィルターなどを使って画像ファイルのパスを拾ってフィルタリングしてあげることになるかと。

ka215
フリーランスのWEBデベロッパー。元々コンシューマゲーム機のチェッカーで、ゲーム好き。WEB業界はWEBデザイナーとして出発し、いまではクラウド系インフラ構築から、アプリケーション開発、サービスコンサルティング等々、およそWEB関連のよろず屋をやってます。近いうちにGitHub公開中のjQuery.Timelineを脱jQuery化したい。今は「MHW:IB」中、大霊脈玉ドロップしなさ過ぎ…orz
https://ka2.org
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした