Code Concepts for Designers: JavaScript Memory Management for Smooth UI Prototypes

JavaScript Memory Management

Have you ever set out to build a robust, high-fidelity UI prototype in a code-based tool like Framer, only to find that it suddenly begins to behave poorly, with janky animations and delayed responses? JavaScript-based prototypes are really just single page web applications, and they’re prone to the same memory management problems as any web app. Memory leaks can present themselves unexpectedly and throw everything out of whack, making your prototype instantly unpresentable until the issue is resolved.

But “memory management” sounds out of our knowledge scope as UX designers. We might recognize the mysterious process of garbage collection is important, but when a Google search surfaces articles about heaps, generational theory, and scavenging, it’s natural to wonder if you need a four-year degree to gain a practical understanding of memory management. This guide is intended for UX designers with some level of familiarity with Framer code, but no in-depth knowledge of JavaScript.

In this article, I’ll give you the fast facts on JavaScript memory management that can help you understand why your prototype is misbehaving. Once you’re equipped with the knowledge to resolve memory management issues, you’ll be able to create polished, high-performance products free of lag and flickering animations.

Related: 5 UX Design Lessons I’ve Learned

The Basics of JavaScript Memory Management

Memory and the JavaScript Engine

You don’t have infinite amounts of memory for a programming task. When a JavaScript application (such as a Framer prototype) is running in the browser, some mysterious force must step in so we don’t consume all the memory available for things we don’t need. This force is called the JavaScript engine, and it is accomplished through a continuous process of memory allocation, usage, and release.

Everything in JavaScript is an object (even your functions!). Every time a new object is created, it needs a chunk of memory. This memory is used from a pool of memory called the heap; it is allocated to the object that needs it. The object will occupy memory for as long as it is needed by the program. Once the object is no longer needed, the memory must be released to be recycled for other purposes.

You might have noticed a glaring ambiguity in that explanation — the object “is no longer needed.”

How do we decide that?

We don’t. The JavaScript engine does. And there’s the problem.

The JavaScript engine includes a process called “garbage collection,” during which it periodically scans through all memory currently in use and decides what to release. This process takes time, depending on how much work it has to do. Framer animations generally run at 60 fps, so if garbage collection takes longer than 16 ms, then your prototype will start to skip frames causing visual issues.

Garbage collection is a convenient service and works well most of the time, but sometimes it doesn’t do what we might expect. In these cases, we can run into memory management issues. When resolving these issues, it helps to know how the garbage collector makes decisions.

Behind the Curtain of Garbage Collection

First of all, garbage collection is a completely automated task; it can’t happen manually. You are at the mercy of the JavaScript engine. You can’t solve memory management problems by trying to explicitly direct garbage collection yourself.

The issue of when garbage collection takes place is, practically speaking, impossible to predict. It is not a continuous process, but rather runs in intervals. Its timing depends on when memory is running low enough to prompt the garbage collector to run. You aren’t going to have a lot of luck trying to time your garbage collection, so it’s important to write your code in a way that lets the process run smoothly in the background.

The garbage collector will “take out” any objects in memory that can’t be reached from the root global object. For our purposes, we can think of it as releasing memory occupied by any objects that are not being referenced. So when you create an object and reference it with a variable name, the JavaScript engine allocates memory to it. The garbage collector will see the variable name referencing the object (provided that it is in the current scope) and lets it live. But if we redefine the variable to point to a different object, then that initial object is no longer referenced, and the garbage collector will release its memory.

The garbage collector takes time. Different browsers feature different JavaScript engines (for example: V8 in Chrome and WebKit with Riptide in Safari), and they approach parts of the garbage collection process in different ways to maximize efficiency. But the core problem garbage collection presents is the lengthy process that sometimes surpasses the 16 ms threshold. Then our prototype starts skipping frames.

Now you can see the key to working effectively with the garbage collector is:

  1. Being thoughtful about how and when we consume memory so the garbage collector does not have to run often to free up space
  2. Letting the garbage collector do its job well by properly removing all references to defunct objects

If you follow these guidelines, application memory will become available and it specifically ensures we don’t consume too much memory at one time (such as creating layers during an animation). Too much memory will prompt the garbage collector to run and results in a messy animation.

3 Tips for High-Performance Prototypes That Never Skip Frames

1. Destroy All References To Layers You No Longer Need

When finished with an object (such as a layer), we need to make sure there are no references to the object in the code’s execution. If there are any references, the garbage collector will assume the object is in use and will not release its memory. This can get unwieldy very quickly.

In Framer, this concept becomes relevant when dealing with removing layers. Let’s say you’ve created a simple layer in your prototype:

