Skip to content

Instantly share code, notes, and snippets.

@H01001000
Created January 6, 2025 09:14
Show Gist options
  • Save H01001000/1091709d6109211488fd6ea2dc6e0ee6 to your computer and use it in GitHub Desktop.
Save H01001000/1091709d6109211488fd6ea2dc6e0ee6 to your computer and use it in GitHub Desktop.
This patch allow using redis or keyDB for caching next.js's image optimizer cache, for yarn on next 15.1.2
diff --git a/dist/server/image-optimizer.js b/dist/server/image-optimizer.js
index bf6beaeeffab903e0b02677fd633729df20f27ed..ce0549fbd0b21d7f4c610958e9eb1bcd0b5d79f8 100644
--- a/dist/server/image-optimizer.js
+++ b/dist/server/image-optimizer.js
@@ -466,33 +466,57 @@ class ImageOptimizerCache {
mimeType
]);
}
- constructor({ distDir, nextConfig }){
+ constructor({ distDir, nextConfig, redisClient }){
this.cacheDir = (0, _path.join)(distDir, 'cache', 'images');
this.nextConfig = nextConfig;
+ this.redisClient = redisClient;
}
async get(cacheKey) {
try {
- const cacheDir = (0, _path.join)(this.cacheDir, cacheKey);
- const files = await _fs.promises.readdir(cacheDir);
- const now = Date.now();
- for (const file of files){
- const [maxAgeSt, expireAtSt, etag, upstreamEtag, extension] = file.split('.', 5);
- const buffer = await _fs.promises.readFile((0, _path.join)(cacheDir, file));
+ if (process.env.REDIS_IMAGE_CACHE === 'true') {
+ const file = await this.redisClient.hgetall(cacheKey)
+ if (Object.keys(file).length === 0) return null
+
+ const now = Date.now();
+ const {maxAge: maxAgeSt, expireAt: expireAtSt, etag, upstreamEtag, extension, buffer} = file;
const expireAt = Number(expireAtSt);
const maxAge = Number(maxAgeSt);
return {
value: {
kind: _responsecache.CachedRouteKind.IMAGE,
- etag,
- buffer,
- extension,
- upstreamEtag
+ etag,
+ buffer: Buffer.from(buffer, "base64"),
+ extension,
+ upstreamEtag
},
revalidateAfter: Math.max(maxAge, this.nextConfig.images.minimumCacheTTL) * 1000 + Date.now(),
curRevalidate: maxAge,
isStale: now > expireAt,
isFallback: false
};
+ } else {
+ const cacheDir = (0, _path.join)(this.cacheDir, cacheKey);
+ const files = await _fs.promises.readdir(cacheDir);
+ const now = Date.now();
+ for (const file of files){
+ const [maxAgeSt, expireAtSt, etag, upstreamEtag, extension] = file.split('.', 5);
+ const buffer = await _fs.promises.readFile((0, _path.join)(cacheDir, file));
+ const expireAt = Number(expireAtSt);
+ const maxAge = Number(maxAgeSt);
+ return {
+ value: {
+ kind: _responsecache.CachedRouteKind.IMAGE,
+ etag,
+ buffer,
+ extension,
+ upstreamEtag
+ },
+ revalidateAfter: Math.max(maxAge, this.nextConfig.images.minimumCacheTTL) * 1000 + Date.now(),
+ curRevalidate: maxAge,
+ isStale: now > expireAt,
+ isFallback: false
+ };
+ }
}
} catch (_) {
// failed to read from cache dir, treat as cache miss
@@ -508,7 +532,18 @@ class ImageOptimizerCache {
}
const expireAt = Math.max(revalidate, this.nextConfig.images.minimumCacheTTL) * 1000 + Date.now();
try {
- await writeToCacheDir((0, _path.join)(this.cacheDir, cacheKey), value.extension, revalidate, expireAt, value.buffer, value.etag, value.upstreamEtag);
+ if (process.env.REDIS_IMAGE_CACHE === 'true') {
+ await this.redisClient.hmset(cacheKey, {
+ extension: value.extension,
+ maxAge: revalidate,
+ expireAt,
+ buffer: value.buffer.toString("base64"),
+ etag: value.etag,
+ upstreamEtag: value.upstreamEtag
+ })
+ } else {
+ await writeToCacheDir((0, _path.join)(this.cacheDir, cacheKey), value.extension, revalidate, expireAt, value.buffer, value.etag, value.upstreamEtag);
+ }
} catch (err) {
_log.error(`Failed to write image to cache ${cacheKey}`, err);
}
diff --git a/dist/server/next-server.js b/dist/server/next-server.js
index 68b9639d21c369a1d574ed548b7953c188c1ab97..1e0542ca207b97fd500fcb4880934931f399a08a 100644
--- a/dist/server/next-server.js
+++ b/dist/server/next-server.js
@@ -62,6 +62,7 @@ const _routekind = require("./route-kind");
const _invarianterror = require("../shared/lib/invariant-error");
const _awaiter = require("./after/awaiter");
const _asynccallbackset = require("./lib/async-callback-set");
+const Redis = require("ioredis");
function _export_star(from, to) {
Object.keys(from).forEach(function(k) {
if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) {
@@ -154,7 +155,8 @@ class NextNodeServer extends _baseserver.default {
const { ImageOptimizerCache } = require('./image-optimizer');
const imageOptimizerCache = new ImageOptimizerCache({
distDir: this.distDir,
- nextConfig: this.nextConfig
+ nextConfig: this.nextConfig,
+ redisClient: this.redisClient
});
const { sendResponse, ImageError } = require('./image-optimizer');
if (!this.imageResponseCache) {
@@ -373,6 +375,11 @@ class NextNodeServer extends _baseserver.default {
}
return result.finished;
};
+ this.redisClient = process.env.REDIS_IMAGE_CACHE ? new Redis({
+ host: process.env.REDIS_HOST,
+ port: parseInt(process.env.REDIS_PORT ?? "6379"),
+ password: process.env.REDIS_PASSWORD,
+ }) : null;
/**
* This sets environment variable to be used at the time of SSR by head.tsx.
* Using this from process.env allows targeting SSR by calling
@H01001000
Copy link
Author

Backstory: Im selfhosting Next.js on multiple vm on cloud, those vm are very small and takes a bit time and cpu for rendering the image.

By using this patch + keyDB multi master replication, Im able to sync the image cache, and restore after update the app

requirement:
only tested on next 15.1.2

install:

  1. put in yarn's .yarn/patches folder
  2. update next js version to patch:next@npm%3A15.1.2#~/.yarn/patches/next-npm-15.1.2-24e7411703.patch in package.json
  3. run yarn

usage:
add follow env

REDIS_IMAGE_CACHE=true # setting to anything other than false will disable it
REDIS_HOST="<some ip>"
REDIS_PORT=6379
REDIS_PASSWORD="<password>"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment