はじめに
お久しぶりです、しょーへいです。
今回は、最近経験した「キャッシュによって画像の差し替えがうまくいかなかった」話をしようと思います。
背景
とあるホームページの管理をしており、頻繁にイベント情報の更新を手動で行っていました。
イベント情報を更新するたびにソースコードを修正する必要があり、管理に手間がかかっていました。
管理者ページを作成することで、依頼者自身でイベント情報の新規作成と更新を行えるようにしようと考えました。
イベント情報の新規作成機能はスムーズに実装でき、問題ありませんでした。
しかし、すでに登録したイベント情報の画像を差し替える際に問題が発生しました。
事象
イベント情報の変更機能を実装し終え、いざ動作確認をしてみると
画像の差し替えがうまくいく場合と、うまくいかない場合がありました。
実装内容
DBにSupabaseを使用して開発していました。
events_tableでid,title,date,imageのカラムで管理していました。
| id | title | date | image |
|---|---|---|---|
| 1 | ホームパーティ | 2026-10-13 | 1/image.png |
| 2 | 映画鑑賞 | 2026-10-14 | 2/image.jpg |
events_tableのimageカラムには、Storageに保存した画像のパスを格納していました。
画像を保存する際は全て、imageという名前に統一して保存していました。
画像はStorageのevent_imagesでevents_tableのidと紐付けて画像を保存していました。
event_images/
├── 1/
│ └── image.png (A画像とする)
│
└── 2/
└── image.jpg
画像を取得する箇所からSupabaseのStorageに格納するまでのソースコードを以下に記します。
※ここには記していませんが、画像を変更する際はすでにある画像データを削除してから変更動作に移るように設計していました。
const image = formData.get("image"); //formから画像情報を取得
if (!(image instanceof File)) {
throw new Error("画像ファイルがありません");
}
const extension = image.name.split(".").pop(); //ファイルの拡張子の取得
const image_path = `${data.id}/image.${extension}`; //保存パスの作成
// Storageに画像を保存
const { error: uploadError } = await supabase.storage
.from("event_images")
.upload(image_path, image);
if (uploadError) {
throw new Error(
`画像のアップロードに失敗しました: ${uploadError.message}`
);
}
// DBに画像の保存パスを保存
const { error: image_updateError } = await supabase
.from("events_table")
.update({ image: image_path })
.eq("id", data.id)
.select("id, image");
if (image_updateError) {
throw new Error(
`画像のパス保存に失敗しました: ${image_updateError.message}`
);
}
発生した問題
id:1のホームパーティのA画像(image.png)からB画像(image.jpg)に差し替えたところ、
正しくB画像(image.jpg)が表示されました。
その後、B画像(image.jpg)からC画像(image.png)に差し替えたところ、
C画像(image.png)ではなくA画像(image.png)が表示されてしまいました。
原因
原因は、キャッシュでした。
画像を保存する際、ファイル名をimageに統一していたため、
同じイベントIDで、以前使用した画像と同じ拡張子の画像をアップロードすると、以前と同じURLが生成される仕組みになっていました。
画像パスは以下のようになっており、A画像とC画像では同じ画像パスが使用されていました。
A画像:1/image.png
B画像:1/image.jpg
C画像:1/image.png
ブラウザは、一度取得した画像をキャッシュとして保存し、同じURLの画像が要求された場合、キャッシュを再利用することがあります。
そのため、Storage上の画像をC画像に更新しても、ブラウザが以前取得したA画像をキャッシュから読み込んでしまい、画像が正しく表示されない事象が発生しました。
【最初の画像表示】
ブラウザ
│
│ 1/image.png を要求
▼
Supabase Storage
│
│ A画像を返す
▼
ブラウザ
│
└── A画像をキャッシュ
【画像を更新した後】
Supabase Storage
│
└── 1/image.png = C画像
ブラウザ
│
│ 1/image.png を要求
▼
キャッシュ
│
└── A画像を返す(古い画像)
対策
画像を保存する際の名前を一意の値にすることで対策しました。
一意の値にするために、Date.now()を使用しました。
Date.now()を使用することで、現在時刻のタイムスタンプをミリ秒単位で取得できます。
取得したタイムスタンプをファイル名に付与することで、画像を更新するたびに異なるURLが生成されるようにしました。
このようにURLを変更して古いキャッシュの再利用を防ぐ方法を「キャッシュバスティング(Cache Busting)」と呼びます。
const image = formData.get("image"); //formから画像情報を取得
if (!(image instanceof File)) {
throw new Error("画像ファイルがありません");
}
const extension = image.name.split(".").pop(); //ファイルの拡張子の取得
- const image_path = `${data.id}/image.${extension}`; //保存パスの作成
+ const image_path = `${data.id}/image.${Date.now()}.${extension}`; //保存パスの作成
// Storageに画像を保存
const { error: uploadError } = await supabase.storage
.from("event_images")
.upload(image_path, image);
if (uploadError) {
throw new Error(
`画像のアップロードに失敗しました: ${uploadError.message}`
);
}
// DBに画像の保存パスを保存
const { error: image_updateError } = await supabase
.from("events_table")
.update({ image: image_path })
.eq("id", data.id)
.select("id, image");
if (image_updateError) {
throw new Error(
`画像のパス保存に失敗しました: ${image_updateError.message}`
);
}
まとめ
今回の問題は、Webに関する知識が不足していたことで発生したものだと感じました。
しかし、実際に問題に直面したことで、キャッシュの仕組みについて学ぶことができました。
今後も開発中に遭遇した問題や、そこから学んだことを投稿していきたいと思います!