DEV Community

Ahmet İlhan
Ahmet İlhan

Posted on

Function memoizer for every framework

memofy 🚀 - Lightweight Memoization for JavaScript Functions (1.3KB)

Cache mechanism(memoizer) for functions executed with the same parameters (Only 1.3 KB)

This project provides a memoize function for improving performance in JavaScript or TypeScript projects by caching the results of expensive function calls. By memoizing, repeated calls with the same arguments will return the cached result, speeding up execution.

This module works like react's useMemo hook but NOT required react. You can use any framework or pure javascript projects

NPM NPM MIT License

Features

  • Function Memoization: Caches results of function calls with the same arguments.
  • Dependency Tracking: Updates the cache if dependencies change.
  • Flexibility: Usable in both JavaScript and TypeScript projects.
  • The best solution for CPU-intensive operations or complex calculations
  • The disconnected functions are deleted from memory. The caches belonging to this function are also deleted.
  • WeakMap based cache store
  • WeakMap Disconnects methods that cannot communicate with weak reference links and triggers the garbage collector to kick in

Why should we use memofy?

Using Memofy you can reduce the execution time of your functions by up to 1500 times. The following results were obtained by testing on a heavy function. 💪🏼

Test Case Function Execute Time (ms)
With Memofy (CACHED) 0.083 ms
Without Memofy 57.571 ms

AND very easy

import memofy from "memofy";

const dep1 = /** Variable to track change */

const heavyMethod = memofy((arg1, arg2, ...args) => {
 // calculate something then return
}, [dep1, dep2, ...deps]);

// heavyMethod looks at the deps and arguments every time it is called.
// If there is no change, it brings it from the cache. If there is a change, it runs the function again
Enter fullscreen mode Exit fullscreen mode

Installation

NPM
npm install memofy
Enter fullscreen mode Exit fullscreen mode
YARN
yarn add memofy
Enter fullscreen mode Exit fullscreen mode

Usage case

Without deps parameters

In the following process, when the concatPhoneNumber method is called again with the same parameters, the function is not executed again, it fetches the result from the cache.

import memofy from "memofy";

const concatPhoneNumber = (extension, number) => {
  // Heavy calculation
  // return result
};

const memoizedConcatPhoneNumber = memofy(concatPhoneNumber, []);

memoizedConcatPhoneNumber(90, 555); // Runs concatPhoneNumber once
memoizedConcatPhoneNumber(90, 555); // Don't run because fetched from cache (same parameter)
memoizedConcatPhoneNumber(90, 552); // Runs concatPhoneNumber because params is changed
Enter fullscreen mode Exit fullscreen mode

With deps parameter

If you want the method to run again with the same parameter according to some dependencies, you can pass the deps parameter as follows.

import memofy from "memofy";

const product = { title: "Test product", price: 10 };

const calculateTax = (taxRatio) => {
  // Calculate tax by product price
  // Heavy calculation
  return taxRatio * product.price;
};

const memoizedCalculateTax = memofy(calculateTax, [product]);

calculateTax(2); // Runs calculateTax when first run -> 20
calculateTax(2); // // Don't run because fetched from cache (same parameter and same deps) -> 20

product.price = 40;
calculateTax(3); // Runs calculateTax because product dep changed -> 120
Enter fullscreen mode Exit fullscreen mode
import memofy from "memofy";

const products = [
  /**Let's say there are more than 100 products */
];

// It is costly to cycle through 100 products each time. Just keep the result in the cache when it runs once.
const getTotalPrice = (fixPrice) => {
  return products.reduce((acc, curr) => acc + curr.price, 0);
};

const _getTotalPrice = memofy(getTotalPrice, [products]);
getTotalPrice(0); // Runs getTotalPrice once
getTotalPrice(0); // Don't run because fetched from cache
products.push({
  /** a few products */
});
getTotalPrice(2); // Runs again getTotalPrice because products and parameter changed
Enter fullscreen mode Exit fullscreen mode

With context

If context(this, globalContex e.g) is used in the function you want to cache, you can pass the context parameter.

import memofy from "memofy";

this.user.name = "Jack"; // For example inject name to context

const getName = (suffix) => {
  return `${suffix} ${this.user.name}`;
};
const memoizedGetName = memofy(getName, [], this);
memoizedGetName("Mr"); // Result is Mr Jack

this.user.name = "John";
memoizedGetName("Mr"); // Result is Mr John because context data changed
Enter fullscreen mode Exit fullscreen mode

Declaration for typescript

type Args = Array<any>;

type Deps = Readonly<Array<any>>;

type MemoizedFunction<A extends Args, ReturnType> = (...args: A) => ReturnType;

declare function memofy<A extends Args, ReturnType extends any>(
  _functionToMemoize: (...args: Array<unknown>) => ReturnType,
  _deps?: Deps,
  _context?: unknown
): MemoizedFunction<A, ReturnType>;
Enter fullscreen mode Exit fullscreen mode

Performance result

Performance results on a complex function that distinguishes prime numbers. Performance Test

Case ms
First execute time (no caching) > 52.08 ms
Second execute time (caching) < 0.03 ms
and subsequent execution (caching) < 0.03 ms

Test coverage result

Tests were written for all cases and all parameter types. Tests

File % Stmts % Branch % Funcs % Lines
All files 90.69 86.95 100 94.59
lib 88.88 92.3 100 87.5
index.ts 88.88 92.3 100 87.5
lib/store 92 80 100 100
DependencyCacheStore.ts.ts 90 75 100 100
FunctionCacheStore.ts 93.33 83.33 100 100

Contributing

This is open source software. If you want, you can support it by becoming a contributor. Github Repository

Top comments (1)

Collapse
 
peterpark88 profile image
PeterPark88

Nice Article.