Performance is a critical part of most applications. Research continually shows that good performance is essential for a good user experience. Reasonable load times, smooth animations, and responsive interaction gives user a sense of interaction and immersion, whereas slow load times frustrate users, and choppy animation and interaction quickly makes an experience awkward and disconcerting. As developers, we wisely work for better performance by understanding performance characteristics, and verifying performance with tests. However, one aspect of performance can be particularly difficult to assess, and its effect can easily be underestimated. Memory consumption can have a large impact on performance, but in ways that can easily be missed and ignored.
Modern computer architectures rely heavily on layers of caching. Network data is cached on the hard drive, the hard drive gets cached in memory, standard memory memory gets cached in L2 cache, then L1 cache. When code is being executed, its code and data quickly moves up to the L1 cache, where operations, particularly repetitive operations, are extremely fast. When fast upper caches can stay fresh, code execution can remain very fast. What this means is that whenever our code is being loaded and moved up to faster caches, by necessity, some other code is being moved out (or down to lower/slower caches). When we test our code, in isolation, these caches make our code look really fast. But every bit of code that we load and execute and every piece of data that we interact with slightly diminishes the speed of other code, by increasing the chance that it is getting bumped out of caches, to slower storage points.
In economics, there is a concept known as externality, a cost or benefit that is experienced by an outside involuntary party. In simple economics, a voluntary transaction is one in which two parties choose to exchange some goods or services, and since the transaction was voluntary, presumably it benefits both parties. However, what if the transaction also impacts a third party who had no say in the transaction? For example, a commercial real estate development project can significantly impact the value of neighbor’s property, even though they were not part of the transaction. This can be beneficial, or negative. Pollution is a classic example of a negative externality, where I may be be participating in some economic activity that is beneficial for me and those I do business with, but negatively impacts others. An example of a positive externality is bee-keeping, where bee-keepers may benefit from the economic activity of producing and selling honey, but raising bees also has significant side-benefits of pollinating surrounding crops (which may possibly be of greater value than the honey), a benefit for agriculture outside of the bee-keeper and honey purchaser market.
Memory consumption is essentially a computer programming form of externality. Memory consumption may have a measurable cost for our own component, and within the calculus of our own component, that cost may be worth the returns and benefits from whatever code or data is consuming that memory. But it is easy to ignore the externality of our component, that other components or applications experience a slight negative impact from our memory consumption, in much the same way that a real estate developer may focus on the benefits of his own development, while ignoring the broader effects on the community.
The hidden-ness of the drain of memory consumption is more readily visible if we take a high-level view of computer development. With the astonishing improvements in computer hardware over the last few decades, and even improvements in software algorithms, 25 years ago one might have reasonably expected that by 2015 everything in modern software would be so fast as to be basically instantaneous. Yet in reality, my Turbo Pascal compiler 25 years ago would compile programs much faster than many of the builds for modern web applications. Why is this? Because we have focused on individual problems in isolation, making software that can do individual tasks very quickly, without worrying about the growing size. But, the aggregate affect is that our modern applications are dominated by moving around massive amounts of bytes, moving data from one computer to another, one cache to another, and so forth.
Conscientious memory use
So how do we practice conscientious coding? We make it a priority to minimize size and memory consumption, even when the most expedient solution may involve a larger data structure, or pulling in some otherwise unnecessary dependencies.
As a JavaScript developer, we have increasing availability of useful tools for analyzing memory issues. The developer tools in Chrome/Safari provide excellent memory tools, and have also improved in recent versions of Firefox and Internet Explorer. There is a heap snapshot tool that allows to get a snapshot of how much memory is in use, and what objects are in memory. There is also a heap allocation monitor, that allows you to see how memory is consumed over time.
Rendering Architecture
A key decision that has a large effect on memory consumption is how we create our user interface elements. Browsers have generally worked hard to create optimized memory-efficient DOM structures. However, modern web applications often make extensive use of memory-heavy component abstractions.
ReactJS has become very popular lately, and for good reason. ReactJS does an excellent job of extending JavaScript with reactive capability. Reactive programming is an emerging approach for web UI development. And this has been coupled with a virtual DOM abstraction. The virtual DOM has demonstrated impressive performance characteristics. However, the virtual DOM essentially requires an additional in-memory representation of the DOM. Again, this is a perfect example where testing performance in isolation yields excellent results, while large amounts of memory can easily be allocated and consumed without much consideration to its side-effects. In fact, the virtual-dom in-memory abstraction can often consume roughly 5 times as much memory as the real DOM elements it is representing.
With Dojo’s Dijit components, powerful functionality can also be achieved, but restraint is advisable here as well. A single simple TextBox takes about 10-20KB of memory and a DateTextBox about 40-80KB of memory. Yet typically, the DOM structure accounts for less than 5% of that memory usage. Dijit components can often provide enormously valuable functionality, but to create an efficient application, carefully consider where CSS might be sufficient instead of layout widgets, where native inputs might be sufficient, and where other newer HTML elements might be used.
An alternate approach is to use a component architecture for encapsulating element functionality that does not require an entire per-instance memory representation for each element. This approach is one of the goals of xstyle. Using techniques like event delegation (rather than event listeners on each element), and component instances that can manage multiple DOM instances/elements can yield substantial improvements in memory efficiency, since instances of components can consist of nothing more than the requisite DOM elements, with the memory structures of behavioral functionality shared by all instances. This can be particularly important for components or inputs that may exist in grids or other repetitive structures.
Memory Leaks
Memory leaks are one of the most problematic sources of memory consumption. Memory leaks can come from a number of different sources, but certainly the most common class of memory leaks in JavaScript applications is when we have a component that sets up an event listener (or some of other type of connection) on another component or element, and fails to remove the listener when that component is eliminated. Typically in garbage collection (GC) based languages, when a component is no longer referenced, it has the nice property of being automatically eliminated from memory. However, when a reference from another component to a listener within your component still exists, this blocks the collection process, retaining your component in memory.
In Dojo/Dijit
The key principle to remember is that if you are connecting or adding a listener to another component, you need to make sure you are prepared to clean up and remove the connection or listener when your component is finished. Dijit provides a very convenient mechanism for this. Dijit components (that inherit from dijit/_WidgetBase
which inherits from dijit/Destroyable
) provide an own(handle)
that can be used to track handles and automatically remove than when the widget is destroyed. This should be used whenever you are connecting to an element or component outside of your own component (it isn’t necessary for internal components). For example:
// creating your custom component
declare(Base, {
postCreate: function() {
// postCreate is a common place to setup listeners
this.own( // pass in the handle returned from on here
someOtherComponent.on('click', function () {
// listener to another component
...
One particular pernicious opportunity for memory leakage is when connecting to the pub/sub hub through dojo/topic
. This again fits into the class of connecting to an separate component, in this case the pub/sub hub. And while in some cases, forgetting to cleanup a listener to another component might end up not causing problems if the other component is eliminated as well, the pub/sub hub is never eliminated for the life of the page, and a pub/sub listener will never go away if it is not explicitly removed. When using pub/sub from a component, the utmost caution should be used to ensure that you are own()
ing the listener handle, so it can be properly removed.
Naturally another important consideration is ensuring that you are destroying Dijit components when they are no longer in use. Simply destroying a DOM structure is insufficient for cleaning up Dijit components. The Dijit’s destroyRecursive()
method will properly destroy a widget and all its children. If you add a DOM node to a widget that is not a child of that widget’s DOM structure, you can also wrap a reference to that node in an own
call.
Finding Leaks
If you suspect memory leaks exist within your application, how do verify and address them? Leaks are manifested by continually growing memory usage. This can be a little trickier to find than you might think. Modern web architecture is highly optimized and makes extensive use of lazily instantiated structures, like JIT code paths, that might not fully consume memory until it has been used (and maybe not even until used multiple times). This means that process memory may grow for an application while it is being used even if it isn’t actually leaking any memory. To further complicate matters, garbage collection is a very non-deterministic mechanism, and it is very difficult to determine how much process memory might be allocated to unreachable data (or even allocated for future objects), that hasn’t been collected yet.
Fortunately, tools such as the developer tools’ heap snapshot provide a more reliable look at memory usage. We can use this to determine the actual count of different objects to determine if the count is a growing.
A typical scenario that might present an opportunity for leakage is when switching views in an application where some components will be destroyed and others instantiated. As we switch back and forth between views, if components are not being cleaned up properly, a leak might be manifested. An easy way to determine this is to do a heap snapshot, switch back and forth between views, preferably ending up at the same view, so we can return to a state where object allocation levels should be restored, and then doing another heap snapshot to compare object counts. Developer tools even includes comparisons, so we can directly see if any object counts have increased.
A particularly telltale sign of components being leaked is if the detached DOM nodes are increasing. While it is not necessarily the case that all detached nodes are bad, if they are continuing to increase, this is sign that something is not being cleaned up correctly.
Within Dojo and Dijit, if we suspect that Dijit components are not being destroyed, another indicator we can check is the Dijit registry. The registry holds all the instantiated Dijits, until they are destroyed. If we suspect that Dijits aren’t being destroyed, we can check the count of Dijits in the registry, and see if it is growing:
// does this grow if we switch back and forth between views:
require('dijit/registry').length
We can also see the specific list of Dijit components in the registry as an array:
// look in here for widgets that may not be cleaned up
require('dijit/registry').toArray()
These are some tips that might save you some time in tracking down memory leaks. Ultimately memory leaks are a challenging bug to deal with, but because the problem is generally not due to steps that may trigger an easily identifiable error or problem, but rather due to the absence of proper cleanup. And finding the absence of an important step can be difficult and time-consuming.
Dependencies
While memory leaks may represent a demonstrable incorrectness in our application, often a the majority of gratuitous memory consumption isn’t due to leaks, but just lazy consumption of resources. Work hard to ensure that you aren’t pulling in unnecessary dependencies. Many applications load enormous amounts of code, simply loading popular libraries, even if only a tiny portion is actually needed. This is similar to using a Hummer as a commuter car just because you like the seats, even if you have no use for 6 liter engine and 16 inches of ground clearance. Dojo does a great job of encouraging conservative use of resources, with very granular modules. And with AMD, we explicitly declare required modules, which can also help encourage use of only what we really need.
Think Globally, Code Locally
Writing conscientious code with minimal memory consumption, can be thought of as ethical programming. It is about making a conscious effort to not just simply solve the immediate problem in front of you, but doing it in a way that does not bloat and gradually slow down the rest of components and even applications in the system. It is about thinking through your memory structures, your cleanup process, and your dependencies to write code with maximum functionality with minimal footprint.
Learning more
Optimizing application performance is a challenging endeavor, and something we help our customers with on a regular basis. Dojo users can benefit from our Dojo workshops, where we emphasize performance at all times. If you are struggling to solve performance problems or memory challenges with your application, contact us for a free 30 minute consultation to discuss how we can help.