При активном использовании Zod в реальных приложениях часто возникает ситуация, когда схемы создаются многократно в процессе выполнения программы. Особенно это заметно в следующих случаях:
Каждое создание схемы в Zod — это выполнение цепочки конструкторов, валидационных описаний и внутренних структур. Хотя сам Zod оптимизирован, повторная генерация идентичных схем приводит к лишним затратам памяти и CPU, а также ухудшает предсказуемость поведения в системах с высокой нагрузкой.
Кэширование схем решает две основные задачи:
Схемы Zod являются объектами. Это означает, что даже при одинаковой структуре две схемы не равны по ссылке:
const a = z.object({ name: z.string() });
const b = z.object({ name: z.string() });
a === b; // false
Это критично в следующих сценариях:
Кэширование позволяет гарантировать, что одинаковая логическая схема всегда имеет одну и ту же ссылку.
Наиболее простой и часто используемый подход — вынесение схем в область модуля:
import { z } fr om "zod";
export const userSchema = z.object({
id: z.string(),
email: z.string().email(),
});
Такой подход обеспечивает:
Особенно эффективно это работает в средах с кешированием модулей (Node.js, большинство bundler-ов).
Сложности начинаются при необходимости создавать схемы параметрически:
function createUserSchema(minPasswordLength: number) {
return z.object({
password: z.string().min(minPasswordLength),
});
}
Каждый вызов функции создаёт новую схему, даже если параметр одинаковый.
Решение — использование кэша на основе параметров:
const schemaCache = new Map<number, ReturnType<typeof createUserSchema>>();
function getUserSchema(minPasswordLength: number) {
if (schemaCache.has(minPasswordLength)) {
return schemaCache.get(minPasswordLength)!;
}
const schema = z.object({
password: z.string().min(minPasswordLength),
});
schemaCache.set(minPasswordLength, schema);
return schema;
}
Такой подход превращает генерацию схемы в операцию с амортизированной стоимостью O(1) после первого вызова.
В реальных системах параметры часто многокомпонентные:
В таких случаях ключ кэша должен быть сериализуемым:
type SchemaKey = {
role: string;
strict: boolean;
};
const cache = new Map<string, z.ZodSchema>();
function getSchema(key: SchemaKey) {
const cacheKey = JSON.stringify(key);
const cached = cache.get(cacheKey);
if (cached) return cached;
let schema = z.object({
id: z.string(),
});
if (key.strict) {
schema = schema.strict();
}
cache.set(cacheKey, schema);
return schema;
}
Использование JSON-сериализации допустимо при стабильной структуре ключей, однако требует контроля порядка полей.
При необходимости связывать схемы с объектами конфигурации, полезен WeakMap:
const cache = new WeakMap<object, z.ZodSchema>();
function getSchema(config: object) {
const cached = cache.get(config);
if (cached) return cached;
const schema = z.object({
value: z.string(),
});
cache.set(config, schema);
return schema;
}
Преимущества:
При построении больших систем часто используются фабрики схем:
function createApiSchema(version: string) {
return z.object({
version: z.literal(version),
payload: z.any(),
});
}
Без кэширования каждая версия создаёт новую структуру.
Оптимизированный вариант:
const apiSchemaCache = new Map<string, z.ZodSchema>();
function getApiSchema(version: string) {
if (apiSchemaCache.has(version)) {
return apiSchemaCache.get(version)!;
}
const schema = z.object({
version: z.literal(version),
payload: z.any(),
});
apiSchemaCache.set(version, schema);
return schema;
}
В средах вроде serverless или edge runtime схемы могут пересоздаваться при каждом cold start. В таких условиях кэширование:
Однако при некорректной реализации возможно:
При использовании рекурсивных структур через z.lazy
важно учитывать, что ленивые схемы уже частично решают проблему
повторного создания:
const nodeSchema = z.lazy(() =>
z.object({
value: z.string(),
children: z.array(nodeSchema),
})
);
Кэширование в таких случаях чаще применяется не к самой схеме, а к фабрике, создающей корневую структуру.
На практике встречаются типовые проблемы:
1. Кэширование через объектные ключи без стабилизации
cache.set({ a: 1 }, schema); // всегда новый ключ
2. Утечки памяти через глобальные Map
При накоплении уникальных параметров кэш становится неограниченным.
3. Смешивание схем разных контекстов
Например, объединение схем API v1 и v2 в одном кэше без разделения пространства имён.
Для долгоживущих процессов применяются ограничения:
Пример LRU-структуры:
class LRUCache<K, V> {
private map = new Map<K, V>();
constructor(private lim it: number) {}
get(key: K): V | undefined {
const value = this.map.get(key);
if (!value) return undefined;
this.map.delete(key);
this.map.set(key, value);
return value;
}
set(key: K, value: V) {
if (this.map.has(key)) {
this.map.delete(key);
}
this.map.set(key, value);
if (this.map.size > this.limit) {
const firstKey = this.map.keys().next().value;
this.map.delete(firstKey);
}
}
}
TypeScript-типизация Zod схем не зависит от кэширования, поскольку типы выводятся на этапе компиляции:
const schema = z.object({
id: z.string(),
});
type SchemaType = z.infer<typeof schema>;
Кэширование влияет только на runtime-экземпляры, не затрагивая систему типов. Это позволяет безопасно применять memoization без потери типовой строгости.
Универсальный подход — обёртка фабрик через memoization:
function memoize<TArgs extends any[], TReturn>(
fn: (...args: TArgs) => TReturn
) {
const cache = new Map<string, TReturn>();
return (...args: TArgs): TReturn => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key)!;
const result = fn(...args);
cache.set(key, result);
return result;
};
}
Применение:
const getSchema = memoize((minLen: number) =>
z.object({
password: z.string().min(minLen),
})
);