How memoization can speed up your Ruby app
Why performance is important?
App performance is a very hot topic between developers and not only. You might think that most users will stop using your app because of a bad user experience or because of the design. In reality, a slow app can be one of the reasons users are not returning to use it. Most of mobile app users will stop using an app if it takes too long to load. But the problem is not only that they will uninstall your app, but they will most probably choose another competitor.
You can deliver good content through your app but if it takes more than a couple of seconds to deliver it, it’s highly likely that you will lose some users because of the poor performance of the app. It is very important for user retention to keep optimizing your application.
How to improve app performance?
Performance improvements require both front-end and back-end development. An app crash could also be considered a performance issue so we need to monitor not only the API but also the client that consumes the API.
A very common technique for improving app performance on the back-end side is memoization.
In a previous project that I’ve worked on, we had to improve the performance for a slow endpoint. We realized that we are making multiple GET requests to a 3rd party service and we are getting the same result every time because it was not supposed to change anyway. This was adding some extra time to our endpoint and we could quickly solve the issue by using memoization to cache the get response.
Memoization to the rescue!
With memoization we can avoid duplicated work by storing a computed value for future usage.
We should use memoization when we need to cache the result of a method that does time-consuming work.
We can use this pattern for caching:
- a database query
- a long running function
- heavy calculations
- http requests
- repetitive work that doesn’t change
Basic memoization
We are using Ruby or-equals operator for memoizing a value. This means that we only query the database the first time we call the method latest_device and any future call will just return the value of the instance variable @latest_device
class User < ActiveRecord::Base
def latest_device
@latest_device ||= devices.last
end
end
Multi-line memoization
When our code won’t fit on one line, we can extend the memoization pattern to work with multiple lines of code:
class User < ActiveRecord::Base
def contact_number
@contact_number ||= begin
telephone = mobile_phone if mobile_phone?
telephone = land_phone unless telephone
end
end
end
How about nil or false?
If the value that we are trying to cache is nil or false then we need to pay attention to the way we are using this pattern because every time we would call the method, the instance variable used for memoization would be nil, so the value would be computed again and again and…again.
Can we avoid this? Yes, for sure! The or-equals operator is probably not the best option. We would need to differentiate between niland undefined:
class User < ActiveRecord::Base
def latest_device
return @latest_device if defined? @latest_device
@latest_device = devices.last
end
end
Methods with parameters
A method that takes parameters can also be memoized by using Ruby’s Hash
class A
def expensive_calculation(arg)
return @expensive_calculation[arg] if defined? @expensive_calculation
@expensive_calculation ||= Hash.new do |hash, key|
hash[key] = calculate(arg)
end
@expensive_calculation[arg]
end
end
Don’t overuse memoization
Memoization can be an effective way to improve your app performance but it has some drawbacks as well. Memoization does not give you the full benefit of caching because the value is saved only for the life of a web request. This technique should only be applied to expensive operations that will not change their value throughout the lifetime of the cached variable.
Also, you need to think if your calculation can return a falsey result, especially when you are fetching a single record from the db or when you are returning a Boolean value which can be false. In this case, you need to add an extra check for definition.
By using memoization your objects will be lazy evaluated and their state will not be initialized until you need it. But even if it might be expensive, you need to think if lazy evaluation helps you in your context or not. Memoization can be powerful but try not to overuse it!