Post

Beyond the UI: Understanding Modern Frontend Engineering · Part 8

New to this series? Start with Part 1

Memoization Explained: When Caching Computation Actually Helps

Beyond the UI: Understanding Modern Frontend EngineeringPart 8

Modern frontend applications perform a surprising amount of computation before anything becomes visible on the screen. A product catalogue filters and sorts thousands of items, a financial dashboard aggregates transactions into charts, a data table calculates summaries, and a form continuously evaluates validation rules as the user types.

Most of these operations are perfectly reasonable. The problem begins when an application repeatedly performs the same expensive calculation even though the information used to produce the result has not changed.

Imagine a financial dashboard displaying spending patterns from 100,000 transactions. The application processes those transactions to calculate monthly totals, category distributions, average spending, and other financial indicators. Once those calculations are complete, the results are displayed in charts and summary cards.

Now suppose the user opens the navigation sidebar. The underlying transactions have not changed, and neither have the financial summaries. However, depending on how the application is structured, the interface update might trigger the same calculations again.

The user requested a visual change, but the application repeated a potentially expensive data-processing operation.

This is where memoization becomes useful.

Memoization is an optimization technique that allows an application to remember the result of a computation and reuse it when the same inputs are encountered again. Instead of calculating the same answer repeatedly, the application checks whether it already knows the answer.

The idea sounds simple, but applying it correctly involves several important engineering decisions. We need to understand how cached results are identified, when they remain valid, how much memory they consume, and whether retrieving a cached result is actually cheaper than calculating it again.

There is also an important distinction between memoization and the broader concept of caching. Although both involve reusing previously obtained results, memoization is specifically concerned with avoiding repeated computation.

In this article, we will explore memoization from first principles, examine its relationship with frontend rendering, and look at how different frameworks apply the concept. More importantly, we will examine when memoization genuinely improves performance and when it simply introduces complexity.

Because the goal of performance engineering is not to eliminate every repeated calculation. It is to identify the calculations that are expensive enough to justify remembering their results.


1. What Is Memoization?

Memoization is a technique in which the result of a function is stored and reused when the function receives the same inputs again.

Consider a function that calculates the square of a number.

1
2
3
function square(number) {
  return number * number;
}

Calling the function repeatedly produces predictable results.

1
2
3
square(5); // 25
square(8); // 64
square(5); // 25

Each invocation performs the multiplication, even when the result has already been calculated.

For a simple operation such as multiplication, this is not a problem. The computation is extremely inexpensive, and adding a caching mechanism would probably introduce more work than it saves.

However, the same principle becomes interesting when a function performs expensive operations such as processing large datasets, traversing complex structures, or evaluating computationally intensive algorithms.

Suppose we introduce a cache that remembers previously calculated values.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
                  Function Call
                        |
                        v
                 Input Received
                        |
                        v
                Check Result Cache
                        |
                +-------+-------+
                |               |
             Cache Hit       Cache Miss
                |               |
                v               v
          Return Stored     Execute Function
              Result            |
                                v
                           Store Result
                                |
                                v
                           Return Result

The first time a particular input is encountered, the application performs the calculation and stores the result. Subsequent calls with the same input can retrieve the stored result instead of executing the computation again.

We can implement this using ordinary JavaScript.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function memoize(fn) {
  const cache = new Map();

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

    const result = fn(input);

    cache.set(input, result);

    return result;
  };
}

const memoizedSquare = memoize(square);

console.log(memoizedSquare(5)); // Calculated
console.log(memoizedSquare(5)); // Cached
console.log(memoizedSquare(8)); // Calculated

This implementation is deliberately simple. It supports a single argument and uses that argument directly as the cache key.

A production implementation may need to handle multiple arguments, object identity, cache expiration, memory limits, and other concerns.

Nevertheless, the fundamental mechanism remains the same.

Memoization exchanges memory and cache-management work for the possibility of avoiding repeated computation.

That trade-off is the foundation of everything that follows.


2. Memoization Is Not the Same as Caching

Memoization is often described as a form of caching, and that description is correct. However, the terms are not interchangeable.

Caching is a broader technique that stores previously obtained information so that it can be retrieved more efficiently in the future.

For example, a browser might cache an image to avoid downloading it again. A CDN might cache an API response to reduce requests to the origin server. A database might cache frequently accessed pages to avoid repeated disk reads.

Memoization is more specific. It stores the result of a computation so that the same computation does not need to be repeated for equivalent inputs.

Consider the following examples.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Browser Cache
    |
    +--> Avoid downloading an image again


API Response Cache
    |
    +--> Avoid requesting the same remote data again


Database Cache
    |
    +--> Avoid repeating expensive data retrieval


Memoization
    |
    +--> Avoid recalculating a function result

The distinction becomes clearer when we consider a financial dashboard.

The dashboard retrieves transactions from an API and then calculates a spending summary.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
                Backend API
                     |
                     v
              Transaction Data
                     |
                     v
             Application Cache
                     |
                     v
            Calculate Analytics
                     |
                     v
               Memoized Result
                     |
                     v
                Dashboard UI

An application cache may prevent unnecessary network requests by reusing previously fetched transactions. Memoization may then prevent unnecessary recalculation of the analytics derived from those transactions.

These are two different optimization opportunities.

The first concerns retrieving information from another system. The second concerns processing information that the application already possesses.

This distinction is especially important in modern frontend applications because server-state libraries and memoization mechanisms are frequently used together.

A query cache might manage the freshness and synchronization of remote data, while memoization manages expensive transformations performed on that data.

Understanding which layer is responsible for which problem helps prevent unnecessary complexity.


3. Why Pure Functions Matter

Memoization works best when the function being memoized is pure.

A pure function has two important characteristics. It produces the same result for the same inputs, and executing it does not cause observable side effects outside the function.

Consider a function that calculates a discounted price.

1
2
3
function calculateDiscount(price, percentage) {
  return price - (price * percentage) / 100;
}

If we call:

1
calculateDiscount(1000, 10);

the result will always be:

1
900

Assuming ordinary numeric inputs, the function does not depend on external state, modify shared data, or produce side effects.

This makes it a good candidate for memoization.

Now consider a different function.

1
2
3
function calculateDiscount(price) {
  return price * currentDiscountRate;
}

The result depends on currentDiscountRate, which exists outside the function.

Suppose the current discount rate is 10%.

1
2
3
Price = 1000
Discount = 10%
Result = 900

Later, the discount rate changes to 20%.

1
2
3
Price = 1000
Discount = 20%
Result = 800

The explicit input remains 1000, but the correct result has changed.

If we memoize this function using only price as the cache key, the application may incorrectly return the previous result of 900.

The problem is not that memoization failed mechanically. The problem is that the cache key did not represent every value that determines the result.

A better design makes the dependency explicit.

1
2
3
function calculateDiscount(price, discountRate) {
  return price * (1 - discountRate);
}

Now both inputs participate in determining the result.

1
2
calculateDiscount(1000, 0.1); // 900
calculateDiscount(1000, 0.2); // 800

This illustrates a broader principle.

Memoization is reliable when the cache can identify all the information that determines the result.

Functions that depend on changing global variables, current time, random values, mutable shared objects, or external services require additional care.

For example, memoizing a function that returns the current time would fundamentally change its behavior.

1
2
3
function getCurrentTime() {
  return Date.now();
}

If the result were cached indefinitely, subsequent calls would return an outdated timestamp.

Similarly, memoizing a function that sends a notification or writes to a database could prevent an operation from happening when it is supposed to happen.

Memoization should therefore be applied primarily to computations whose results can be safely reused.


4. How Cache Keys Determine Correctness

Once we decide to cache a calculation, we need a way to identify whether a previously stored result can be reused.

That identifier is the cache key.

For simple primitive inputs, cache keys are relatively straightforward.

1
2
3
4
5
const cache = new Map();

cache.set(5, 25);

console.log(cache.get(5)); // 25

However, real frontend applications frequently work with arrays, objects, and complex data structures.

This introduces a more subtle problem.

Consider two objects.

1
2
3
4
5
6
7
8
9
const first = {
  category: "electronics",
  page: 1
};

const second = {
  category: "electronics",
  page: 1
};

The objects contain identical information, but JavaScript considers them different references.

1
console.log(first === second); // false

If we use these objects as keys in a Map, they identify different entries.

1
2
3
4
5
6
7
8
9
const cache = new Map();

cache.set(first, ["Laptop", "Monitor"]);

console.log(cache.get(first));
// ["Laptop", "Monitor"]

console.log(cache.get(second));
// undefined

Conceptually:

1
2
3
4
5
6
7
8
9
10
11
12
Object A
{ category: "electronics", page: 1 }
                 |
                 v
              Cache A


Object B
{ category: "electronics", page: 1 }
                 |
                 v
              Cache Miss

This behavior is not incorrect. JavaScript is comparing object identity rather than structural equivalence.

However, it means that a memoization strategy based on object references can fail to reuse results when equivalent objects are recreated.

There are several ways to address this, depending on the problem.

One approach is to use primitive values as cache keys. Another is to create a stable representation of the relevant inputs. A third is to preserve object identity when the underlying data has not changed.

