Search code examples
cachingember.jsember-octane

Best approach to caching in Ember Octane


I have a project running [email protected]. We are currently in the process of migrating from classic to glimmer based components and have come across some expensive computational patterns which would benefit from caching.

My question is, what is the best approach to caching functionality to getters for glimmer components? It looks like there are currently a few ways to do this:

  1. @cached via tracked-toolbox - I believe this was released prior to the ember cached api. I didn't peek under the hood but it has the has a @cached decorator which might collide with future ember @cached.
  2. ember-cache-primitive-polyfill - Mentioned in the Ember docs as a polyfill for the ember cached API (3.22) but the syntax isn't as concise as the @cached decorator
  3. ember-cached-decorator-polyfill - related to RFC566 appears to be based on option 2 with a more ergonomic syntax
  4. Upgrade to 3.22 - Trying to avoid bumping ember unless there is a significant benefit. At a glance, I didn't see @cached included here though.

Any additional insight/guidelines into how expensive a getter should be to warrant it being cached? For example, preventing re-renders seems a fairly obvious use case but there can be a wide range of what developers might consider an "expensive" computation.


Solution

  • There are two categories of things here:

    1. The two @cached decorators.
    2. The caching primitives introduced via RFC 0566.

    In the vast majority of Ember or Glimmer app or normal library code, you’ll just be using the decorator. You’d only ever really reach for the caching primitives if you were building some low-level library code yourself (not never, but not exactly common, either).

    As for the @cached decorators, they have basically the same semantics. The tracked-toolbox version was research that fed into the the development of the primitive that Glimmer ships (and Ember uses), and so ember-cached-decorator-polyfill is implemented using the actual public API—polyfilling it via ember-cache-primitive-polyfill if necessary.


    In terms of the performance characteristics, you don’t even actually need to think about it in terms of preventing re-renders: that’s not how the system works anyway. (See this blog post I wrote last year (2020) for a deep dive on how re-rendering gets scheduled in Ember and Glimmer using the autotracking concepts.) It’s also worth remembering that caching is not free! So it’s not as simple as “this thing costs something, so I should cache it”—the caching has to pay for itself to be worth it, and it costs both memory use and CPU time to create and to check caches.

    With that caveat firmly in mind, I tend to think of “expense” here in the following categories:

    • am I rendering this hundreds or thousands of times?
    • does rendering this cause a long-running computation that will impact render (i.e. on the order of multiple milliseconds)
    • does this trigger asynchronous behavior?
    • (especially) does this trigger an API call?

    In a lot of normal app code, the only getters you’ll really need to decorate with @cached are getters which produce API calls based on the components’ arguments. Since the getter will otherwise be invoked every time it is referenced, you will end up with multiple API calls, which can produce a situation where the apparent state in the UI flips back and forth as references to different promises resolve.