layerA = new Layer
    x: 100
    y: 100

This layer exists in your DOM (that is, it is rendered on the screen) and occupies memory. layerA is a reference to this DOM element. So if you later want to remove this layer:

layerA.destroy()

You might reasonably assume the garbage collector will come through and release the memory that this object was occupying. This is not the case! destroy() serves to remove the object from the DOM, but layerA itself is still the same reference it was before. It’s referencing an object that is no longer rendered in the DOM, but that object (regardless of its visibility in the prototype) will continue to exist as long as layerA references it. The solution, then, is to remove the reference by redefining the variable:

layerA = null

Now the garbage collector can come through and release the memory.

2. Unbind Event Listeners that You No Longer Need

Your prototype probably includes some number of event listeners that add interactivity to the experience. But you can waste a lot of memory if you aren’t removing event listeners when you’re finished with them.

The callback function you give to your event listener will stay in scope long after the event is triggered. For events that can be triggered repeatedly at any time, this is a desirable behavior. But if you have an event that should only fire once, such as upon loading, keeping the objects referenced inside this callback function from being garbage collected is a waste.

There are two solutions to this: off and once

off is the reverse of the on function that creates an event listener. (Even when you are using a shorthand to bind an event listener — such as layerA.onTap — the CoffeeScript compiles the same as if you had used layerA.on.) The example usage from the Framer docs:

layerA = new Layer
layerA.name = "Layer A"
 
clickHandler = (event, layer) ->
    print "Clicked", layer.name
 
layerA.on(Events.Click, clickHandler)
layerA.off(Events.Click, clickHandler)

An easier and cleaner way of going about this for events that only fire a single time is to use the once function. once works just like on, but unbinds itself after the first event.

layerA = new Layer
layerA.name = "Layer A"
 
clickHandler = (event, layer) ->
    print "Clicked", layer.name
 
layerA.once(Events.Click, clickHandler)

Once events are unbound, the garbage collector is free to release any memory occupied by objects inside the event callback.

3. Be Careful How Many Layers You Create at Once

When you create lots of layers at once (including a complex layer with several children), you consume a lot of memory very quickly. This makes it much more likely that the garbage collector will be called to make more memory available for usage. This can be a very big problem, particularly if you are animating something at the same time. If the garbage collector runs mid-animation, then your animation may skip frames and look choppy or slow. Similarly, if the garbage collector runs at a point when you expect to interact with the user, your user may find a lack of responsiveness or considerable delay at these points while the garbage collector finishes its job before handling any user input.

It is common to run into this problem when you are creating complex layers iteratively, such as within a loop. More layers, more problems — whether this means your loop runs a large number of times, or each execution of the loop creates a large number of layers. Avoid this wherever possible, especially when animation is involved at the point when layers are created.

Bonus: Avoid These Specific Animations

Another powerful performance tip that doesn’t strictly concern garbage collection is to avoid specific types of animations that require use of the CPU. Most Framer animations make use of the GPU, which is made for this kind of thing and does it super quickly. The CPU, on the other hand, slows things down considerably. Luckily, there are only a few animations that put a burden on the CPU (but they’re common ones!).

Here are the common animations you should avoid, if you can help it:

  • width and height
  • scrollX and scrollY
  • borderRadius
  • borderWidth
  • backgroundColor
  • shadow

In general, any animations involving masking also place a burden on the CPU.

If these are properties that you do need to animate, there are some effective work-arounds to let the GPU handle them instead. Animations to width and height can be replaced by scaleX and scaleY, respectively. For shadow animations, check out Jonas Treub’s workaround using child layers as shadows and animating their visibility as needed.

Conclusion

This basic introduction to JavaScript’s garbage collection explains the essential information to write smooth, lag-free prototypes. Good memory management habits will set you up for success and put users’ focus where it belongs — on the design and experience, not on odd behavior or choppy transitions.

If you’re interested in learning more about performance optimization in Framer, the Framer docs are a good place to start. You can also search Framer’s support community on Facebook for answered questions related to performance. And, of course, feel free to leave questions in the comments.

For more information related to JavaScript memory management and garbage collection, start with Mozilla’s awesome intro to the topic. For practical guidance with JavaScript memory leaks aimed at front-end developers, check out these detailed (and super useful) tips at Smashing Magazine, written by Google engineer Addy Osmani. And if you’re curious and really want a deep dive into garbage collection mechanisms, take a look at Irina Shestak’s fun illustrated introduction to memory management in Chrome’s V8 engine.

0 replies

Leave a Reply

Want to join the discussion?
Feel free to contribute!

Leave a Reply

Your email address will not be published. Required fields are marked *