Минимизация количества экземпляров

При работе с крупными интерфейсами часто возникает ситуация, когда форматирование подключается к каждому полю отдельно. В небольших формах это не создаёт проблем, однако в административных панелях, CRM-системах, интернет-магазинах и SPA-приложениях количество инстансов быстро растёт.

Типичный пример:

<input class="phone">
<input class="phone">
<input class="phone">
<input class="phone">
document.querySelectorAll('.phone').forEach(input => {
    new Cleave(input, {
        phone: true,
        phoneRegionCode: 'US'
    });
});

Каждый вызов new Cleave() создаёт:

  • собственный набор обработчиков событий;
  • внутреннее состояние;
  • буферы форматирования;
  • ссылки на DOM;
  • вычислительные операции при каждом вводе.

При наличии десятков или сотен элементов это начинает влиять на:

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

Архитектура экземпляра Cleave.js

Каждый экземпляр библиотеки содержит:

const cleave = new Cleave(input, {
    numeral: true
});

Внутри создаются:

  • ссылки на DOM-элемент;
  • конфигурационный объект;
  • обработчики input;
  • обработчики keydown;
  • обработчики focus;
  • обработчики cut;
  • обработчики copy;
  • форматирующие функции;
  • кешированные значения;
  • внутренние utility-модули.

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


Повторное использование экземпляров

Повторная инициализация вместо создания новых объектов

Одна из распространённых ошибок:

function activateMask(input) {
    return new Cleave(input, {
        creditCard: true
    });
}

Если функция вызывается многократно для одного и того же поля:

activateMask(input);
activateMask(input);
activateMask(input);

то создаются новые обработчики поверх старых.

Это приводит к:

  • двойному форматированию;
  • утечкам памяти;
  • деградации производительности;
  • множественным вызовам callback-функций.

Хранение экземпляров

Гораздо эффективнее сохранять инстансы:

const instances = new Map();

function getCleave(input) {
    if (instances.has(input)) {
        return instances.get(input);
    }

    const instance = new Cleave(input, {
        creditCard: true
    });

    instances.set(input, instance);

    return instance;
}

Теперь для каждого поля существует только один объект.


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

WeakMap особенно полезен для DOM-элементов.

const instances = new WeakMap();

function init(input) {
    if (!instances.has(input)) {
        instances.set(input, new Cleave(input, {
            numeral: true
        }));
    }

    return instances.get(input);
}

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

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

Ленивая инициализация

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

Необязательно создавать экземпляры сразу после загрузки страницы.

Плохой вариант:

document.querySelectorAll('input').forEach(input => {
    new Cleave(input, options);
});

Если на странице 500 полей, то 500 экземпляров будут созданы мгновенно.


Инициализация при focus

Более эффективный подход:

const initialized = new WeakSet();

document.addEventListener('focusin', event => {
    const input = event.target;

    if (!input.matches('.masked')) {
        return;
    }

    if (initialized.has(input)) {
        return;
    }

    new Cleave(input, {
        phone: true,
        phoneRegionCode: 'US'
    });

    initialized.add(input);
});

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

  • инициализируются только реально используемые поля;
  • уменьшается стартовая нагрузка;
  • ускоряется initial render.

Использование делегирования

Проблема массового создания обработчиков

Каждый экземпляр Cleave.js добавляет собственные listeners.

Если таких объектов много:

new Cleave(input1);
new Cleave(input2);
new Cleave(input3);

число обработчиков быстро растёт.


Делегирование через контейнер

Часть логики можно перенести на родительский контейнер:

form.addEventListener('focusin', event => {
    const input = event.target;

    if (!input.dataset.cleave) {
        return;
    }

    if (!input.cleaveInstance) {
        input.cleaveInstance = new Cleave(input, {
            numeral: true
        });
    }
});

Создание происходит только при необходимости.


Уничтожение неиспользуемых экземпляров

Проблема SPA-приложений

В React, Vue и Angular поля постоянно удаляются и создаются заново.

Если экземпляры Cleave.js не уничтожаются:

new Cleave(input);

то:

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

Использование destroy()

const cleave = new Cleave(input, options);

cleave.destroy();

Метод удаляет:

  • listeners;
  • внутренние ссылки;
  • служебные данные.

Очистка при размонтировании компонентов

React

useEffect(() => {
    const cleave = new Cleave(ref.current, {
        numeral: true
    });

    return () => {
        cleave.destroy();
    };
}, []);

Vue

mounted() {
    this.cleave = new Cleave(this.$refs.input, {
        numeral: true
    });
},

beforeUnmount() {
    this.cleave.destroy();
}

Angular

ngOnInit() {
    this.cleave = new Cleave(this.input.nativeElement, {
        numeral: true
    });
}

ngOnDestroy() {
    this.cleave.destroy();
}

Пул экземпляров

Концепция object pooling

При интенсивной работе с динамическими формами можно переиспользовать уже созданные объекты.

Например:

class CleavePool {
    constructor() {
        this.pool = [];
    }

    acquire(input, options) {
        let instance = this.pool.pop();

        if (!instance) {
            instance = new Cleave(input, options);
        }

        return instance;
    }

    release(instance) {
        instance.destroy();
        this.pool.push(instance);
    }
}

Подобный подход полезен:

  • в виртуализированных таблицах;
  • при бесконечном скролле;
  • в динамических grid-интерфейсах.

Минимизация количества активных полей

Ограничение области инициализации

Ошибка:

