Caching Architecture
The Charles Schwab NextJS Web Monorepo implements a comprehensive multi-layer caching strategy designed to optimize performance, reduce server load, and improve user experience. This document details the caching mechanisms, strategies, and tools used throughout the application ecosystem.
Overview
The caching architecture employs multiple layers of caching strategies across different components of the system:
Next.js Caching Configuration
App Router Cache Settings
The primary caching configuration is managed in Next.js configuration files:
const nextConfig = {
reactStrictMode: true,
experimental: {
useCache: true, // Enables Next.js experimental caching
},
// Additional cache-related configurations...
};
Page-Level Caching
Individual pages can specify their own revalidation intervals:
// Next.js will invalidate the cache when a
// request comes in, at most once every 30 minutes (1800 seconds)
export const revalidate = 1800;
export const dynamicParams = true;
export const dynamic = 'force-static';
Cache Revalidation System
Revalidation API Architecture
The monorepo includes a sophisticated cache revalidation system that allows for targeted cache invalidation:
export async function GET(request: NextRequest): Promise<Response> {
const path = request.nextUrl.searchParams.get('path');
const tags = request.nextUrl.searchParams.get('tags');
const type = request.nextUrl.searchParams.get('type');
const source = request.nextUrl.searchParams.get('source') ?? 'education';
try {
let results: TRevalidateResults | TRevalidateResults[] | unknown;
const ESource = convertToPascal(source);
if (type === 'redirects') {
revalidateCacheByTag(`redirects_${EDrupalSource[ESource].toLowerCase()}`);
results = await syncRedirectsToEdge(EDrupalSource[ESource]);
}
if (tags && tags.length > 0) {
results = revalidateCacheByTag(tags, EDrupalSource[ESource]);
}
if (path && path.length > 0) {
results = [] as TRevalidateResults[];
const flagPermutations = await generatePermutations(allFlags);
flagPermutations.forEach((code) => {
(results as TRevalidateResults[]).push(revalidateCacheByPath(`/${code}${path}`));
});
}
return Response.json({
results,
now: Date.now(),
});
} catch (error) {
return Response.json({ error: error.message }, { status: 500 });
}
}
Tag-Based Cache Invalidation
The system implements tag-based cache invalidation for precise control over cache purging:
export function revalidateCacheByTag(tag: string | string[] | null, source?: EDrupalSource) {
let tags: string[];
if (isArrayWithData(tag)) {
tags = tag as string[];
} else {
tags = (tag as string).split(',');
}
tags.forEach((tagItem) => {
const tagName = !source || tagItem.includes(source) ? tagItem : `${source}_${tagItem}`;
revalidateTag(tagName);
});
return { status: 'revalidatedTags', tags };
}
Path-Based Cache Invalidation
Path-specific cache invalidation for targeted content updates:
export function revalidateCacheByPath(path: string | null): TRevalidateResults {
if (path && path.length > 0) {
revalidatePath(path);
return { status: 'revalidatedPath', path };
}
return { status: 'invalidRequest', path: 'no-path-provided' };
}
Cache Tags Implementation
Component-Level Cache Tags
Components implement cache tags for granular cache management:
const cacheTagList = [
`branches_${zipcode}`,
`branches_location_${city}_${state}`,
'branches_search_results',
'branches_global'
];
const response = await getBranchesFetch(queryParams, cacheTagList);
Content Management System Integration
Cache tags are integrated with Drupal CMS for content-driven invalidation:
{
"cacheTags": "paragraph:33326,paragraph:33331,media:12051,media:12056,media:12061,taxonomy_term:466,taxonomy_term:906,taxonomy_term:331,paragraph:33336,taxonomy_term:791,taxonomy_term:1151,taxonomy_term:491,taxonomy_term:511,taxonomy_term:1131,taxonomy_term:901,taxonomy_term:826,taxonomy_term:671,taxonomy_term:856,paragraph:986,media:8651,node:2966"
}
Edge Config Cache
Vercel Edge Config Integration
The system leverages Vercel Edge Config for distributed configuration caching:
EDGE_CONFIG_SCHWAB_DATA_CACHE="https://edge-config.vercel.com/ecfg_kmiaazuadspy4hyjx4mmeam7vbge?token=7de67818-04d7-45b7-b06d-dd7d162a084c"
Redirect Cache Synchronization
Edge config is used to manage redirect caching and synchronization:
export async function syncRedirectsToEdge(source: EDrupalSource.Education) {
try {
const ESource: EDrupalSource = EDrupalSource[source.toUpperCase()];
const redirectsFromDrupal = await getRedirectsFromDrupal(ESource);
const redirectsOnEdge = await getAllRedirectsFromEdge();
const edgeConfigPostData = redirectsFromDrupal.data ? redirectsFromDrupal.data : [];
// Search through all current redirects and identify which ones should be deleted
Object.entries(redirectsOnEdge).forEach(([key, value]) => {
const matchedRecord = edgeConfigPostData.find((record) => record.key === key);
if (matchedRecord === undefined && value.source === source) {
edgeConfigPostData.push({
key,
operation: 'delete',
});
}
});
if (redirectsFromDrupal.data) {
const postResult = await postRedirectsToEdge(redirectsFromDrupal.data);
return { postResult, postedData: redirectsFromDrupal };
}
return { error: 'No redirects returned from Drupal API.' };
} catch (error) {
SchLogger.error(`syncRedirectsToEdge Error: ${error.message}`);
}
}
API Route Caching
HTTP Cache Headers
API routes implement sophisticated HTTP caching headers:
// Error response - no caching
return new Response(JSON.stringify(responseError), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
Pragma: 'no-cache',
Expires: '0',
},
});
// Success response - aggressive caching with stale-while-revalidate
return new Response(JSON.stringify(response), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300, stale-while-revalidate=150',
},
});
Rate Data Caching
Financial data APIs implement specific caching strategies:
// Error response - no caching
return new Response(JSON.stringify(responseError), {
status: 500,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'no-store, no-cache, must-revalidate, proxy-revalidate',
Pragma: 'no-cache',
Expires: '0',
},
});
// Success response - 5-minute cache with stale-while-revalidate
return new Response(JSON.stringify(response.data), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Cache-Control': 'public, max-age=300, stale-while-revalidate=150',
},
});
Data Fetching Cache Strategy
Drupal Content Caching
Content fetched from Drupal CMS includes configurable revalidation times:
DRUPAL_FETCH_REVALIDATE_TIME="31536000" # 1 year in seconds
Cache Tag Association
Data fetching functions associate appropriate cache tags:
const cacheTagList = [
`branches_${branchId}`,
'branches_detail',
'branches_global'
];
const branchData = await getBranchesFetch(queryParams, cacheTagList);
Browser Cache Optimization
Back-Forward Cache (bfcache)
The system includes specific optimizations for browser back-forward cache:
{
"testName": "BFCACHE_INTEGRITY_REQUIRE_NOOPENER_ATTRIBUTE",
"description": "Requires that links opened with `window.open` use the `noopener` attribute to eliminate a source of eviction from the browser's Back-Forward Cache.",
"error": "When using `window.open()`, use `rel='noopener'` to avoid breaking bfcache integrity and reduce security risks."
}
Static Asset Caching
Next.js configuration includes static asset cache optimization:
{
source: '/nextassets/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable'
}
]
}
Cache Monitoring and Analytics
Cache Performance Metrics
The system includes monitoring for cache performance through Vercel's Observability dashboard:
Vercel Observability Dashboard
Cache performance is monitored through Vercel's built-in observability tools:
Dashboard Location: https://vercel.com/charles-schwab/www.schwab.com/observability/runtime-cache
Available Metrics:
- Cache Reads: Total number of cache read operations
- Cache Writes: Total number of cache write operations
- Hit Rate: Percentage of successful cache hits (target: >90%)
- On-demand Revalidations: Number of manual cache invalidations
The dashboard provides real-time monitoring with configurable time ranges and detailed performance analytics for optimizing cache strategies.
Cache Health Monitoring
Cache health is monitored through:
- Hit/Miss Ratios: Tracking cache effectiveness
- Revalidation Frequency: Monitoring content freshness
- Edge Cache Distribution: Ensuring global cache coverage
- Performance Impact: Measuring cache contribution to page speed
Cache Invalidation Strategies
Content-Driven Invalidation
// When content is updated in CMS
const contentTags = [
`node_${nodeId}`,
`paragraph_${paragraphId}`,
`media_${mediaId}`,
`taxonomy_term_${termId}`
];
// Invalidate all related cache entries
contentTags.forEach(tag => {
revalidateTag(tag);
});
Time-Based Invalidation
// Short-lived dynamic content (5 minutes)
export const revalidate = 300;
// Medium-lived content (30 minutes)
export const revalidate = 1800;
// Long-lived content (24 hours)
export const revalidate = 86400;
User-Driven Invalidation
// Admin-triggered cache clearing
POST /api/revalidate?type=full&source=education
// Content editor cache refresh
POST /api/revalidate?tags=node_123,paragraph_456&source=education
// Path-specific invalidation
POST /api/revalidate?path=/learn/investing&source=education
Performance Optimizations
Incremental Static Regeneration (ISR)
export const revalidate = 1800; // Revalidate every 30 minutes
export const dynamicParams = true; // Generate pages on-demand
export const dynamic = 'force-static'; // Static generation preferred
Streaming and Suspense
import { Suspense } from 'react';
export default function Page() {
return (
<div>
<Suspense fallback={<Loading />}>
<CachedContent />
</Suspense>
</div>
);
}
Cache Configuration Examples
Development Environment
const nextConfig = {
experimental: {
useCache: true,
},
// Disable cache in development for immediate updates
...(process.env.NODE_ENV === 'development' && {
experimental: {
useCache: false,
},
}),
};
Production Environment
const nextConfig = {
experimental: {
useCache: true,
},
// Aggressive caching in production
headers: async () => [
{
source: '/((?!api/).*)',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, stale-while-revalidate=86400',
},
],
},
],
};
Cache Debugging and Tools
Cache Inspection
# Check cache status
curl -I "https://www.schwab.com/learn/story/example"
# Force cache refresh
curl -X POST "https://www.schwab.com/api/revalidate?path=/learn/story/example"
# Inspect cache tags
curl -H "Cache-Control: no-cache" "https://www.schwab.com/api/content/example"
Development Tools
// Log cache status in development
if (process.env.NODE_ENV === 'development') {
console.log('Cache Tags:', cacheTagList);
console.log('Revalidation Time:', revalidate);
console.log('Dynamic Params:', dynamicParams);
}
Best Practices
Cache Key Design
- Hierarchical Structure: Use hierarchical cache keys for easy bulk invalidation
- Source Prefixing: Include data source in cache keys (
education_,drupal_) - Granular Tags: Use specific tags for precise invalidation control
Cache Timing Strategy
| Content Type | Revalidation Time | Strategy |
|---|---|---|
| Financial Data | 5 minutes (300s) | Short-lived, frequently updated |
| Educational Content | 30 minutes (1800s) | Medium-lived, periodic updates |
| Static Marketing | 24 hours (86400s) | Long-lived, infrequent updates |
| User-Generated | On-demand | Event-driven invalidation |
Error Handling
try {
const cachedData = await fetchWithCache(url, { tags: cacheTagList });
return cachedData;
} catch (error) {
// Fallback to uncached request
SchLogger.error(`Cache fetch failed: ${error.message}`);
return await fetchWithoutCache(url);
}
The caching architecture provides a robust, scalable, and performant foundation for the Charles Schwab digital properties, ensuring optimal user experience while minimizing server load and infrastructure costs.