For example, a cache key for a product search could be constructed from known primitive fields.

1
2
3
4
5
6
7
8
9
10
11
function createProductCacheKey({
  category,
  page,
  sort
}) {
  return JSON.stringify([
    category,
    page,
    sort
  ]);
}

Using an ordered array avoids ambiguity between the selected fields.

However, serialization is not a universal solution. It introduces computational overhead and requires careful handling of complex values, unsupported types, and data structures where different representations may be semantically equivalent.

The important lesson is that cache-key design affects both correctness and performance.

A cache that cannot recognize equivalent inputs misses opportunities to reuse results.

A cache that incorrectly treats different inputs as equivalent can return the wrong result.

Both problems matter.


5. Referential Equality and Immutable Data

Frontend frameworks frequently use reference identity to determine whether data has changed.

This is one reason immutable data structures and immutable update patterns are so common in modern frontend engineering.

Consider a list of transactions.

1
2
3
4
const transactions = [
  { id: 1, amount: 500 },
  { id: 2, amount: 1200 }
];

Suppose we calculate the total and cache the result using the array reference.

1
const total = calculateTotal(transactions);

Now imagine modifying the array directly.

1
2
3
4
transactions.push({
  id: 3,
  amount: 800
});

The contents have changed, but the array reference remains the same.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
Before Mutation

Reference A
    |
    v
[500, 1200]


After Mutation

Reference A
    |
    v
[500, 1200, 800]

A memoization mechanism that relies solely on reference equality may not detect the change.

It may continue returning the old total.

A more predictable approach is to create a new array.

1
2
3
4
5
6
7
const updatedTransactions = [
  ...transactions,
  {
    id: 4,
    amount: 300
  }
];

Now we have:

1
2
3
4
5
6
7
8
9
10
11
12
Original Array
Reference A
    |
    v
[500, 1200, 800]


Updated Array
Reference B
    |
    v
[500, 1200, 800, 300]

The new reference signals that the collection has changed.

This does not mean immutability automatically makes every application faster. Creating new objects and arrays also has a cost.

However, immutable update patterns make change detection more predictable and allow reference-based optimizations to work reliably.

This is particularly valuable in systems that use reactive dependencies, shallow comparisons, or memoized component boundaries.

The relationship between immutability and memoization is therefore architectural, not merely syntactic.


6. The Economics of Memoization

Memoization is often introduced as a performance improvement, but it is more accurate to describe it as a performance trade-off.

Without memoization, the application pays the cost of computation every time the function executes.

With memoization, the application pays the cost of looking up a cached result. When the result is missing, it also pays the computation cost and the cost of storing the new result.

A simplified cost model is:

\[C_{\text{without}} = N \times C_{\text{compute}}\]

where (N) represents the number of calls and (C_{\text{compute}}) represents the cost of one calculation.

With memoization:

\[C_{\text{with}} = H \times C_{\text{lookup}} + M \times (C_{\text{lookup}} + C_{\text{compute}} + C_{\text{store}})\]

Here, (H) represents cache hits and (M) represents cache misses, with (H + M = N).

This model is simplified because it does not include memory allocation, garbage collection, eviction, or other runtime effects.

Nevertheless, it illustrates the central trade-off.

Suppose an expensive calculation takes 40 milliseconds, and a cache lookup takes 0.05 milliseconds.

If the same input is processed ten times, the difference could be substantial.

ScenarioApproximate total computation and lookup time
No memoization400 ms
Memoization with one miss and nine hits40.5 ms, excluding storage overhead

These numbers are illustrative rather than benchmark results.

Now consider a calculation that takes only 0.001 milliseconds.

If cache lookup takes longer than the calculation itself, memoization may increase execution time.

This is why blindly memoizing simple arithmetic, string concatenation, or trivial comparisons is rarely useful.

The calculation must be expensive enough, repeated often enough, and stable enough for caching to provide a meaningful benefit.


7. Cache Hit Rate Matters

A cache is valuable only when it successfully reuses previous results.

Suppose an application processes 1,000 function calls.

If 900 calls reuse cached results, the cache hit rate is 90%.

1
2
3
4
5
1,000 Calls
    |
    +---- 900 Cache Hits
    |
    +---- 100 Cache Misses

That may provide substantial savings if the underlying calculation is expensive.

But suppose almost every call uses a unique input.

1
2
3
4
5
1,000 Calls
    |
    +---- 20 Cache Hits
    |
    +---- 980 Cache Misses

The application now spends time checking the cache, performing almost every calculation anyway, and storing results that may never be reused.

This is an important consideration for highly dynamic frontend data.

For example, memoizing a search function may be useful if users repeatedly switch between a small set of filters.

It may be less useful if every input is unique and rarely revisited.

The effectiveness of memoization depends on actual usage patterns, not simply on the existence of an expensive function.


8. Cache Lifetime and Memory Pressure

Our earlier memoization implementation used a Map that retained every result indefinitely.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function memoize(fn) {
  const cache = new Map();

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

    const result = fn(input);

    cache.set(input, result);

    return result;
  };
}

This works for small, bounded sets of inputs.

But imagine applying it to a search function that receives thousands of unique queries.

1
2
3
4
5
6
7
"l"
"la"
"lap"
"lapt"
"lapto"
"laptop"
...

Over time, the cache may accumulate results that are never used again.

If those results contain large arrays or objects, memory consumption can grow significantly.

The application may save CPU time while increasing memory pressure and garbage-collection activity.

A more robust cache needs a retention strategy.

One common approach is a bounded least-recently-used cache, usually called an LRU cache.

The principle is straightforward. The cache retains a limited number of entries and removes the least recently used entry when it reaches capacity.

1
2
3
4
5
6
7
8
9
10
11
Cache Capacity: 3

[A] [B] [C]

Access A

[B] [C] [A]

Insert D

[C] [A] [D]

Entry B is removed because it was the least recently used.

A simplified implementation for a single-argument synchronous function might look like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function memoizeLRU(fn, maxEntries = 100) {
  if (!Number.isInteger(maxEntries) || maxEntries < 1) {
    throw new Error("maxEntries must be positive");
  }

  const cache = new Map();

  return function (key) {
    if (cache.has(key)) {
      const value = cache.get(key);

      cache.delete(key);
      cache.set(key, value);

      return value;
    }

    const result = fn(key);

    cache.set(key, result);

    if (cache.size > maxEntries) {
      const oldestKey = cache.keys().next().value;
      cache.delete(oldestKey);
    }

    return result;
  };
}

This prevents the number of cache entries from growing indefinitely.

It does not guarantee a fixed memory footprint, because individual entries can have different sizes. It also assumes that the input key reliably represents the calculation’s dependencies.

Nevertheless, it demonstrates an important principle.

Memoization is not only about deciding what to cache. It is also about deciding how long cached results should remain available.


9. Memoization in Frontend Rendering

So far, we have discussed memoization as a general programming technique.

Now let’s connect it to frontend rendering.

In the previous article, we explored how state changes can trigger component rendering.

Different frameworks use different rendering models, but they all need mechanisms for updating the interface when relevant application data changes.

Consider a dashboard with three sections.

1
2
3
4
5
                 Dashboard
                     |
          +----------+----------+
          |          |          |
       Sidebar    Summary     Chart

Suppose the sidebar changes from closed to open.

The financial summary and chart data remain unchanged.

Ideally, the application should avoid repeating expensive calculations that have nothing to do with the sidebar.

Memoization can help by preserving previously calculated results.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Sidebar State Changes
          |
          v
   UI Update Triggered
          |
          v
  Analytics Inputs Changed?
          |
      +---+---+
      |       |
     Yes      No
      |       |
      v       v
  Recompute  Reuse Cached
  Analytics     Result
      |       |
      +---+---+
          |
          v
     Update Interface

However, the exact mechanism depends on the framework.

Some frameworks re-execute component functions and provide explicit memoization APIs.

Others track reactive dependencies and recalculate derived values only when those dependencies change.

These approaches share a similar goal, but their internal behavior is not identical.

That distinction is worth understanding.


10. How Different Frontend Frameworks Apply Memoization

Memoization is framework-independent, but frontend frameworks expose different mechanisms for applying related optimizations.

Rather than treating these APIs as interchangeable, it is more useful to understand the problem each one solves.

React: Explicit Memoization

React provides several APIs for controlling repeated computation and rendering.

useMemo caches a calculation result between renders when its dependencies remain unchanged.

1
2
3
const summary = useMemo(() => {
  return calculateAnalytics(transactions);
}, [transactions]);

useCallback preserves a function reference when its dependencies remain unchanged.

1
2
3
const handleExport = useCallback(() => {
  exportReport(summary);
}, [summary]);

React.memo allows React to skip eligible component renders when props remain unchanged.

1
2
3
4
5
const AnalyticsChart = React.memo(function AnalyticsChart({
  data
}) {
  return <Chart data={data} />;
});

These mechanisms operate at different levels.

The first concerns calculation results, the second concerns function identity, and the third concerns component rendering.

Modern React also supports compiler-assisted memoization through the React Compiler in compatible configurations, reducing the need for some manual optimizations.

The underlying engineering question remains the same: which work is expensive enough to justify avoiding?

Vue: Cached Computed Properties

Vue provides computed properties that automatically track reactive dependencies.

1
2
3
4
5
6
7
8
9
10
import { ref, computed } from "vue";

const transactions = ref([]);

const total = computed(() => {
  return transactions.value.reduce(
    (sum, transaction) => sum + transaction.amount,
    0
  );
});

The computed value is cached based on its tracked reactive dependencies.

When the underlying transactions change, Vue invalidates the computed value so it can be recalculated when needed.

If unrelated reactive state changes, the computed result does not need to be recalculated.

This is a form of dependency-aware caching.

Unlike a general-purpose memoization function that may retain results for many different argument combinations, a Vue computed property normally maintains the current cached result associated with its reactive dependencies.

The distinction matters because the caching model is different.

Angular: Pure Pipes and Computed Signals

Angular provides pure pipes and computed signals, which can help avoid unnecessary repeated transformations.

A pure pipe can transform data for a template.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { Pipe, PipeTransform } from "@angular/core";

@Pipe({
  name: "transactionTotal",
  pure: true
})
export class TransactionTotalPipe implements PipeTransform {
  transform(
    transactions: { amount: number }[]
  ): number {
    return transactions.reduce(
      (sum, transaction) => sum + transaction.amount,
      0
    );
  }
}

Angular can skip invoking a pure pipe when its input values have not changed according to the framework’s change-detection rules.

Angular signals also provide computed values.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { signal, computed } from "@angular/core";

const transactions = signal([
  { amount: 500 },
  { amount: 1200 }
]);

const total = computed(() =>
  transactions().reduce(
    (sum, transaction) => sum + transaction.amount,
    0
  )
);

The computed signal derives its value from other signals and caches its calculation until the relevant dependencies invalidate it.

As with Vue, this is dependency-driven reactive computation rather than an unrestricted memoization cache for arbitrary argument combinations.

Svelte: Derived Reactive Values

Svelte supports derived reactive values through its reactive system.

In Svelte 5, for example:

1
2
3
4
5
6
7
8
9
10
11
let transactions = $state([
  { amount: 500 },
  { amount: 1200 }
]);

let total = $derived(
  transactions.reduce(
    (sum, transaction) => sum + transaction.amount,
    0
  )
);

The derived value depends on the reactive transactions.

When the relevant state changes, Svelte can invalidate and update the derived calculation.

This reduces the need to manually synchronize calculated values with their underlying inputs.

As with the other reactive frameworks, the important concept is dependency tracking. The framework understands which values influence the result and can avoid unrelated recomputation.

What These Approaches Have in Common

Despite their different APIs, these mechanisms address a related problem.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
        Application Data
               |
               v
        Derived Calculation
               |
               v
       Dependency Tracking
               |
       +-------+-------+
       |               |
   Unchanged         Changed
       |               |
       v               v
  Reuse Result      Recalculate

The differences lie in how dependencies are identified, when calculations execute, how results are cached, and how changes propagate through the rendering system.

This is why learning the underlying concept is more valuable than memorizing a particular framework’s API.

Once we understand memoization and reactive dependency tracking, it becomes easier to reason about performance regardless of the framework being used.


11. Memoization and Derived State

One of the most useful applications of memoization is optimizing derived state.

Derived state is information that can be calculated from existing application state.

Consider a shopping cart.

1
2
3
4
const items = [
  { price: 500, quantity: 2 },
  { price: 1200, quantity: 1 }
];

The total can be calculated directly.

1
2
3
4
5
6
function calculateTotal(items) {
  return items.reduce(
    (sum, item) => sum + item.price * item.quantity,
    0
  );
}

There is usually no need to maintain a separate authoritative total that must be manually updated whenever the cart changes.

Doing so introduces a synchronization problem.

1
2
3
4
5
6
7
Cart Items Change
        |
        v
Update Stored Total
        |
        v
Keep Both Values Consistent

Instead, the total can remain derived from the cart items.

1
2
3
4
5
6
7
Cart Items
    |
    v
Calculate Total
    |
    v
Display Total

If the calculation is expensive, memoization can optimize it without turning the result into independent mutable state.

This distinction is important.

Memoization does not replace state management.

It can optimize the computation of values derived from state.

For small shopping carts, recalculating the total is usually inexpensive enough that memoization is unnecessary.

For a complex financial application performing substantial aggregation across large datasets, memoization may be valuable.

The same architectural principle applies to both cases.

Derived information should generally remain derived information, while performance optimizations should be introduced only when the computation justifies them.


12. Memoization Does Not Fix Inefficient Algorithms

One of the biggest mistakes in performance optimization is using caching to compensate for an inefficient algorithm.

Imagine a product search function that processes a very large catalogue.

1
2
3
4
5
6
7
function searchProducts(products, query) {
  return products.filter(product =>
    product.name
      .toLowerCase()
      .includes(query.toLowerCase())
  );
}

Suppose the catalogue contains 500,000 products.

Every time the search query changes, the function must scan the collection.

Memoization might help if the user repeats exactly the same search.

But consider what happens while the user types.

1
2
3
4
5
6
"l"
"la"
"lap"
"lapt"
"lapto"
"laptop"

Each query is different.

The cache cannot reuse a result associated with a different query unless the implementation supports a more sophisticated incremental search strategy.

As a result, the expensive operation still runs repeatedly.

This is not a memoization problem.

It is an algorithmic and architectural problem.

Potential improvements include building a search index, reducing the amount of data processed, performing filtering on the server, or using a Web Worker to keep heavy computation away from the main thread.

For example, a simple optimization is to normalize the query once instead of repeatedly converting it to lowercase inside the filter callback.

1
2
3
4
5
6
7
8
9
function searchProducts(products, query) {
  const normalizedQuery = query.toLowerCase();

  return products.filter(product =>
    product.name
      .toLowerCase()
      .includes(normalizedQuery)
  );
}

This reduces some repeated work, although the algorithm still scans the collection.

A more substantial improvement might involve precomputing normalized product names or using a dedicated search index.

The important distinction is that memoization avoids repeating previous computations, while algorithmic optimization reduces the cost of computations that genuinely need to happen.

Both can be valuable, but they solve different problems.


13. When Other Optimizations Are More Appropriate

Memoization is only one tool in frontend performance engineering.

Consider an application rendering 100,000 table rows.

Even if every row is memoized, the browser still needs to manage a large DOM tree.

The application may suffer from expensive layout, painting, memory consumption, and event handling.

In this situation, virtualization may provide a greater improvement.

1
2
3
4
5
6
7
8
9
10
100,000 Records
       |
       v
Virtualized List
       |
       v
Visible Rows Only
       |
       v
Smaller DOM

Similarly, if a computation takes several hundred milliseconds every time its inputs legitimately change, moving the calculation into a Web Worker may improve responsiveness.

Memoization cannot prevent that calculation from running when a new result is required.

But a Web Worker can execute suitable computations away from the main UI thread.

Another possibility is changing state ownership.

If an unrelated UI update causes expensive components to recalculate, moving state closer to the components that need it may reduce unnecessary update propagation.

The correct optimization depends on the actual bottleneck.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Repeated Same Calculation
          |
          v
      Memoization


Expensive New Calculation
          |
          v
  Better Algorithm / Worker


Too Many DOM Elements
          |
          v
      Virtualization


Too Much Update Propagation
          |
          v
   Better State Architecture


Repeated Network Requests
          |
          v
      Data Caching

Understanding these distinctions prevents us from applying memoization to problems it was never designed to solve.


14. A Practical Example: Financial Analytics Dashboard

Let’s bring everything together using a realistic frontend scenario.

Imagine an application that processes financial transactions and displays spending analytics.

The application receives a collection of transactions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const transactions = [
  {
    id: 1,
    category: "Food",
    amount: 1500
  },
  {
    id: 2,
    category: "Transport",
    amount: 800
  },
  {
    id: 3,
    category: "Food",
    amount: 2200
  }
];

We want to calculate the total spending for each category.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function calculateCategoryTotals(transactions) {
  const totals = new Map();

  for (const transaction of transactions) {
    const previous = totals.get(transaction.category) ?? 0;

    totals.set(
      transaction.category,
      previous + transaction.amount
    );
  }

  return [...totals.entries()]
    .map(([category, amount]) => ({
      category,
      amount
    }))
    .sort((a, b) => b.amount - a.amount);
}

For a small collection, this calculation is inexpensive.

For hundreds of thousands of records, it may become significant, particularly if the application repeatedly performs it during unrelated UI updates.

We can introduce a memoization mechanism based on the transaction collection’s identity.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function memoizeLast(fn) {
  let hasCachedResult = false;
  let lastInput;
  let lastResult;

  return function (input) {
    if (hasCachedResult && Object.is(input, lastInput)) {
      return lastResult;
    }

    lastInput = input;
    lastResult = fn(input);
    hasCachedResult = true;

    return lastResult;
  };
}

const getCategoryTotals = memoizeLast(
  calculateCategoryTotals
);

This implementation remembers only the most recent calculation.

That makes it suitable for situations where the same collection is processed repeatedly before being replaced by a new one.

1
2
3
4
5
const first = getCategoryTotals(transactions);

const second = getCategoryTotals(transactions);

console.log(first === second); // true

The second call reuses the cached result.

However, if we create a new array, the function recalculates.

1
2
3
4
5
6
7
8
9
10
const updatedTransactions = [
  ...transactions,
  {
    id: 4,
    category: "Utilities",
    amount: 3000
  }
];

const updated = getCategoryTotals(updatedTransactions);

This is appropriate because the input collection changed.

The implementation assumes that transaction arrays and their relevant contents are treated immutably. It also assumes that calculateCategoryTotals is deterministic for those inputs.

Understanding the Result

Our memoization function doesn’t know anything about React, Angular, Vue, or Svelte.

It simply remembers the result of a computation associated with a particular input reference.

A frontend framework may provide a more integrated mechanism for tracking dependencies and controlling when calculations execute.

But the underlying optimization remains recognizable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Transaction Collection
          |
          v
  Has Input Changed?
          |
      +---+---+
      |       |
     No      Yes
      |       |
      v       v
  Use Cache  Calculate
      |       |
      |       v
      |    Store Result
      |       |
      +---+---+
          |
          v
   Category Summary
          |
          v
     Dashboard UI

This example also illustrates the limits of memoization.

If a new transaction collection is created on every update, the cache may miss repeatedly.

If the original collection is mutated in place, the cache may return an outdated result.

If the calculation is already extremely cheap, the cache may provide little benefit.

Memoization works best when the application has predictable data ownership, reliable change detection, and expensive calculations whose inputs remain stable across repeated uses.


15. Measuring Whether Memoization Actually Helps

Performance optimization should begin with measurement.

Before introducing memoization, we should understand how much time the calculation consumes.

JavaScript provides a simple way to measure execution time.

1
2
3
4
5
6
7
const start = performance.now();

const result = calculateCategoryTotals(transactions);

const duration = performance.now() - start;

console.log(`Calculation took ${duration}ms`);

However, a single measurement can be misleading.

JavaScript execution times vary because of engine optimization, garbage collection, background browser activity, dataset size, and hardware differences.

For more reliable comparisons, we should measure repeated executions using representative data and compare the original implementation with the memoized version.

We also need to distinguish between cache hits and cache misses.

A memoized function may appear extremely fast when the cache is warm, but the first calculation still incurs the full computational cost.

If most real-world calls are cache misses, the optimization may not deliver the expected improvement.

For frontend applications, browser performance tools can reveal whether calculations contribute to long JavaScript tasks.

Framework-specific profilers can provide additional information about rendering and reactive updates.

The most useful measurements answer practical questions.

How often does the calculation execute? How much time does it consume? How frequently do its inputs change? How much memory does the cache retain? Does the optimization improve actual user interactions?

These questions are more valuable than simply counting the number of memoized functions in a codebase.


16. When Memoization Makes Applications Worse

Memoization can make applications slower, more memory-intensive, and harder to maintain when used without a clear reason.

Consider a simple expression.

1
const fullName = `${firstName} ${lastName}`;

There is little reason to introduce a cache for this operation.

The computation is cheap, and the cache would require additional bookkeeping.

Now consider a large memoization cache that stores every search result indefinitely.

The application might avoid some repeated filtering operations, but it could also retain thousands of arrays that are never used again.

That memory consumption may increase garbage-collection pressure and degrade performance elsewhere.

Memoization can also introduce correctness problems when dependencies are incomplete or when mutable data changes without changing the reference used as a cache key.

These problems are particularly difficult to diagnose because the application may appear correct until a specific sequence of updates occurs.

For example, a financial summary might continue displaying an outdated total even though the underlying transaction collection has changed.

The interface looks functional, but the data is wrong.

That is a much more serious problem than an unnecessary recalculation.

For this reason, correctness must always take priority over caching.

A slower correct result is preferable to a fast incorrect result.

The objective should be to achieve both correctness and performance, but not by compromising the assumptions that make the calculation reliable.


17. A Better Mental Model for Memoization

A useful way to approach memoization is to treat it as an engineering decision rather than a default coding pattern.

Start by identifying the calculation and understanding how frequently it runs.

Then determine whether the inputs often remain unchanged.

If the calculation is inexpensive, there may be no reason to cache it.

If the calculation is expensive but every call receives different inputs, memoization may still provide limited value.

If the calculation is expensive and repeatedly receives equivalent inputs, memoization becomes a stronger candidate.

Finally, consider whether the cache can identify those inputs reliably and whether the memory and maintenance costs are acceptable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
                Calculation
                     |
                     v
            Is It Expensive?
                     |
              +------+------+
              |             |
             No            Yes
              |             |
              v             v
        Keep Simple    Is Work Repeated?
                            |
                       +----+----+
                       |         |
                      No        Yes
                       |         |
                       v         v
                   Optimize   Are Inputs
                   Algorithm  Often Stable?
                                 |
                            +----+----+
                            |         |
                           No        Yes
                            |         |
                            v         v
                         Consider   Evaluate
                         Other      Memoization
                         Solutions      |
                                        v
                                  Measure Benefit

This approach helps avoid premature optimization.

It also encourages us to consider whether a different solution, such as a better algorithm, a smaller dataset, or improved state architecture, would provide a larger benefit.


Bringing It All Together

Memoization is a general optimization technique that applies far beyond frontend development.

Its purpose is to avoid repeating computations when previously calculated results can be safely reused.

In frontend applications, this becomes particularly useful because interfaces frequently respond to changing state, and those updates may cause calculations to execute repeatedly.

However, memoization is not the same as preventing all rendering work.

It does not automatically reduce the size of the DOM, eliminate network requests, improve inefficient algorithms, or prevent the browser from performing layout and painting.

It operates at the computation layer.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
             Application State
                     |
                     v
              State Changes
                     |
                     v
            Derived Computations
                     |
                     v
              Memoization
                     |
          +----------+----------+
          |                     |
       Cache Hit             Cache Miss
          |                     |
          v                     v
     Reuse Result          Recalculate
          |                     |
          |                     v
          |                 Store Result
          |                     |
          +----------+----------+
                     |
                     v
              Updated UI Data
                     |
                     v
              Framework Update
                     |
                     v
              Browser Rendering

Different frontend frameworks provide different mechanisms for applying these ideas.

React exposes explicit memoization APIs and compiler-assisted optimizations. Vue provides cached computed properties. Angular offers computed signals and pure pipes. Svelte uses dependency-driven derived values.

Although the implementations differ, they share a common objective: avoid unnecessary work when the information needed to produce a result has not meaningfully changed.

The effectiveness of these mechanisms depends on several factors.

The computation must be expensive enough to justify caching. The inputs must be identifiable. The cached result must remain valid. The cache must not consume unreasonable resources. Most importantly, the optimization should improve measurable application behavior.

This is why memoization should be considered part of a broader performance strategy rather than a universal solution.

Sometimes the correct answer is to cache a result.

Sometimes it is to use a better algorithm.

Sometimes it is to reduce the amount of data being processed.

Sometimes it is to restructure state ownership so that unrelated updates do not trigger expensive work.

And sometimes the correct answer is to leave a simple calculation alone.


Final Thoughts

Memoization is one of the clearest examples of how performance engineering involves trade-offs rather than universal rules.

At its core, the idea is simple. If a calculation has already been performed and its inputs have not changed, we may be able to reuse the previous result instead of performing the same work again.

But deciding whether that reuse is worthwhile requires understanding the cost of computation, the reliability of cache keys, the behavior of mutable data, the lifetime of cached results, and the memory consumed by the cache.

A well-designed memoization strategy can make a complex application significantly more responsive, especially when expensive calculations are triggered repeatedly by unrelated interface updates.

A poorly designed strategy can introduce stale results, memory pressure, and unnecessary complexity while delivering little measurable improvement.

The goal is therefore not to memoize as much as possible.

It is to understand where the application spends its time and determine whether remembering a previous result is cheaper than calculating it again.

That distinction separates deliberate performance engineering from optimization by habit.

So before introducing memoization, ask one question:

Is the cost of remembering this result actually lower than the cost of calculating it again?

If the answer is yes, memoization can be a powerful optimization.

If the answer is no, the simpler implementation is often the better engineering decision.


What’s Next?

Next in Beyond the UI: Understanding Modern Frontend Engineering:

Server State vs Client State: Why They Shouldn’t Be Treated the Same

Throughout this article, we have focused on caching computation. We have explored how an application can reuse previously calculated results when the inputs remain unchanged.

But frontend applications also work with information that originates outside the browser.

A dashboard retrieves transactions from an API. An e-commerce application fetches products and inventory. A banking interface displays balances that may change independently of the current browser session.

In these situations, the challenge is no longer simply avoiding repeated calculations.

The challenge is keeping a local representation of remote information synchronized with its authoritative source.

We will explore why server state and client state represent fundamentally different problems, how caching and freshness affect remote data, and why tools such as TanStack Query, SWR, and other server-state mechanisms exist.

Because remembering the result of a calculation and keeping remote information synchronized are two very different responsibilities.

This post is licensed under CC BY 4.0 by the author.