document.querySelectorAll('input')

Такой селектор захватывает:

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

Точная фильтрация

Лучше:

document.querySelectorAll('[data-cleave]')

или:

form.querySelectorAll('.currency-input')

Виртуализация списков

Проблема таблиц

В таблице на 1000 строк:

<input class="price">

создание 1000 экземпляров становится тяжёлой операцией.


Виртуальный рендеринг

Современные подходы:

  • React Window;
  • React Virtualized;
  • Vue Virtual Scroll List;
  • Angular CDK Virtual Scroll.

В DOM присутствуют только видимые элементы.

Следовательно:

  • уменьшается число экземпляров Cleave.js;
  • снижается нагрузка на CPU;
  • сокращается потребление памяти.

Избежание повторной инициализации

Частая ошибка

function render() {
    document.querySelectorAll('.phone').forEach(input => {
        new Cleave(input, options);
    });
}

Если render() вызывается повторно:

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

Использование флага

document.querySelectorAll('.phone').forEach(input => {
    if (input.dataset.initialized) {
        return;
    }

    new Cleave(input, options);

    input.dataset.initialized = 'true';
});

Централизованный менеджер экземпляров

Организация через registry

class CleaveManager {
    constructor() {
        this.instances = new WeakMap();
    }

    init(input, options) {
        if (this.instances.has(input)) {
            return this.instances.get(input);
        }

        const instance = new Cleave(input, options);

        this.instances.set(input, instance);

        return instance;
    }

    destroy(input) {
        const instance = this.instances.get(input);

        if (!instance) {
            return;
        }

        instance.destroy();

        this.instances.delete(input);
    }
}

Снижение нагрузки callback-функций

Избыточные callbacks

new Cleave(input, {
    onValueChanged(event) {
        expensiveOperation(event.target.value);
    }
});

Если экземпляров много:

  • callbacks вызываются одновременно;
  • нагрузка возрастает лавинообразно.

Debounce

function debounce(fn, delay) {
    let timeout;

    return (...args) => {
        clearTimeout(timeout);

        timeout = setTimeout(() => {
            fn(...args);
        }, delay);
    };
}

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

new Cleave(input, {
    onValueChanged: debounce(event => {
        save(event.target.value);
    }, 300)
});

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

Автоматический контроль жизненного цикла

const observer = new MutationObserver(mutations => {
    mutations.forEach(mutation => {
        mutation.removedNodes.forEach(node => {
            if (node.cleaveInstance) {
                node.cleaveInstance.destroy();
            }
        });
    });
});

Оптимизация в React

Ошибка повторного рендера

function Input() {
    const ref = useRef();

    new Cleave(ref.current, {
        numeral: true
    });

    return <input ref={ref} />;
}

Экземпляр создаётся при каждом render-cycle.


Правильный подход

function Input() {
    const ref = useRef();

    useEffect(() => {
        const cleave = new Cleave(ref.current, {
            numeral: true
        });

        return () => cleave.destroy();
    }, []);

    return <input ref={ref} />;
}

Минимизация конфигураций

Избежание создания новых объектов options

Плохо:

new Cleave(input, {
    numeral: true,
    numeralThousandsGroupStyle: 'thousand'
});

внутри больших циклов.


Переиспользуемая конфигурация

const numeralOptions = {
    numeral: true,
    numeralThousandsGroupStyle: 'thousand'
};

inputs.forEach(input => {
    new Cleave(input, numeralOptions);
});

Batch-инициализация

Разделение нагрузки

Вместо:

inputs.forEach(input => {
    new Cleave(input, options);
});

можно использовать постепенную инициализацию:

function batchInit(inputs, size = 20) {
    let index = 0;

    function process() {
        const batch = inputs.slice(index, index + size);

        batch.forEach(input => {
            new Cleave(input, options);
        });

        index += size;

        if (index < inputs.length) {
            requestAnimationFrame(process);
        }
    }

    process();
}

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

  • браузер не блокируется;
  • интерфейс остаётся отзывчивым;
  • уменьшаются длинные frame-паузы.

Сокращение количества форматируемых полей

Не каждое поле требует Cleave.js.

Частая ошибка:

new Cleave(input, {
    uppercase: true
});

Для простых задач выгоднее использовать нативные возможности:

text-transform: uppercase;

или:

<input type="number">

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


Мониторинг количества экземпляров

Диагностика

Полезно отслеживать число активных объектов:

console.log(manager.instances);

или:

window.__cleaveCount++;

Анализ утечек

Признаки проблем:

  • рост памяти после переходов между страницами;
  • увеличение количества listeners;
  • постепенное замедление ввода;
  • рост времени GC;
  • подвисания интерфейса.

Профилирование производительности

Chrome DevTools

Особенно важны:

  • Performance;
  • Memory;
  • Event Listeners;
  • Heap Snapshot.

Что искать

Основные признаки переизбытка экземпляров:

  • тысячи listeners;
  • detached DOM nodes;
  • рост retained size;
  • множественные объекты Cleave;
  • повторные callbacks.

Стратегия минимизации экземпляров

Наиболее эффективная архитектура обычно включает:

  1. Ленивую инициализацию.
  2. WeakMap для хранения.
  3. destroy() при удалении.
  4. Виртуализацию длинных списков.
  5. Централизованный registry.
  6. Исключение повторной инициализации.
  7. Ограничение области применения.
  8. Переиспользование конфигураций.
  9. Batch-инициализацию.
  10. Делегирование событий.