Мемоизация и кэширование вычислений

Мемоизация — это сохранение результатов вычислений для повторного использования при тех же входных данных. В Mithril она особенно важна из-за частых перерисовок: любой вызов m.redraw() приводит к повторному выполнению функций view, а значит — к повторным вычислениям. При сложных представлениях и производительных расчётах это становится узким местом.

Мемоизация в Mithril не встроена как отдельный механизм, но легко реализуется за счёт функционального стиля, замыканий и жизненного цикла компонентов.


Характер перерисовок и влияние на вычисления

Mithril использует виртуальный DOM и диффинг, но функции view вызываются полностью при каждом обновлении. Даже если DOM не меняется, вычисления внутри view выполняются заново.

Критичные места:

  • агрегация данных (фильтрация, сортировка, группировка);
  • генерация больших структур vnode;
  • вычисление производных состояний из attrs и state.

Поэтому кэширование результатов вычислений должно находиться вне тела view или быть привязано к жизненному циклу компонента.


Простейшая мемоизация через замыкание

Функциональные компоненты позволяют хранить кэш в замыкании:

function createSum() {
    let lastArgs = null;
    let lastResult = null;

    return function(a, b) {
        if (lastArgs && lastArgs[0] === a && lastArgs[1] === b) {
            return lastResult;
        }

        lastArgs = [a, b];
        lastResult = a + b;
        return lastResult;
    };
}

const memoizedSum = createSum();

Такой подход применим для:

  • чистых функций;
  • вычислений, зависящих от ограниченного числа параметров;
  • локальных вычислений внутри компонента.

Мемоизация внутри компонента Mithril

Компонентное состояние (vnode.state) — естественное место для хранения кэша:

const ExpensiveComponent = {
    oninit(vnode) {
        vnode.state.cache = {
            input: null,
            result: null
        };
    },

    view(vnode) {
        const data = vnode.attrs.data;
        const cache = vnode.state.cache;

        if (cache.input !== data) {
            cache.input = data;
            cache.result = heavyComputation(data);
        }

        return m("div", cache.result);
    }
};

Преимущества:

  • кэш живёт столько же, сколько компонент;
  • нет утечек памяти;
  • прозрачная инвалидация при смене attrs.

Использование onbeforeupdate для контроля обновлений

Хук onbeforeupdate позволяет полностью пропустить обновление компонента:

const StaticView = {
    onbeforeupdate(vnode, old) {
        return vnode.attrs.value !== old.attrs.value;
    },

    view(vnode) {
        return m("span", vnode.attrs.value);
    }
};

Если возвращается false, view не вызывается. Это форма структурной мемоизации, полезная для компонентов, зависящих от ограниченного набора параметров.


Кэширование вычислений с использованием WeakMap

Для вычислений, зависящих от объектов, эффективно использовать WeakMap:

const cache = new WeakMap();

function compute(model) {
    if (cache.has(model)) {
        return cache.get(model);
    }

    const result = heavyComputation(model);
    cache.set(model, result);
    return result;
}

Преимущества:

  • автоматическая очистка при сборке мусора;
  • отсутствие ручной инвалидации;
  • безопасность для долгоживущих приложений.

Подходит для:

  • моделей данных;
  • объектов состояния;
  • результатов парсинга или нормализации.

Мемоизация списков и ключи (key)

При работе со списками важно использовать key:

items.map(item =>
    m(ItemView, { key: item.id, item })
);

key обеспечивает:

  • сохранение vnode.state между перерисовками;
  • повторное использование кэша внутри компонента элемента списка;
  • стабильность мемоизации при изменении порядка элементов.

Без key кэш в vnode.state будет теряться.


Кэширование производных данных (селекторы)

Частый шаблон — вычисление производных данных из состояния:

function createSelector(fn) {
    let lastInput;
    let lastOutput;

    return function(input) {
        if (input === lastInput) {
            return lastOutput;
        }
        lastInput = input;
        lastOutput = fn(input);
        return lastOutput;
    };
}

const selectVisibleItems = createSelector(items =>
    items.filter(i => i.visible)
);

Использование в view:

view(vnode) {
    const visible = selectVisibleItems(vnode.attrs.items);
    return m("ul", visible.map(...));
}

Этот подход:

  • минимизирует повторные вычисления;
  • сохраняет декларативность;
  • хорошо сочетается с иммутабельными структурами данных.

Кэширование запросов данных

m.request уже возвращает промисы, но логика кэширования может быть вынесена выше:

const requestCache = new Map();

function fetchUser(id) {
    if (!requestCache.has(id)) {
        requestCache.set(id, m.request({
            method: "GET",
            url: `/api/users/${id}`
        }));
    }
    return requestCache.get(id);
}

Особенности:

  • повторные вызовы используют один и тот же промис;
  • исключается дублирование сетевых запросов;
  • удобно комбинируется с oninit.

Инвалидация кэша и согласованность состояния

Любая мемоизация требует стратегии инвалидции:

  • смена attrs;
  • обновление глобального состояния;
  • события пользователя;
  • серверные обновления.

Типичные способы:

  • сброс кэша в onupdate;
  • сравнение версий данных;
  • иммутабельные обновления с заменой ссылок;
  • ограничение времени жизни (TTL).

Пример:

onupdate(vnode) {
    if (vnode.attrs.version !== vnode.state.version) {
        vnode.state.cache = null;
        vnode.state.version = vnode.attrs.version;
    }
}

Связь мемоизации и архитектуры Mithril

Эффективная мемоизация в Mithril опирается на следующие принципы:

  • чистые функции для вычислений;
  • явное управление состоянием через vnode.state;
  • иммутабельные данные для надёжного сравнения;
  • ключи компонентов для сохранения локального кэша;
  • осознанный контроль обновлений через хуки.

Мемоизация не заменяет правильную архитектуру, но усиливает её, позволяя сохранять производительность даже при сложных и насыщенных интерфейсах.