Skip to content

Instantly share code, notes, and snippets.

@markudevelop
Last active January 12, 2021 07:49
Show Gist options
  • Save markudevelop/5428f0eda51ae3ca672d13664f37cbae to your computer and use it in GitHub Desktop.
Save markudevelop/5428f0eda51ae3ca672d13664f37cbae to your computer and use it in GitHub Desktop.
React SSR Express Caching Middleware (Next.js)
import LRU from 'lru-cache';
import { Request, Response } from 'express';
import createEtag from 'etag';
import prettyMs from 'pretty-ms';
import { isPublicRoute } from './not-found-middleware';
import { app } from '../';
import { debuglog } from 'util';
const logger = debuglog('livescore-app-ssr-cache');
// Can be replaced with shared redix/memcache to share cache between servers
const ssrCache = new LRU({
maxAge: 10000,
});
const toSeconds = (ms: number) => Math.floor(ms / 1000);
const createSetHeaders = (
revalidate = (ttl: number) => Math.round(ttl * 0.2)
) => {
return ({ res, createdAt, isHit, ttl, hasForce, etag }: any) => {
const diff = hasForce ? 0 : createdAt + ttl - Date.now();
const maxAge = toSeconds(diff);
const revalidation = revalidate ? toSeconds(revalidate(ttl)) : 0;
let cacheControl = `public, must-revalidate, max-age=${maxAge}, s-maxage=${maxAge}`;
if (revalidation) {
cacheControl = `${cacheControl}, stale-while-revalidate=${revalidation}, stale-if-error=${revalidation}`;
}
res.setHeader('Cache-Control', cacheControl);
res.setHeader('X-Cache-Status', isHit ? 'HIT' : 'MISS');
res.setHeader('X-Cache-Expired-At', prettyMs(diff));
res.setHeader('ETag', etag);
};
};
const SsrCache = async (req: Request, res: Response, next: Function) => {
// TODO Can be replaced by a more reliable way?
const isPublic = isPublicRoute(req.path);
if (!isPublic) {
const cacheKey = req.path;
const cacheResult = ssrCache.has(cacheKey) && ssrCache.get(cacheKey);
if (cacheResult) {
const {
etag: cachedEtag,
createdAt,
data,
ttl = ssrCache.maxAge,
} = cacheResult as any;
const ifNoneMatch = req.headers['if-none-match'];
const isModified = cachedEtag !== ifNoneMatch;
logger(
`SSR Response from cache key: ${cacheKey}, etag: ${cachedEtag}, noneMatch: ${ifNoneMatch}`
);
const setHeaders = createSetHeaders();
setHeaders({
etag: cachedEtag,
res,
createdAt,
isHit: true,
ttl,
} as any);
// Save bandwidth/cpu cycles use etag comparison
if (!isModified) {
res.statusCode = 304;
return res.end();
}
return res.end(data);
} else {
const _resEnd = res.end.bind(res);
res.end = function (payload: any) {
if (res.statusCode === 200) {
const cacheResponse = {
etag: createEtag(payload),
ttl: ssrCache.maxAge,
data: payload,
createdAt: Date.now(),
} as any;
ssrCache.set(cacheKey, cacheResponse);
createSetHeaders()({
...cacheResponse,
isHit: false,
res,
});
}
return _resEnd(payload);
};
logger('SSR rendering without cache and try caching for ', cacheKey);
return await app.renderToHTML(req, res, req.path);
}
}
next();
};
export default SsrCache;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment