Optimizing with Memoization
Cache is tempting because it sounds like a quick answer to unoptimized code. But remembering the result is only the beginning. You still need to know what the key is, when the result becomes invalid, and whether you might return data to the wrong user. That is what memoization is about.

From this article you will learn:
What is memoization?
How is memoization different from regular caching?
Why is memoization not only a frontend tool?
What does memoization look like in React, Vue, Angular, and Java?
Does memoization make sense on the backend?
What are the costs, traps, and limits of this technique?
Optimization often starts with an uncomfortable feeling that the program is doing too much. At first glance everything works correctly. Data is displayed, the API response comes back, the report is generated, and the component renders. Only after a while do we notice that the same work is being done several, dozens, or hundreds of times.
It may be filtering a large product list in a frontend application. It may be recalculating user permissions in several places during one request. It may be a parser rebuilding the same structure again and again. It may also be the classic recursive Fibonacci example, which shows the problem very well, even if it is rarely the real business problem.
This is where memoization appears. Not as magic, not as decoration for code, and not as a React feature. Memoization is the idea of not calculating the same thing twice if we already know the result.
It sounds simple. And that is exactly why it is worth pausing for a moment, because in real projects the hardest part is not saving a result. The hardest part is answering whether that result is still true.
Where Does the Problem Come From?
Imagine a simple shop application. We have a list of products and we want to display only the available ones.
There is nothing suspicious here. The function is short, readable, and probably fast enough for a small list. The problem starts only when the list is large, the function is called often, and the input data stays the same most of the time.
That is the moment when it is worth asking: do we really need to do the same work again?
The answer is not always "no". Sometimes the calculation is so cheap that adding cache will hurt more than help. Sometimes the data changes so often that we almost never hit the remembered result. Sometimes the real problem is somewhere else: in a badly written database query, too many renders, or an inefficient algorithm.
Memoization makes sense only when we have repeatable work and we can safely recognize that the input is the same.
What Is Memoization?
Memoization is an optimization technique based on remembering the result of a function for specific input data. If later we call the same function with the same data, we can return the previously calculated result instead of calculating it again.
In short:
the first call still costs the same,
later calls may be cheaper,
we pay with memory for time,
we need a key that identifies the input data,
we need to know when the remembered result becomes invalid.
Memoization is a specific form of caching. Cache is a broader term. We can cache HTTP responses, database query results, static files, view fragments, or data in Redis. Memoization is closer to functions: for arguments A, remember result B.
It works best when the function is deterministic, meaning it always returns the same result for the same arguments.
If we call square(4), we always get 16. Here memoization is easy to understand. The only question is whether it makes sense performance-wise, because multiplication itself is cheaper than handling the cache.
A Simple Example in JavaScript
Let's write the simplest possible memoize function.
We can use it like this:
The first call for 100 performs the actual calculation. The second call for 100 returns the result from memory (Map). For 200 we have a different key, so the function has to calculate the result again.
This example shows the idea well, but it does not solve several practical problems yet. What do we do with a function that has multiple arguments? What about objects? Can the cache grow forever? Is the result still true after an hour? These questions appear very quickly in real code.
The Problem with Keys
In memoization, the key is just as important as the result. If we build the key incorrectly, we may either not use the cache at all, or worse, return a result for the wrong data.
In JavaScript it is easy to fall into a trap with objects.
At first glance both objects look the same. For Map, however, they are two different keys, because objects are compared by reference, not by structure.
We can try to solve this with JSON.stringify.
Sometimes that is enough, but I would not treat it as a universal answer. Property order, data types, dates, functions, undefined values, cyclic structures, and large objects can make this strategy unreliable or too expensive.
In other words: memoization starts with cache, but very quickly becomes a question of modeling data identity.
Memoization Beyond the Frontend
In recent years many developers have associated memoization mostly with React. It is easy to understand why. useMemo, useCallback, and memo are very visible parts of the API. But memoization is not a React idea, and it is not even a frontend idea.
Let's take a classic example in Java.
Without memoization, recursive Fibonacci calculation repeats many of the same computations. calculate(40) needs calculate(39) and calculate(38), but calculate(39) again needs calculate(38). And so on. The program keeps circling around the same values.
After adding a map, each value is calculated once. Later it is only read from cache.
This is a good teaching example, but it is worth keeping a clear head. In production code we would need to ask a few questions: is HashMap safe under concurrent access, does the cache have a limit, can the result become stale, and is recursion the best way to solve this particular problem?
The Java example matters for one reason: it shows that memoization is an idea, not a feature of a specific framework.
Does Memoization Make Sense on the Backend?
Yes, but on the backend we need to be more careful than in a typical frontend example.
On the frontend, memoization often protects us from another render, another list filter, or another function reference. On the backend, a remembered result may be shared between requests, users, tenants, or application instances. That immediately raises the stakes.
Memoization on the backend makes sense when:
during one request we repeat an expensive operation,
we have a long-lived process, for example a worker, a queue consumer, a JVM application, Node.js, RoadRunner, or Swoole,
the result is deterministic or has a clearly defined lifetime,
we can describe the key and the moment when the result should be invalidated,
we know that the result is safe for the request that reads it.
Examples? Recalculating user permissions, parsing schemas, compiling validators, transforming configuration, expensive domain calculations, reference data, or query results, if we have a clear refresh rule.
Notice that last part: "if we have a clear refresh rule". This is the point where memoization starts becoming the broader topic of caching.
If a function depends on a database, an external API, time, configuration, or the current user, a simple in-memory map may be too weak a tool. Then we need a strategy: TTL, result invalidation, size limits, metrics, protection against cache stampede, and sometimes an external cache such as Redis or Memcached.
The PHP Case: a Request Lives Briefly
It is worth mentioning PHP separately, because it is a good example of a language where the answer to "should we memoize on the backend?" is: it depends on how the application is run.
In classic PHP, for example with PHP-FPM (Magento, WordPress), each request has its own application lifecycle. The FPM process may later handle another request, and mechanisms such as OPcache or APCu can store data beyond the lifecycle of a single request. The state of objects created by the application for a specific request, however, is not preserved between requests.
That means a cache like this:
can help only within a single request. If several layers of the system ask about the same permission during one application run, we save repeated work. After the request ends, that memory disappears.
So it is not true that memoization in PHP never makes sense. It is true, however, that in the classic PHP model, in-process memoization is not a cache between requests.
If we want to speed up later requests, we need another mechanism: Redis, Memcached, APCu, Symfony Cache, HTTP cache, database cache, or materialized data on the infrastructure side. It is also worth remembering that APCu is local to a process or server, so it does not solve the same problems as a shared cache such as Redis in an environment with multiple application instances.
The situation changes with long-lived processes: queue workers, CLI applications, RoadRunner, Swoole, or Laravel Octane. There memory may live longer, so memoization starts to look like cache between operations. We gain more possibilities, but also more risks: stale data, memory leaks, and accidental state sharing.
Memoization in React, Vue, and Angular
Since memoization is a general idea, let's look at how different frontend frameworks solve a similar problem.
React: Explicit Optimization
In React we usually point out ourselves what should be remembered and what it depends on.
useMemo remembers the result of a calculation as long as the dependency list does not change. We should not base program correctness on this, though. React treats useMemo as a performance optimization and, in some situations, may discard the remembered value. useCallback works similarly, but remembers a function reference. memo lets us reduce component re-renders when its props have not changed. By default, React compares props with Object.is, so a new object, array, or function created during rendering will still be treated as a reference change.
This gives a lot of control, but it requires care. If the dependency list is wrong, the result may be stale. If we memoize everything just in case, the code becomes harder while the gain may be zero.
Vue: Cache as Part of Reactivity
In Vue, the natural place for this kind of calculation is computed.
Vue automatically tracks reactive dependencies used inside computed. If products does not change, the result is returned from cache. If a dependency changes, Vue recalculates the value on the next read.
In practice this means less manual dependency management than in React. Vue also has v-memo, which can skip updating a fragment of the template tree, but the documentation treats this mechanism as a micro-optimization for rare, performance-critical cases.
Angular: Computed Signals and Pure Pipes
In newer Angular, computed signals play a similar role.
Angular remembers the result of computed and invalidates it when a dependent signal changes. computed is also lazy, so the calculation does not have to run immediately after declaration, but only when someone reads the result.
Angular also has pure pipes. By default, a pipe does not need to run a transformation again if the input has not changed. This is not identical to manually memoizing many results in a map, but it solves a similar problem in templates: do not transform the same input twice.
What Does This Tell Us?
React more often asks us to manually describe what should be remembered and when. Vue and Angular more often rely on reactivity, which tracks dependencies automatically.
This does not mean one model is always better. React gives control, Vue and Angular remove part of the work from the developer. In all cases, however, the principle is the same: do not repeat work if the input has not changed and the result is still valid.
What Can Go Wrong?
Memoization is tempting because it is easy to show in a short example. Unfortunately, in real code cache rarely stays a small map with three entries.
The first problem is memory. If the cache has no limit, it can grow forever. In a frontend application this means higher memory usage in the browser. On the backend it can lead to problems with a process, a worker, or the whole application instance.
The second problem is data freshness. A result may be correct at 10:00 and wrong at 10:05. This is especially true for data from databases, permissions, prices, stock levels, and configuration.
The third problem is security. If the cache key does not include the user, tenant, language, currency, or permission context, we may return data prepared for someone else. That is no longer a small optimization, but a security bug.
The fourth problem is the cost of memoization itself. For cheap functions, handling the cache may be more expensive than running the calculation again. If a function only adds two numbers, memoization is like building a warehouse for one screw.
And finally: memoization can make debugging harder. The program not only executes code, but also carries state from previous calls. When that state is hidden, finding a bug can become much harder.
When Is Memoization Worth It?
In my opinion, memoization makes sense when we can answer a few questions clearly.
Is the function often called with the same data? Is the calculation expensive? Is the result predictable? Is the memory cost acceptable? Do we know when the result becomes invalid? Can we measure the gain?
If the answers are concrete, memoization can be a very good tool.
Good candidates include:
expensive mathematical calculations,
parsing or compiling schemas,
configuration transformations,
derived values in a reactive system,
repeated permission checks within one request,
recalculating reference data,
filtering large lists when the input often repeats.
It is worth noting, however, that memoization does not replace a better algorithm. If the problem is a bad SQL query or fetching ten times more data from an API than needed, cache may only hide the issue.
When Is It Better to Let It Go?
It is better to let it go when the function is cheap, arguments are almost always different, or the result depends on time, randomness, a database, an external API, or the current user, and that dependency is not explicitly included in the key and the result invalidation strategy.
We also need to be careful when we cannot safely build the key. If we do not know exactly what distinguishes one result from another, the cache will rely on trust. And that is a weak strategy for optimization.
I would also avoid memoizing "just in case". This is a common mistake in React, but not only there. Someone sees useMemo, assumes that more memoization means more performance, and after a few weeks the project is full of dependencies nobody wants to touch. The gain is invisible, the readability cost remains.
In an ideal world, optimization starts with measurement. In practice we do not always have a full performance profile, but it is still worth setting a simple bar: can I point to repeatable, expensive work that memoization actually eliminates?
Summary
Memoization is not strictly a frontend or backend technique. It is a way of thinking about repeatable work. If a program performs the same expensive calculation for the same data twice, we can consider remembering the result.
I would not start with memoization, though. First it is worth understanding the problem, then finding the repeatable calculation, and only then deciding whether caching the result actually makes the situation simpler.
In React we will see useMemo, useCallback, and memo. In Vue it will mostly be computed, sometimes v-memo. In Angular, computed signals and pure pipes. On the backend it may be a map inside a process, a cache within one request, Redis, or another shared mechanism. The tools differ, but the question remains the same.
What is the key? What is the value? When does the result become invalid? How much are we willing to pay for it, in memory?
If we cannot answer these questions, memoization is probably not the solution yet. If we can, it may be one of the simplest and most elegant optimizations we have at hand.




Comments (0)
No one has posted anything yet, but that means.... you may be the first.