Кеширование скомпилированных спецификаций в Vega и Vega-Lite опирается на разделение двух стадий работы: декларативного описания визуализации и её императивного выполнения внутри рантайма. Компиляция Vega-Lite-спецификации в Vega-спецификацию — одна из самых затратных операций в цепочке рендеринга, и именно она становится первым кандидатом на кэширование при построении интерактивных дашбордов и высоконагруженных интерфейсов визуализации.
Vega-Lite описывает визуализацию на уровне абстракций: шкалы, каналы, трансформации данных и интерактивные параметры. При выполнении такая спецификация проходит стадию трансляции в низкоуровневую модель Vega, где каждая визуальная примитивная операция превращается в граф вычислений.
Ключевые этапы:
Наиболее дорогими оказываются:
transform, facet, layer)Именно эти стадии дают основную мотивацию к кэшированию.
Кеширование компиляции основано на предположении, что одна и та же входная спецификация всегда порождает идентичную выходную Vega-спецификацию.
Формально:
compile(spec) -> vegaSpec
cache[hash(spec)] = vegaSpec
При повторном вызове:
if cache.has(hash(spec)):
return cache.get(hash(spec))
else:
compile and store
Однако в реальных системах это предположение требует уточнений:
params в Vega-Lite)Поэтому простое структурное сравнение недостаточно.
Основой кэширования выступает детерминированное хеширование JSON-структуры.
Перед вычислением ключа выполняется нормализация:
description,
title при включённой оптимизации)expr) в строковый канонический
форматПример нормализации:
function normalizeSpec(spec) {
if (Array.isArray(spec)) {
return spec.map(normalizeSpec);
}
if (spec && typeof spec === "object") {
const sortedKeys = Object.keys(spec).sort();
const result = {};
for (const k of sortedKeys) {
if (k === "description") continue;
result[k] = normalizeSpec(spec[k]);
}
return result;
}
return spec;
}
После нормализации применяется хеш-функция:
import crypto fr om "crypto";
function hashSpec(spec) {
const normalized = normalizeSpec(spec);
return crypto
.createHash("sha256")
.update(JSON.stringify(normalized))
.digest("hex");
}
Наиболее распространённый подход:
const compileCache = new Map();
function compileVegaLite(spec) {
const key = hashSpec(spec);
if (compileCache.has(key)) {
return compileCache.get(key);
}
const compiled = compileToVega(spec);
compileCache.set(key, compiled);
return compiled;
}
Ограничение подхода — неуправляемый рост памяти.
Использование LRU (Least Recently Used) структуры:
class LRUCache {
constructor(lim it = 100) {
this.limit = limit;
this.map = new Map();
}
get(key) {
if (!this.map.has(key)) return null;
const value = this.map.get(key);
this.map.delete(key);
this.map.set(key, value);
return value;
}
set(key, value) {
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);
}
}
}
Использование:
const compileCache = new LRUCache(50);
function compileVegaLite(spec) {
const key = hashSpec(spec);
const cached = compileCache.get(key);
if (cached) return cached;
const compiled = compileToVega(spec);
compileCache.set(key, compiled);
return compiled;
}
Внутри Vega важным объектом является View, который
представляет исполняемую визуализацию.
Компиляция Vega-Lite → Vega → View может быть разделена:
Разделение позволяет повторно использовать уже скомпилированные графы данных при изменении только данных.
Одно из ключевых преимуществ архитектуры заключается в том, что изменение данных не требует повторной компиляции спецификации.
const vegaSpec = compileCache.get(specHash);
const view = new vega.View(vega.parse(vegaSpec))
.renderer("canvas")
.initialize("#vis");
view.change("table", vega.changeset().remove(() => true).insert(newData));
view.run();
В этом сценарии кэшируется именно vegaSpec, а не
результат рендера.
Отдельный уровень оптимизации связан с повторным использованием вычисленных трансформаций:
filteraggregatejoinaggregatewindowcalculateПри идентичной комбинации трансформаций и одинаковом входном наборе данных возможно кэширование промежуточных результатов.
const transformCache = new Map();
function applyTransforms(data, transforms) {
const key = hashSpec({ data, transforms });
if (transformCache.has(key)) {
return transformCache.get(key);
}
const result = runTransforms(data, transforms);
transformCache.set(key, result);
return result;
}
Vega использует систему сигналов (signals) для реактивных вычислений.
Сигналы могут быть компилированы в выражения, и эти выражения также поддаются кэшированию:
Пример:
const exprCache = new Map();
function compileEx * pression(expr) {
if (exprCache.has(expr)) return exprCache.get(expr);
const compiled = vega.expressionFunction(expr);
exprCache.set(expr, compiled);
return compiled;
}
Vega-Lite поддерживает params, что приводит к частично
стабильным спецификациям.
Проблема: два объекта с одинаковой структурой, но разными параметрами могут иметь различную семантику.
Решение — разделение ключа кэша:
cacheKey = hash(staticSpec) + hash(params)
Пример:
function compile(spec, params) {
const key = hashSpec(spec) + "::" + hashSpec(params);
if (compileCache.has(key)) return compileCache.get(key);
const compiled = compileToVega(spec, params);
compileCache.set(key, compiled);
return compiled;
}
Внутри Vega runtime граф данных представляет собой DAG (Directed Acyclic Graph). Возможна стратегия:
Особенно эффективно при использовании:
layer) визуализацийfacet)scale reuse)В серверных сценариях применяется сериализация кэша:
import fs from "fs";
function saveCache(cache) {
fs.writeFileSync("cache.json", JSON.stringify([...cache]));
}
function loadCache() {
const data = JSON.parse(fs.readFileSync("cache.json"));
return new Map(data);
}
Кэширование компиляции сталкивается с рядом проблем:
time(),
now())Для контроля вводятся версии ключей:
cacheKey = hash(spec) + version(vega) + version(vegaLite)
Наиболее эффективная стратегия — многоуровневое кэширование:
Такая архитектура позволяет минимизировать повторные вычисления даже при частичной модификации визуализации.
Обобщённая схема:
Vega-Lite spec
↓ (cache A)
normalized spec hash
↓
Vega spec
↓ (cache B)
parsed runtime graph
↓ (cache C)
view instance
Каждый слой имеет собственную стратегию invalidation, зависящую от типа изменений: структура, данные или интерактивные параметры.