Related: 5 UX Design Lessons I’ve Learned
You might have noticed a glaring ambiguity in that explanation — the object “is no longer needed.”
How do we decide that?
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
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.
Now you can see the key to working effectively with the garbage collector is:
- Being thoughtful about how and when we consume memory so the garbage collector does not have to run often to free up space
- 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:
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 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 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:
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
height can be replaced by
scaleY, respectively. For
shadow animations, check out Jonas Treub’s workaround using child layers as shadows and animating their visibility as needed.
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.